├── .npmrc ├── assets ├── social-preview.png ├── logo.svg ├── BRAND_ASSETS.md └── social-preview.svg ├── bunfig.toml ├── .release-it.json ├── .npmignore ├── .github ├── workflows │ └── ci.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── CODE_OF_CONDUCT.md ├── .cyrusrc.example.json ├── .cyrusrc.local.example.json ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── src ├── utils │ ├── render-markdown.ts │ ├── logger.ts │ ├── error-handler.ts │ └── progress-bar.ts ├── index.ts ├── types │ └── index.ts ├── commands │ ├── detect.ts │ ├── analyze.ts │ ├── quality.ts │ └── compare.ts ├── config │ └── config.ts ├── cli.ts ├── services │ └── ai-service.ts └── analyzers │ └── code-analyzer.ts ├── .gitignore ├── package.json ├── schema └── cyrus-config.schema.json └── CONTRIBUTING.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /assets/social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ali-master/cyrus/HEAD/assets/social-preview.png -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | smol = true 3 | saveTextLockfile = true 4 | logLevel = "debug" 5 | auto = "auto" 6 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@19/schema/release-it.json", 3 | "git": { 4 | "commitMessage": "chore(): release v${version}", 5 | "commit": true, 6 | "tag": true, 7 | "push": true 8 | }, 9 | "github": { 10 | "release": true 11 | }, 12 | "npm": { 13 | "publish": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | src 3 | index.ts 4 | package-lock.json 5 | tslint.json 6 | tsconfig.json 7 | .prettierrc 8 | 9 | # dotenv environment variable files 10 | 11 | .env 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | .env.local 16 | 17 | # misc 18 | .commitlintrc.json 19 | .release-it.json 20 | .eslintignore 21 | .eslintrc.js 22 | 23 | .idea 24 | .vscode 25 | # test 26 | jest.config.js 27 | # coverage 28 | coverage 29 | # docs 30 | docs 31 | 32 | .cyrusrc.json 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | push: 9 | branches: 10 | - master 11 | - develop 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: oven-sh/setup-bun@v2 20 | with: 21 | bun-version: 1.2.15 22 | 23 | - name: 📦 Install dependencies 24 | run: bun install --frozen-lockfile 25 | 26 | - name: 🛠 Build project 27 | run: bun run build 28 | 29 | - name: 🔠 Lint project 30 | run: bun run lint 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.cyrusrc.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/ali-master/cyrus/refs/heads/master/schema/cyrus-config.schema.json", 3 | "aiProvider": { 4 | "name": "openai", 5 | "model": "gpt-4-turbo-preview", 6 | "apiKey": "your-api-key-here", 7 | "temperature": 0.7, 8 | "maxTokens": 4096 9 | }, 10 | "features": { 11 | "securityScan": true, 12 | "performanceAnalysis": true, 13 | "codeGeneration": true, 14 | "refactorSuggestions": true, 15 | "mentorMode": true 16 | }, 17 | "languages": ["javascript", "typescript", "python", "java", "go", "rust"], 18 | "outputFormat": "text", 19 | "detectLanguage": { 20 | "enabled": true, 21 | "confidence": 0.7 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.cyrusrc.local.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cyrus.usestrict.dev/schema.json", 3 | "aiProvider": { 4 | "name": "ollama", 5 | "model": "llama3.2", 6 | "baseURL": "http://localhost:11434/v1" 7 | }, 8 | "features": { 9 | "securityScan": true, 10 | "performanceAnalysis": true, 11 | "codeGeneration": true, 12 | "refactorSuggestions": true, 13 | "mentorMode": true 14 | }, 15 | "languages": ["javascript", "typescript", "python", "java", "go", "rust"], 16 | "outputFormat": "text", 17 | "detectLanguage": { 18 | "enabled": true, 19 | "confidence": 0.7 20 | }, 21 | "localModels": { 22 | "ollama": { 23 | "models": ["llama3.2", "codellama", "mistral"], 24 | "defaultModel": "llama3.2" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | "removeComments": false, 10 | "strictNullChecks": true, 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | "strict": true, 16 | "declaration": true, 17 | "skipLibCheck": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noPropertyAccessFromIndexSignature": false, 22 | "outDir": "./dist", 23 | "rootDir": "./src" 24 | }, 25 | "include": [ 26 | "src/**/*", 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ali Torki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from "@antfu/eslint-config"; 2 | 3 | export default antfu({ 4 | formatters: { 5 | prettierOptions: { 6 | printWidth: 100, 7 | trailingComma: "all", 8 | singleQuote: false, 9 | semi: true, 10 | tabWidth: 2, 11 | quoteProps: "as-needed", 12 | jsxSingleQuote: false, 13 | arrowParens: "always", 14 | }, 15 | }, 16 | stylistic: false, 17 | type: "lib", 18 | typescript: true, 19 | name: "Cyrus", 20 | gitignore: true, 21 | }).append({ 22 | ignores: ["README.md"], 23 | files: ["./src/**/*.ts"], 24 | rules: { 25 | "no-console": "off", 26 | "unicorn/prefer-node-protocol": "off", 27 | "antfu/if-newline": "off", 28 | "test/prefer-lowercase-title": "off", 29 | "unicorn/no-new-array": "off", 30 | "test/prefer-hooks-in-order": "off", 31 | "ts/no-unsafe-function-type": "off", 32 | "perfectionist/sort-imports": "off", 33 | "ts/explicit-function-return-type": "off", 34 | "regexp/no-unused-capturing-group": "off", 35 | "node/prefer-global/buffer": "off", 36 | "node/prefer-global/process": "off", 37 | "no-throw-literal": "off", 38 | "perfectionist/sort-named-imports": ["error", { order: "desc" }], 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/utils/render-markdown.ts: -------------------------------------------------------------------------------- 1 | import { marked } from "marked"; 2 | import { markedTerminal } from "marked-terminal"; 3 | import chalk from "chalk"; 4 | 5 | // Configure marked-terminal with proper terminal styling 6 | marked.use( 7 | // @ts-expect-error - marked-terminal types are not fully compatible with marked v5+ 8 | markedTerminal({ 9 | // Headers 10 | heading: chalk.blue.bold, 11 | strong: chalk.bold, 12 | em: chalk.italic, 13 | codespan: chalk.yellow, 14 | code: chalk.gray, 15 | blockquote: chalk.gray.italic, 16 | 17 | // Links and formatting 18 | link: chalk.blue.underline, 19 | href: chalk.blue.underline, 20 | 21 | // General styling 22 | emoji: true, 23 | reflowText: true, 24 | tab: 2, 25 | width: 80, 26 | 27 | // Table styling 28 | tableOptions: { 29 | chars: { 30 | top: "─", 31 | "top-mid": "┬", 32 | "top-left": "┌", 33 | "top-right": "┐", 34 | bottom: "─", 35 | "bottom-mid": "┴", 36 | "bottom-left": "└", 37 | "bottom-right": "┘", 38 | left: "│", 39 | "left-mid": "├", 40 | mid: "─", 41 | "mid-mid": "┼", 42 | right: "│", 43 | "right-mid": "┤", 44 | middle: "│", 45 | }, 46 | }, 47 | }), 48 | ); 49 | 50 | export const renderMarkdown = async (markdown: string): Promise => { 51 | try { 52 | return await marked.parse(markdown, { 53 | async: true, 54 | silent: false, 55 | gfm: true, 56 | breaks: true, 57 | }); 58 | } catch (error) { 59 | // Fallback to plain text if markdown rendering fails 60 | console.error("Markdown rendering failed:", error); 61 | return markdown; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ConfigManager } from "./config/config"; 2 | import { AIService } from "./services/ai-service"; 3 | import { CodeAnalyzer } from "./analyzers/code-analyzer"; 4 | 5 | export { CodeAnalyzer } from "./analyzers/code-analyzer"; 6 | export { LanguageDetector } from "./analyzers/language-detector"; 7 | export { AnalyzeCommand } from "./commands/analyze"; 8 | // Command exports 9 | export { ConfigCommand } from "./commands/config"; 10 | 11 | export { GenerateCommand } from "./commands/generate"; 12 | export { HealthCommand } from "./commands/health"; 13 | 14 | export { MentorCommand } from "./commands/mentor"; 15 | // Main exports for programmatic usage 16 | export { ConfigManager } from "./config/config"; 17 | export { AIService } from "./services/ai-service"; 18 | // Type exports 19 | export type { 20 | AIProvider, 21 | AnalysisResult, 22 | CodeDiagnostic, 23 | CodeMetrics, 24 | Config, 25 | FileAnalysis, 26 | GeneratedCode, 27 | MentorContext, 28 | ProjectHealth, 29 | RefactorSuggestion, 30 | SecurityVulnerability, 31 | SupportedLanguage, 32 | } from "./types"; 33 | export { 34 | AIServiceError, 35 | AnalysisError, 36 | ConfigurationError, 37 | CyrusError, 38 | errorHandler, 39 | ErrorHandler, 40 | FileSystemError, 41 | ValidationError, 42 | } from "./utils/error-handler.js"; 43 | 44 | // Utility exports 45 | export { logger, Logger, LogLevel } from "./utils/logger"; 46 | 47 | // Constants 48 | export const SUPPORTED_LANGUAGES = [ 49 | "javascript", 50 | "typescript", 51 | "python", 52 | "java", 53 | "go", 54 | "rust", 55 | "csharp", 56 | "php", 57 | "ruby", 58 | "jsx", 59 | "tsx", 60 | ] as const; 61 | 62 | export const AI_PROVIDERS = ["openai", "anthropic", "google"] as const; 63 | 64 | // Utility functions 65 | export const createCyrusInstance = () => { 66 | return { 67 | config: ConfigManager.getInstance(), 68 | ai: AIService.getInstance(), 69 | analyzer: CodeAnalyzer.getInstance(), 70 | }; 71 | }; 72 | 73 | // Version 74 | export const VERSION = "1.0.0"; 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | **/.cyrusrc.json 4 | 5 | # Logs 6 | 7 | logs 8 | _.log 9 | npm-debug.log_ 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Caches 16 | 17 | .cache 18 | 19 | # Diagnostic reports (https://nodejs.org/api/report.html) 20 | 21 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 22 | 23 | # Runtime data 24 | 25 | pids 26 | _.pid 27 | _.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | 36 | coverage 37 | *.lcov 38 | 39 | # nyc test coverage 40 | 41 | .nyc_output 42 | 43 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 44 | 45 | .grunt 46 | 47 | # Bower dependency directory (https://bower.io/) 48 | 49 | bower_components 50 | 51 | # node-waf configuration 52 | 53 | .lock-wscript 54 | 55 | # Compiled binary addons (https://nodejs.org/api/addons.html) 56 | 57 | build/Release 58 | 59 | # Dependency directories 60 | 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | 66 | web_modules/ 67 | 68 | # TypeScript cache 69 | 70 | *.tsbuildinfo 71 | 72 | # Optional npm cache directory 73 | 74 | .npm 75 | 76 | # Optional eslint cache 77 | 78 | .eslintcache 79 | 80 | # Optional stylelint cache 81 | 82 | .stylelintcache 83 | 84 | # Microbundle cache 85 | 86 | .rpt2_cache/ 87 | .rts2_cache_cjs/ 88 | .rts2_cache_es/ 89 | .rts2_cache_umd/ 90 | 91 | # Optional REPL history 92 | 93 | .node_repl_history 94 | 95 | # Output of 'npm pack' 96 | 97 | *.tgz 98 | 99 | # Yarn Integrity file 100 | 101 | .yarn-integrity 102 | 103 | # dotenv environment variable files 104 | 105 | .env 106 | .env.* 107 | 108 | # parcel-bundler cache (https://parceljs.org/) 109 | 110 | .parcel-cache 111 | 112 | # Next.js build output 113 | 114 | .next 115 | out 116 | 117 | # Nuxt.js build / generate output 118 | 119 | .nuxt 120 | dist 121 | 122 | # Gatsby files 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | 138 | # Docusaurus cache and generated files 139 | 140 | .docusaurus 141 | 142 | # Serverless directories 143 | 144 | .serverless/ 145 | 146 | # FuseBox cache 147 | 148 | .fusebox/ 149 | 150 | # DynamoDB Local files 151 | 152 | .dynamodb/ 153 | 154 | # TernJS port file 155 | 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | 160 | .vscode-test 161 | 162 | # yarn v2 163 | 164 | .yarn/cache 165 | .yarn/unplugged 166 | .yarn/build-state.yml 167 | .yarn/install-state.gz 168 | .pnp.* 169 | 170 | # IntelliJ based IDEs 171 | .idea 172 | 173 | # Finder (MacOS) folder config 174 | .DS_Store 175 | 176 | .claude 177 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [daniel@roe.dev](mailto:daniel@roe.dev). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 44 | 45 | [homepage]: https://www.contributor-covenant.org 46 | 47 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@usex/cyrus", 3 | "version": "1.2.0", 4 | "description": "The Code Empire Analyzer. AI-Powered Debugging & Analysis CLI for Modern Developers", 5 | "type": "module", 6 | "module": "src/index.ts", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "bin": { 13 | "cyrus": "./dist/cli.js" 14 | }, 15 | "sideEffects": false, 16 | "keywords": [ 17 | "cyrus", 18 | "ai", 19 | "cli", 20 | "openai", 21 | "claude", 22 | "gemini", 23 | "grok" 24 | ], 25 | "author": { 26 | "name": "Ali Torki", 27 | "url": "https://github.com/ali-master", 28 | "email": "ali_4286@live.com" 29 | }, 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/ali-master/cyrus.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/ali-master/cyrus/issues" 37 | }, 38 | "homepage": "https://github.com/ali-master/cyrus#readme", 39 | "logo": "https://raw.githubusercontent.com/ali-master/cyrus/master/assets/logo.svg", 40 | "engines": { 41 | "node": ">=18.0.0", 42 | "bun": ">=1.0.0", 43 | "npm": ">=8.0.0" 44 | }, 45 | "scripts": { 46 | "prebuild": "bunx rimraf dist", 47 | "build": "bun run build:main && bun run build:cli && bun run build:dts", 48 | "build:dts": "tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist --project ./tsconfig.json", 49 | "build:main": "bun build src/index.ts --outdir dist --target node --format esm --minify", 50 | "build:cli": "bun build src/cli.ts --outdir dist --target node --format esm --minify", 51 | "postbuild": "chmod +x dist/cli.js", 52 | "start:dev": "bun --bun run src/index.ts", 53 | "start:cli:dev": "bun --bun run src/cli.ts", 54 | "prepublish:next": "bun run build", 55 | "publish:next": "bun publish --access public --tag next", 56 | "prepublish:npm": "bun run build", 57 | "publish:npm": "bun publish --access public", 58 | "prerelease": "bun run build", 59 | "release": "release-it", 60 | "typecheck": "tsc --noEmit", 61 | "format": "prettier --write \"**/*.ts\"", 62 | "lint": "eslint \"src/**/*.ts\"", 63 | "lint:fix": "eslint \"src/**/*.ts\" --fix", 64 | "preinstall": "bunx only-allow bun" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^4.14.1", 68 | "@types/bun": "latest", 69 | "@types/inquirer": "^9.0.8", 70 | "@types/figlet": "^1.7.0", 71 | "@types/marked-terminal": "^6.1.1", 72 | "eslint": "^9.28.0", 73 | "eslint-plugin-format": "^1.0.1", 74 | "prettier": "^3.5.3", 75 | "release-it": "^19.0.3" 76 | }, 77 | "peerDependencies": { 78 | "typescript": "^5.7.3" 79 | }, 80 | "dependencies": { 81 | "@ai-sdk/anthropic": "^1.2.12", 82 | "@ai-sdk/google": "^1.2.19", 83 | "@ai-sdk/openai": "^1.3.22", 84 | "@ai-sdk/xai": "^1.2.16", 85 | "ai": "^4.3.16", 86 | "chalk": "^5.4.1", 87 | "commander": "^14.0.0", 88 | "cosmiconfig": "^9.0.0", 89 | "cosmiconfig-typescript-loader": "^6.1.0", 90 | "diff": "^8.0.2", 91 | "dotenv": "^16.5.0", 92 | "figlet": "^1.8.1", 93 | "glob": "^11.0.2", 94 | "gradient-string": "^3.0.0", 95 | "inquirer": "^12.6.3", 96 | "marked": "^15.0.12", 97 | "marked-terminal": "^7.3.0", 98 | "ora": "^8.2.0", 99 | "typescript": "^5.8.3" 100 | }, 101 | "packageManager": "bun@1.2.15", 102 | "changelog": { 103 | "labels": { 104 | "feature": "Features", 105 | "bug": "Bug fixes", 106 | "enhancement": "Enhancements", 107 | "docs": "Docs", 108 | "dependencies": "Dependencies", 109 | "type: code style": "Code style tweaks", 110 | "status: blocked": "Breaking changes", 111 | "breaking change": "Breaking changes" 112 | } 113 | }, 114 | "publishConfig": { 115 | "access": "public" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface CodeDiagnostic { 2 | message: string; 3 | line: number; 4 | column: number; 5 | severity: "error" | "warning" | "info"; 6 | rule?: string; 7 | source?: string; 8 | } 9 | 10 | export interface AnalysisResult { 11 | diagnostics: CodeDiagnostic[]; 12 | metrics?: CodeMetrics; 13 | suggestions?: RefactorSuggestion[]; 14 | } 15 | 16 | export interface CodeMetrics { 17 | complexity: number; 18 | maintainabilityIndex: number; 19 | linesOfCode: number; 20 | technicalDebt: number; 21 | duplicateLines: number; 22 | testCoverage?: number; 23 | } 24 | 25 | export interface RefactorSuggestion { 26 | id: string; 27 | title: string; 28 | description: string; 29 | impact: "low" | "medium" | "high"; 30 | category: "performance" | "readability" | "maintainability" | "security"; 31 | before: string; 32 | after: string; 33 | line: number; 34 | confidence: number; 35 | } 36 | 37 | export interface SecurityVulnerability { 38 | id: string; 39 | title: string; 40 | severity: "low" | "medium" | "high" | "critical"; 41 | description: string; 42 | line: number; 43 | file: string; 44 | cwe?: string; 45 | owasp?: string; 46 | fix?: string; 47 | } 48 | 49 | export type AIProviderType = 50 | | "openai" 51 | | "anthropic" 52 | | "google" 53 | | "xai" 54 | | "ollama" 55 | | "lmstudio" 56 | | "local"; 57 | 58 | export interface AIProvider { 59 | name: AIProviderType; 60 | model: string; 61 | apiKey?: string; 62 | baseURL?: string; 63 | temperature?: number; 64 | maxTokens?: number; 65 | } 66 | 67 | export interface LocalAIProvider extends AIProvider { 68 | name: "ollama" | "lmstudio" | "local"; 69 | baseURL: string; 70 | apiKey?: never; 71 | } 72 | 73 | export interface Config { 74 | $schema?: string; 75 | aiProvider: AIProvider; 76 | features: { 77 | securityScan: boolean; 78 | performanceAnalysis: boolean; 79 | codeGeneration: boolean; 80 | refactorSuggestions: boolean; 81 | mentorMode: boolean; 82 | }; 83 | languages: string[]; 84 | outputFormat: "text" | "json" | "markdown"; 85 | detectLanguage?: { 86 | enabled: boolean; 87 | confidence: number; 88 | }; 89 | localModels?: { 90 | ollama?: { 91 | models: string[]; 92 | defaultModel: string; 93 | }; 94 | lmstudio?: { 95 | models: string[]; 96 | defaultModel: string; 97 | }; 98 | }; 99 | } 100 | 101 | export interface MentorContext { 102 | codeHistory: string[]; 103 | userLevel: "beginner" | "intermediate" | "advanced"; 104 | focusAreas: string[]; 105 | learningGoals: string[]; 106 | } 107 | 108 | export interface GeneratedCode { 109 | type: "test" | "documentation" | "refactor" | "implementation"; 110 | content: string; 111 | language: string; 112 | framework?: string; 113 | dependencies?: string[]; 114 | explanation: string; 115 | } 116 | 117 | export type SupportedLanguage = 118 | | "javascript" 119 | | "typescript" 120 | | "python" 121 | | "java" 122 | | "go" 123 | | "rust" 124 | | "csharp" 125 | | "php" 126 | | "ruby" 127 | | "jsx" 128 | | "tsx"; 129 | 130 | export interface FileAnalysis { 131 | filePath: string; 132 | language: SupportedLanguage; 133 | size: number; 134 | lastModified: Date; 135 | analysis: AnalysisResult; 136 | healthScore: number; 137 | issues: SecurityVulnerability[]; 138 | } 139 | 140 | export interface ProjectHealth { 141 | overallScore: number; 142 | fileAnalyses: FileAnalysis[]; 143 | summary: { 144 | totalFiles: number; 145 | totalIssues: number; 146 | criticalIssues: number; 147 | technicalDebt: number; 148 | testCoverage: number; 149 | }; 150 | recommendations: string[]; 151 | } 152 | 153 | // Export config types for JS/TS config files 154 | export type CyrusConfig = Config; 155 | export type CyrusAIProvider = AIProvider; 156 | export type CyrusAIProviderType = AIProviderType; 157 | export type CyrusLocalAIProvider = LocalAIProvider; 158 | export type CyrusSupportedLanguage = SupportedLanguage; 159 | -------------------------------------------------------------------------------- /src/commands/detect.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import type { Ora } from "ora"; 3 | import chalk from "chalk"; 4 | import path from "path"; 5 | import fs from "fs/promises"; 6 | import { LanguageDetector } from "../analyzers/language-detector"; 7 | import { Logger } from "../utils/logger"; 8 | 9 | export class DetectCommand { 10 | private logger = Logger.getInstance(); 11 | 12 | async handle(targetPath: string, options: any): Promise { 13 | const spinner = ora(); 14 | 15 | try { 16 | const resolvedPath = path.resolve(targetPath); 17 | const stats = await fs.stat(resolvedPath); 18 | 19 | if (stats.isFile()) { 20 | await this.detectFile(resolvedPath, spinner, options); 21 | } else if (stats.isDirectory()) { 22 | await this.detectProject(resolvedPath, spinner, options); 23 | } 24 | } catch (error) { 25 | this.logger.error(`Failed to analyze: ${error}`); 26 | process.exit(1); 27 | } 28 | } 29 | 30 | private async detectFile( 31 | filePath: string, 32 | spinner: Ora, 33 | options: any, 34 | ): Promise { 35 | spinner.start(chalk.gray("Analyzing file...")); 36 | 37 | try { 38 | const content = await fs.readFile(filePath, "utf-8"); 39 | const result = await LanguageDetector.detectLanguage(filePath, content); 40 | 41 | spinner.stop(); 42 | 43 | // Display results 44 | console.log(chalk.bold("\n📄 File Analysis")); 45 | console.log(chalk.gray("─".repeat(50))); 46 | 47 | console.log(`${chalk.cyan("File:")} ${path.basename(filePath)}`); 48 | 49 | if (result.language) { 50 | const languageInfo = LanguageDetector.getLanguageInfo(result.language); 51 | console.log(`${chalk.cyan("Language:")} ${languageInfo.name}`); 52 | console.log( 53 | `${chalk.cyan("Confidence:")} ${this.getConfidenceBar(result.confidence)} ${(result.confidence * 100).toFixed(1)}%`, 54 | ); 55 | 56 | if (result.frameworks && result.frameworks.length > 0) { 57 | console.log( 58 | `${chalk.cyan("Frameworks:")} ${result.frameworks.join(", ")}`, 59 | ); 60 | } 61 | 62 | if (result.testFrameworks && result.testFrameworks.length > 0) { 63 | console.log( 64 | `${chalk.cyan("Test Frameworks:")} ${result.testFrameworks.join(", ")}`, 65 | ); 66 | } 67 | } else { 68 | console.log(chalk.yellow("⚠️ Unable to detect language")); 69 | } 70 | 71 | if (options.detailed) { 72 | console.log(`\n${chalk.bold("Language Features")}`); 73 | console.log(chalk.gray("─".repeat(50))); 74 | 75 | if (result.language) { 76 | const info = LanguageDetector.getLanguageInfo(result.language); 77 | console.log( 78 | `${chalk.cyan("Extensions:")} ${info.extensions.join(", ")}`, 79 | ); 80 | console.log( 81 | `${chalk.cyan("Static Analysis:")} ${info.hasStaticAnalysis ? "✓" : "✗"}`, 82 | ); 83 | console.log( 84 | `${chalk.cyan("Security Rules:")} ${info.hasSecurityRules ? "✓" : "✗"}`, 85 | ); 86 | console.log( 87 | `${chalk.cyan("Test Frameworks:")} ${info.testFrameworks.join(", ")}`, 88 | ); 89 | } 90 | } 91 | } catch (error) { 92 | spinner.fail(chalk.red("Failed to analyze file")); 93 | throw error; 94 | } 95 | } 96 | 97 | private async detectProject( 98 | projectPath: string, 99 | spinner: Ora, 100 | options: any, 101 | ): Promise { 102 | spinner.start(chalk.gray("Scanning project...")); 103 | 104 | try { 105 | const projectInfo = 106 | await LanguageDetector.detectProjectLanguages(projectPath); 107 | 108 | spinner.stop(); 109 | 110 | // Display results 111 | console.log(chalk.bold("\n🗂️ Project Analysis")); 112 | console.log(chalk.gray("─".repeat(50))); 113 | 114 | console.log(`${chalk.cyan("Total Files:")} ${projectInfo.totalFiles}`); 115 | 116 | if (projectInfo.primaryLanguage) { 117 | const primaryInfo = LanguageDetector.getLanguageInfo( 118 | projectInfo.primaryLanguage, 119 | ); 120 | console.log(`${chalk.cyan("Primary Language:")} ${primaryInfo.name}`); 121 | } 122 | 123 | if (projectInfo.languages.size > 0) { 124 | console.log(`\n${chalk.bold("Language Distribution")}`); 125 | console.log(chalk.gray("─".repeat(50))); 126 | 127 | const sortedLanguages = Array.from( 128 | projectInfo.languages.entries(), 129 | ).sort((a, b) => b[1] - a[1]); 130 | 131 | const maxCount = Math.max(...projectInfo.languages.values()); 132 | 133 | for (const [lang, count] of sortedLanguages) { 134 | const info = LanguageDetector.getLanguageInfo(lang); 135 | const percentage = (count / projectInfo.totalFiles) * 100; 136 | const barLength = Math.round((count / maxCount) * 30); 137 | const bar = "█".repeat(barLength) + "░".repeat(30 - barLength); 138 | 139 | console.log( 140 | `${info.name.padEnd(15)} ${chalk.cyan(bar)} ${count.toString().padStart(4)} files (${percentage.toFixed(1)}%)`, 141 | ); 142 | } 143 | } 144 | 145 | if (projectInfo.frameworks.length > 0) { 146 | console.log(`\n${chalk.bold("Detected Frameworks")}`); 147 | console.log(chalk.gray("─".repeat(50))); 148 | console.log(projectInfo.frameworks.map((f) => `• ${f}`).join("\n")); 149 | } 150 | 151 | if (projectInfo.packageManagers.length > 0) { 152 | console.log(`\n${chalk.bold("Package Managers")}`); 153 | console.log(chalk.gray("─".repeat(50))); 154 | console.log( 155 | projectInfo.packageManagers.map((pm) => `• ${pm}`).join("\n"), 156 | ); 157 | } 158 | 159 | if (projectInfo.buildTools.length > 0) { 160 | console.log(`\n${chalk.bold("Build Tools")}`); 161 | console.log(chalk.gray("─".repeat(50))); 162 | console.log(projectInfo.buildTools.map((bt) => `• ${bt}`).join("\n")); 163 | } 164 | 165 | if (projectInfo.testFrameworks.length > 0) { 166 | console.log(`\n${chalk.bold("Test Frameworks")}`); 167 | console.log(chalk.gray("─".repeat(50))); 168 | console.log( 169 | projectInfo.testFrameworks.map((tf) => `• ${tf}`).join("\n"), 170 | ); 171 | } 172 | 173 | if (options.json) { 174 | const outputPath = options.output || "language-detection.json"; 175 | await fs.writeFile( 176 | outputPath, 177 | JSON.stringify( 178 | { 179 | ...projectInfo, 180 | languages: Object.fromEntries(projectInfo.languages), 181 | }, 182 | null, 183 | 2, 184 | ), 185 | ); 186 | console.log(`\n${chalk.green("✓")} Results saved to ${outputPath}`); 187 | } 188 | } catch (error) { 189 | spinner.fail(chalk.red("Failed to analyze project")); 190 | throw error; 191 | } 192 | } 193 | 194 | private getConfidenceBar(confidence: number): string { 195 | const filled = Math.round(confidence * 10); 196 | const empty = 10 - filled; 197 | 198 | let color = chalk.red; 199 | if (confidence > 0.8) color = chalk.green; 200 | else if (confidence > 0.6) color = chalk.yellow; 201 | 202 | return color("●".repeat(filled)) + chalk.gray("○".repeat(empty)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /schema/cyrus-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://cyrus.usestrict.dev/schema.json", 4 | "title": "Cyrus Configuration", 5 | "description": "Configuration schema for Cyrus AI code analysis tool", 6 | "type": "object", 7 | "properties": { 8 | "$schema": { 9 | "type": "string", 10 | "description": "JSON Schema reference" 11 | }, 12 | "aiProvider": { 13 | "type": "object", 14 | "description": "AI provider configuration", 15 | "properties": { 16 | "name": { 17 | "type": "string", 18 | "enum": ["openai", "anthropic", "google", "xai", "ollama", "lmstudio", "local"], 19 | "description": "AI provider name" 20 | }, 21 | "model": { 22 | "type": "string", 23 | "description": "Model name to use" 24 | }, 25 | "apiKey": { 26 | "type": "string", 27 | "description": "API key for cloud providers (not required for local providers)" 28 | }, 29 | "baseURL": { 30 | "type": "string", 31 | "description": "Base URL for API requests (required for local providers)" 32 | }, 33 | "temperature": { 34 | "type": "number", 35 | "minimum": 0, 36 | "maximum": 2, 37 | "description": "Sampling temperature for AI responses" 38 | }, 39 | "maxTokens": { 40 | "type": "number", 41 | "minimum": 1, 42 | "description": "Maximum number of tokens in AI responses" 43 | } 44 | }, 45 | "required": ["name", "model"], 46 | "additionalProperties": false, 47 | "allOf": [ 48 | { 49 | "if": { 50 | "properties": { 51 | "name": { 52 | "enum": ["ollama", "lmstudio", "local"] 53 | } 54 | } 55 | }, 56 | "then": { 57 | "required": ["baseURL"] 58 | } 59 | }, 60 | { 61 | "if": { 62 | "properties": { 63 | "name": { 64 | "enum": ["openai", "anthropic", "google", "xai"] 65 | } 66 | } 67 | }, 68 | "then": { 69 | "properties": { 70 | "apiKey": { 71 | "type": "string", 72 | "minLength": 1 73 | } 74 | } 75 | } 76 | } 77 | ] 78 | }, 79 | "features": { 80 | "type": "object", 81 | "description": "Feature toggles for Cyrus functionality", 82 | "properties": { 83 | "securityScan": { 84 | "type": "boolean", 85 | "description": "Enable security vulnerability scanning" 86 | }, 87 | "performanceAnalysis": { 88 | "type": "boolean", 89 | "description": "Enable performance analysis" 90 | }, 91 | "codeGeneration": { 92 | "type": "boolean", 93 | "description": "Enable AI code generation" 94 | }, 95 | "refactorSuggestions": { 96 | "type": "boolean", 97 | "description": "Enable refactoring suggestions" 98 | }, 99 | "mentorMode": { 100 | "type": "boolean", 101 | "description": "Enable AI mentoring mode" 102 | } 103 | }, 104 | "additionalProperties": false 105 | }, 106 | "languages": { 107 | "type": "array", 108 | "description": "Supported programming languages", 109 | "items": { 110 | "type": "string", 111 | "enum": [ 112 | "javascript", 113 | "typescript", 114 | "python", 115 | "java", 116 | "go", 117 | "rust", 118 | "csharp", 119 | "php", 120 | "ruby", 121 | "jsx", 122 | "tsx" 123 | ] 124 | }, 125 | "uniqueItems": true 126 | }, 127 | "outputFormat": { 128 | "type": "string", 129 | "enum": ["text", "json", "markdown"], 130 | "description": "Output format for analysis results" 131 | }, 132 | "detectLanguage": { 133 | "type": "object", 134 | "description": "Language detection configuration", 135 | "properties": { 136 | "enabled": { 137 | "type": "boolean", 138 | "description": "Enable automatic language detection" 139 | }, 140 | "confidence": { 141 | "type": "number", 142 | "minimum": 0, 143 | "maximum": 1, 144 | "description": "Minimum confidence threshold for language detection" 145 | } 146 | }, 147 | "additionalProperties": false 148 | }, 149 | "localModels": { 150 | "type": "object", 151 | "description": "Local AI model configurations", 152 | "properties": { 153 | "ollama": { 154 | "type": "object", 155 | "description": "Ollama configuration", 156 | "properties": { 157 | "models": { 158 | "type": "array", 159 | "items": { 160 | "type": "string" 161 | }, 162 | "description": "Available Ollama models" 163 | }, 164 | "defaultModel": { 165 | "type": "string", 166 | "description": "Default Ollama model to use" 167 | } 168 | }, 169 | "required": ["models", "defaultModel"], 170 | "additionalProperties": false 171 | }, 172 | "lmstudio": { 173 | "type": "object", 174 | "description": "LM Studio configuration", 175 | "properties": { 176 | "models": { 177 | "type": "array", 178 | "items": { 179 | "type": "string" 180 | }, 181 | "description": "Available LM Studio models" 182 | }, 183 | "defaultModel": { 184 | "type": "string", 185 | "description": "Default LM Studio model to use" 186 | } 187 | }, 188 | "required": ["models", "defaultModel"], 189 | "additionalProperties": false 190 | } 191 | }, 192 | "additionalProperties": false 193 | } 194 | }, 195 | "required": ["aiProvider"], 196 | "additionalProperties": false, 197 | "examples": [ 198 | { 199 | "$schema": "https://cyrus.usestrict.dev/schema.json", 200 | "aiProvider": { 201 | "name": "openai", 202 | "model": "gpt-4-turbo-preview", 203 | "apiKey": "your-api-key-here", 204 | "temperature": 0.7, 205 | "maxTokens": 4096 206 | }, 207 | "features": { 208 | "securityScan": true, 209 | "performanceAnalysis": true, 210 | "codeGeneration": true, 211 | "refactorSuggestions": true, 212 | "mentorMode": true 213 | }, 214 | "languages": ["javascript", "typescript", "python", "java", "go", "rust"], 215 | "outputFormat": "text", 216 | "detectLanguage": { 217 | "enabled": true, 218 | "confidence": 0.7 219 | } 220 | }, 221 | { 222 | "$schema": "https://cyrus.usestrict.dev/schema.json", 223 | "aiProvider": { 224 | "name": "ollama", 225 | "model": "llama3.2", 226 | "baseURL": "http://localhost:11434" 227 | }, 228 | "features": { 229 | "securityScan": true, 230 | "performanceAnalysis": true, 231 | "codeGeneration": false, 232 | "refactorSuggestions": true, 233 | "mentorMode": false 234 | }, 235 | "languages": ["javascript", "typescript", "python"], 236 | "outputFormat": "markdown", 237 | "localModels": { 238 | "ollama": { 239 | "models": ["llama3.2", "codellama", "mistral"], 240 | "defaultModel": "llama3.2" 241 | } 242 | } 243 | } 244 | ] 245 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import chalk from "chalk"; 4 | 5 | export enum LogLevel { 6 | DEBUG = 0, 7 | INFO = 1, 8 | WARN = 2, 9 | ERROR = 3, 10 | } 11 | 12 | export interface LogEntry { 13 | timestamp: string; 14 | level: LogLevel; 15 | message: string; 16 | category?: string; 17 | metadata?: any; 18 | } 19 | 20 | export class Logger { 21 | private static instance: Logger; 22 | private logLevel: LogLevel = LogLevel.INFO; 23 | private enableFileLogging: boolean = false; 24 | private logDirectory: string = "logs"; 25 | private maxLogFiles: number = 5; 26 | 27 | private constructor() { 28 | // Initialize log directory if file logging is enabled 29 | if (this.enableFileLogging) { 30 | this.ensureLogDirectory(); 31 | } 32 | } 33 | 34 | public static getInstance(): Logger { 35 | if (!Logger.instance) { 36 | Logger.instance = new Logger(); 37 | } 38 | return Logger.instance; 39 | } 40 | 41 | public configure(options: { 42 | logLevel?: LogLevel; 43 | enableFileLogging?: boolean; 44 | logDirectory?: string; 45 | maxLogFiles?: number; 46 | }): void { 47 | if (options.logLevel !== undefined) { 48 | this.logLevel = options.logLevel; 49 | } 50 | if (options.enableFileLogging !== undefined) { 51 | this.enableFileLogging = options.enableFileLogging; 52 | } 53 | if (options.logDirectory) { 54 | this.logDirectory = options.logDirectory; 55 | } 56 | if (options.maxLogFiles) { 57 | this.maxLogFiles = options.maxLogFiles; 58 | } 59 | 60 | if (this.enableFileLogging) { 61 | this.ensureLogDirectory(); 62 | } 63 | } 64 | 65 | public debug(message: string, category?: string, metadata?: any): void { 66 | this.log(LogLevel.DEBUG, message, category, metadata); 67 | } 68 | 69 | public info(message: string, category?: string, metadata?: any): void { 70 | this.log(LogLevel.INFO, message, category, metadata); 71 | } 72 | 73 | public warn(message: string, category?: string, metadata?: any): void { 74 | this.log(LogLevel.WARN, message, category, metadata); 75 | } 76 | 77 | public error(message: string, category?: string, metadata?: any): void { 78 | this.log(LogLevel.ERROR, message, category, metadata); 79 | } 80 | 81 | public logAnalysis( 82 | filePath: string, 83 | analysisTime: number, 84 | issueCount: number, 85 | ): void { 86 | this.info(`Analysis completed: ${path.basename(filePath)}`, "analysis", { 87 | filePath, 88 | analysisTime, 89 | issueCount, 90 | }); 91 | } 92 | 93 | public logAIRequest( 94 | provider: string, 95 | model: string, 96 | tokens: number, 97 | duration: number, 98 | ): void { 99 | this.info(`AI request completed: ${provider}/${model}`, "ai", { 100 | provider, 101 | model, 102 | tokens, 103 | duration, 104 | }); 105 | } 106 | 107 | public logError(error: Error, context?: string): void { 108 | this.error(`${context ? `[${context}] ` : ""}${error.message}`, "error", { 109 | name: error.name, 110 | stack: error.stack, 111 | context, 112 | }); 113 | } 114 | 115 | public logPerformance(operation: string, startTime: number): void { 116 | const duration = Date.now() - startTime; 117 | this.debug(`Performance: ${operation} took ${duration}ms`, "performance", { 118 | operation, 119 | duration, 120 | }); 121 | } 122 | 123 | private log( 124 | level: LogLevel, 125 | message: string, 126 | category?: string, 127 | metadata?: any, 128 | ): void { 129 | if (level < this.logLevel) { 130 | return; 131 | } 132 | 133 | const logEntry: LogEntry = { 134 | timestamp: new Date().toISOString(), 135 | level, 136 | message, 137 | category, 138 | metadata, 139 | }; 140 | 141 | // Console output 142 | this.logToConsole(logEntry); 143 | 144 | // File output 145 | if (this.enableFileLogging) { 146 | this.logToFile(logEntry); 147 | } 148 | } 149 | 150 | private logToConsole(entry: LogEntry): void { 151 | const levelColors = { 152 | [LogLevel.DEBUG]: chalk.gray, 153 | [LogLevel.INFO]: chalk.blue, 154 | [LogLevel.WARN]: chalk.yellow, 155 | [LogLevel.ERROR]: chalk.red, 156 | }; 157 | 158 | const levelNames = { 159 | [LogLevel.DEBUG]: "DEBUG", 160 | [LogLevel.INFO]: "INFO ", 161 | [LogLevel.WARN]: "WARN ", 162 | [LogLevel.ERROR]: "ERROR", 163 | }; 164 | 165 | const color = levelColors[entry.level]; 166 | const levelName = levelNames[entry.level]; 167 | const timestamp = new Date(entry.timestamp).toLocaleTimeString(); 168 | const category = entry.category ? chalk.gray(`[${entry.category}]`) : ""; 169 | 170 | console.log( 171 | `${chalk.gray(timestamp)} ${color(levelName)} ${category} ${entry.message}`, 172 | ); 173 | 174 | // Log metadata for debug level or errors 175 | if ( 176 | (entry.level === LogLevel.DEBUG || entry.level === LogLevel.ERROR) && 177 | entry.metadata 178 | ) { 179 | console.log(chalk.gray(" Metadata:"), entry.metadata); 180 | } 181 | } 182 | 183 | private logToFile(entry: LogEntry): void { 184 | try { 185 | const logFileName = this.getLogFileName(); 186 | const logLine = `${JSON.stringify(entry)}\n`; 187 | 188 | fs.appendFileSync(logFileName, logLine, "utf-8"); 189 | 190 | // Rotate logs if needed 191 | this.rotateLogsIfNeeded(); 192 | } catch (error) { 193 | // Fallback to console if file logging fails 194 | console.error(chalk.red("Failed to write to log file:"), error); 195 | } 196 | } 197 | 198 | private ensureLogDirectory(): void { 199 | if (!fs.existsSync(this.logDirectory)) { 200 | fs.mkdirSync(this.logDirectory, { recursive: true }); 201 | } 202 | } 203 | 204 | private getLogFileName(): string { 205 | const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD 206 | return path.join(this.logDirectory, `cyrus-${date}.log`); 207 | } 208 | 209 | private rotateLogsIfNeeded(): void { 210 | try { 211 | const files = fs 212 | .readdirSync(this.logDirectory) 213 | .filter((file) => file.startsWith("cyrus-") && file.endsWith(".log")) 214 | .map((file) => ({ 215 | name: file, 216 | path: path.join(this.logDirectory, file), 217 | stats: fs.statSync(path.join(this.logDirectory, file)), 218 | })) 219 | .sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime()); 220 | 221 | // Remove old log files if we exceed the limit 222 | if (files.length > this.maxLogFiles) { 223 | const filesToDelete = files.slice(this.maxLogFiles); 224 | filesToDelete.forEach((file) => { 225 | fs.unlinkSync(file.path); 226 | }); 227 | } 228 | } catch (error) { 229 | console.error(chalk.red("Failed to rotate log files:"), error); 230 | } 231 | } 232 | 233 | public createChildLogger(category: string): ChildLogger { 234 | return new ChildLogger(this, category); 235 | } 236 | } 237 | 238 | export class ChildLogger { 239 | constructor( 240 | private parent: Logger, 241 | private category: string, 242 | ) {} 243 | 244 | public debug(message: string, metadata?: any): void { 245 | this.parent.debug(message, this.category, metadata); 246 | } 247 | 248 | public info(message: string, metadata?: any): void { 249 | this.parent.info(message, this.category, metadata); 250 | } 251 | 252 | public warn(message: string, metadata?: any): void { 253 | this.parent.warn(message, this.category, metadata); 254 | } 255 | 256 | public error(message: string, metadata?: any): void { 257 | this.parent.error(message, this.category, metadata); 258 | } 259 | 260 | public logError(error: Error, context?: string): void { 261 | this.parent.logError( 262 | error, 263 | `${this.category}${context ? `:${context}` : ""}`, 264 | ); 265 | } 266 | } 267 | 268 | // Global logger instance 269 | export const logger = Logger.getInstance(); 270 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 64 | 65 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | [ 114 | ] 115 | [ 116 | ] 117 | 118 | </> 119 | {} 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /assets/BRAND_ASSETS.md: -------------------------------------------------------------------------------- 1 | # Cyrus Brand Assets 2 | 3 | ## Brand Identity 4 | 5 | Cyrus embodies the power of artificial intelligence in code analysis and development. Our brand represents intelligence, precision, and sovereignty - making complex code debugging and analysis accessible through advanced AI while maintaining developer empowerment. 6 | 7 | ## Logo Design 8 | 9 | ### Concept 10 | The Cyrus logo combines three core elements: 11 | 1. **Imperial Crown** - Representing dominance and mastery over code ("The Code Empire") 12 | 2. **Code Brackets [ ]** - Symbolizing programming and syntax 13 | 3. **Neural Network Nodes** - AI intelligence and analysis capabilities 14 | 15 | ### Design Philosophy 16 | - **Royal & Powerful**: Crown symbolizes mastery and command over code 17 | - **Tech-Forward**: Neural network visualization shows AI integration 18 | - **Professional**: Clean gradients and geometric precision 19 | - **Scalable**: Works from favicon to large displays 20 | - **Modern**: Contemporary design with depth and glow effects 21 | 22 | ### Logo Variations 23 | - **Primary Logo**: Full color gradient version with glow effects (logo.svg) 24 | - **Icon Only**: Crown with brackets for app icons and favicons 25 | - **Monochrome**: Single color version for simplified contexts 26 | - **Simplified**: Without glow effects for small sizes 27 | 28 | ## Color Palette 29 | 30 | ### Primary Colors 31 | ```css 32 | /* Primary Gradient */ 33 | --primary-indigo: #6366f1; 34 | --primary-purple: #8b5cf6; 35 | --primary-magenta: #d946ef; 36 | 37 | /* Accent Gradient */ 38 | --accent-cyan: #06b6d4; 39 | --accent-blue: #3b82f6; 40 | 41 | /* Crown Jewels */ 42 | --gold: #fbbf24; 43 | --gold-dark: #f59e0b; 44 | ``` 45 | 46 | ### Dark Theme Colors 47 | ```css 48 | /* Background Gradient */ 49 | --bg-dark-1: #0f0f23; 50 | --bg-dark-2: #1e1b4b; 51 | --bg-dark-3: #312e81; 52 | 53 | /* Text Colors */ 54 | --text-primary: #ffffff; 55 | --text-secondary: #e0e7ff; 56 | --text-tertiary: #c7d2fe; 57 | ``` 58 | 59 | ### Semantic Colors 60 | ```css 61 | /* Status Colors */ 62 | --success: #10b981; 63 | --warning: #f59e0b; 64 | --error: #ef4444; 65 | --info: #3b82f6; 66 | 67 | /* Code Highlighting */ 68 | --syntax-keyword: #8b5cf6; 69 | --syntax-string: #10b981; 70 | --syntax-number: #f59e0b; 71 | --syntax-comment: #6b7280; 72 | ``` 73 | 74 | ## Typography 75 | 76 | ### Font Families 77 | - **Display**: SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif 78 | - **Body**: SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif 79 | - **Code**: SF Mono, Monaco, 'Cascadia Code', monospace 80 | 81 | ### Text Hierarchy 82 | 1. **Hero Title**: 72px, 700 weight, gradient text 83 | 2. **Section Headers**: 32px, 600 weight 84 | 3. **Subheadings**: 24px, 500 weight 85 | 4. **Body Text**: 16px, 400 weight 86 | 5. **Captions**: 14px, 300 weight 87 | 6. **Code**: 14px, 400 weight, monospace 88 | 89 | ## Visual Elements 90 | 91 | ### Neural Network Pattern 92 | - Represents AI/ML capabilities 93 | - Used subtly in backgrounds and logo 94 | - Connection lines show intelligent analysis flow 95 | - 60% opacity for background use 96 | 97 | ### Code Pattern 98 | - Subtle dot pattern representing code structure 99 | - Used in backgrounds for tech feel 100 | - 30-40% opacity for layering 101 | 102 | ### Glow Effects 103 | - Applied to important elements (logo, buttons) 104 | - Gaussian blur with 3-4px radius 105 | - Enhances the "powered" and "intelligent" feel 106 | 107 | ### Geometric Shapes 108 | - Angular corners for technical precision 109 | - Rounded elements for approachability 110 | - Triangular accents for dynamic movement 111 | 112 | ## Usage Guidelines 113 | 114 | ### Do's 115 | ✅ Use the logo with adequate spacing (minimum 32px clear space) 116 | ✅ Maintain gradient color integrity 117 | ✅ Keep crown jewels visible and prominent 118 | ✅ Use on dark backgrounds for optimal impact 119 | ✅ Scale proportionally maintaining aspect ratio 120 | ✅ Apply glow effects consistently 121 | ✅ Use neural network patterns subtly 122 | 123 | ### Don'ts 124 | ❌ Don't rotate the logo or flip it 125 | ❌ Don't change gradient colors or directions 126 | ❌ Don't remove the crown jewels or neural nodes 127 | ❌ Don't use on light backgrounds without adjustment 128 | ❌ Don't stretch, compress, or distort 129 | ❌ Don't add unauthorized effects or modifications 130 | ❌ Don't use the crown without the code brackets 131 | 132 | ## Applications 133 | 134 | ### Social Media 135 | - **GitHub**: Use social-preview.svg for repository cards 136 | - **Twitter/X**: Social preview for link cards 137 | - **LinkedIn**: Professional posts and company pages 138 | - **Dev.to**: Article headers and profile images 139 | 140 | ### Documentation 141 | - **README**: Logo at top, maintain high contrast 142 | - **Website**: Hero sections, navigation bars 143 | - **Blog**: Article headers and author bylines 144 | - **Presentations**: Title slides and watermarks 145 | 146 | ### CLI Interface 147 | - **Terminal**: ASCII art version with gradient colors 148 | - **Progress Indicators**: Consistent color coding 149 | - **Error Messages**: Semantic color usage 150 | - **Success States**: Green glow effects 151 | 152 | ### Development 153 | - **IDE Extensions**: Icon variants for different sizes 154 | - **Package Managers**: npm/bun package icons 155 | - **Docker**: Container images and badges 156 | - **CI/CD**: Status badges and build indicators 157 | 158 | ## File Formats 159 | 160 | ### Available Assets 161 | 1. **logo.svg** - Primary logo (256x256) 162 | 2. **logo-icon.svg** - Icon variant (64x64) 163 | 3. **social-preview.svg** - Social media card (1280x640) 164 | 4. **favicon.ico** - Browser favicon (32x32, 16x16) 165 | 5. **logo.png** - Raster versions (@1x, @2x, @3x) 166 | 167 | ### Export Guidelines 168 | - **SVG**: Preferred for web and infinite scaling 169 | - **PNG**: Use for fixed-size applications 170 | - **WebP**: Modern format for web optimization 171 | - **ICO**: Browser favicon only 172 | - Always include @2x and @3x versions for high-DPI displays 173 | 174 | ## Animation Guidelines 175 | 176 | ### Logo Animation 177 | - **Entrance**: Fade in crown first, then brackets, then neural network 178 | - **Hover States**: Gentle glow intensity increase (20%) 179 | - **Loading**: Neural network nodes pulse in sequence 180 | - **Success**: Brief golden glow on crown jewels 181 | 182 | ### Transition Timing 183 | - **Easing**: cubic-bezier(0.4, 0.0, 0.2, 1) 184 | - **Duration**: 200-300ms for micro-interactions 185 | - **Delay**: 50ms between sequential elements 186 | 187 | ## Brand Voice 188 | 189 | ### Personality Traits 190 | - **Intelligent**: Advanced AI-powered analysis 191 | - **Authoritative**: "Empire" implies mastery and control 192 | - **Precise**: Technical accuracy and detailed insights 193 | - **Empowering**: Gives developers superpowers 194 | - **Modern**: Cutting-edge technology and design 195 | 196 | ### Core Messages 197 | - "Rule your codebase with AI intelligence" 198 | - "The Code Empire Analyzer" 199 | - "AI-powered precision for modern developers" 200 | - "Debug like an emperor, analyze like an AI" 201 | 202 | ### Tone of Voice 203 | - **Professional** but approachable 204 | - **Confident** without being arrogant 205 | - **Technical** but accessible 206 | - **Inspiring** and empowering 207 | 208 | ## Integration Examples 209 | 210 | ### CLI Banner 211 | ```bash 212 | ██████╗██╗ ██╗██████╗ ██╗ ██╗███████╗ 213 | ██╔════╝╚██╗ ██╔╝██╔══██╗██║ ██║██╔════╝ 214 | ██║ ╚████╔╝ ██████╔╝██║ ██║███████╗ 215 | ██║ ╚██╔╝ ██╔══██╗██║ ██║╚════██║ 216 | ╚██████╗ ██║ ██║ ██║╚██████╔╝███████║ 217 | ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ 218 | 219 | The Code Empire Analyzer 220 | AI-Powered Code Analysis & Debugging CLI 221 | ``` 222 | 223 | ### Package Badge 224 | ```html 225 | Powered by Cyrus 226 | ``` 227 | 228 | ## Accessibility 229 | 230 | ### Color Contrast 231 | - All text meets WCAG 2.1 AA standards 232 | - Minimum 4.5:1 contrast ratio for normal text 233 | - Minimum 3:1 contrast ratio for large text 234 | - Alternative high-contrast versions available 235 | 236 | ### Alternative Formats 237 | - High contrast monochrome version 238 | - Simplified icon without gradients 239 | - Text-only fallbacks for screen readers 240 | - SVG includes proper ARIA labels 241 | 242 | ## Legal & Usage Rights 243 | 244 | ### License 245 | All Cyrus brand assets are licensed under MIT License, allowing: 246 | - ✅ Commercial use 247 | - ✅ Modification for integration 248 | - ✅ Distribution with attribution 249 | - ✅ Private use 250 | 251 | ### Attribution Requirements 252 | When using Cyrus branding: 253 | - Include "Powered by Cyrus" where reasonable 254 | - Don't imply official endorsement without permission 255 | - Respect the open-source nature of the project 256 | - Link back to the official repository when possible 257 | 258 | ### Trademark 259 | "Cyrus" and "The Code Empire Analyzer" are trademarks of Ali Torki. Use is permitted under the MIT license terms with proper attribution. 260 | 261 | --- 262 | 263 | ## Contact 264 | 265 | For questions about brand usage, custom assets, or partnership opportunities: 266 | - **GitHub Issues**: [github.com/ali-master/cyrus/issues](https://github.com/ali-master/cyrus/issues) 267 | - **Email**: [me](mailto:ali_4286@live.com) 268 | - **Linkedin**: [@alitorki](https://linkedin.com/in/alitorki) 269 | 270 | **Last Updated**: November 2024 271 | **Version**: 1.5.8 272 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { cosmiconfig } from "cosmiconfig"; 2 | import type { CosmiconfigResult } from "cosmiconfig"; 3 | import { TypeScriptLoader } from "cosmiconfig-typescript-loader"; 4 | import path from "path"; 5 | import fs from "fs/promises"; 6 | import type { Config, AIProviderType, AIProvider } from "../types"; 7 | import { handleFileError, ConfigurationError } from "../utils/error-handler"; 8 | 9 | export class ConfigManager { 10 | private static instance: ConfigManager; 11 | private explorer; 12 | private config: Config | null = null; 13 | private configResult: CosmiconfigResult | null = null; 14 | 15 | private constructor() { 16 | this.explorer = cosmiconfig("cyrus", { 17 | searchPlaces: [ 18 | "package.json", 19 | ".cyrusrc", 20 | ".cyrusrc.json", 21 | ".cyrusrc.yaml", 22 | ".cyrusrc.yml", 23 | ".cyrusrc.js", 24 | ".cyrusrc.cjs", 25 | ".cyrusrc.mjs", 26 | ".cyrusrc.ts", 27 | "cyrus.config.js", 28 | "cyrus.config.cjs", 29 | "cyrus.config.mjs", 30 | "cyrus.config.ts", 31 | ], 32 | loaders: { 33 | ".ts": TypeScriptLoader(), 34 | }, 35 | }); 36 | } 37 | 38 | public static getInstance(): ConfigManager { 39 | if (!ConfigManager.instance) { 40 | ConfigManager.instance = new ConfigManager(); 41 | } 42 | return ConfigManager.instance; 43 | } 44 | 45 | public async getConfig(): Promise { 46 | if (this.config) { 47 | return this.config; 48 | } 49 | 50 | try { 51 | this.configResult = await this.explorer.search(); 52 | if (this.configResult) { 53 | this.config = this.validateConfig(this.configResult.config); 54 | return this.config; 55 | } 56 | } catch (error) { 57 | console.error("Error reading config:", error); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | private validateConfig(config: any): Config { 64 | // Basic validation and type checking 65 | const validatedConfig: Config = { 66 | aiProvider: this.validateAIProvider(config.aiProvider), 67 | features: { 68 | securityScan: config.features?.securityScan ?? true, 69 | performanceAnalysis: config.features?.performanceAnalysis ?? true, 70 | codeGeneration: config.features?.codeGeneration ?? true, 71 | refactorSuggestions: config.features?.refactorSuggestions ?? true, 72 | mentorMode: config.features?.mentorMode ?? true, 73 | }, 74 | languages: config.languages || this.getDefaultConfig().languages, 75 | outputFormat: config.outputFormat || "text", 76 | detectLanguage: config.detectLanguage || { 77 | enabled: true, 78 | confidence: 0.7, 79 | }, 80 | localModels: config.localModels, 81 | }; 82 | 83 | if (config.$schema) { 84 | validatedConfig.$schema = config.$schema; 85 | } 86 | 87 | return validatedConfig; 88 | } 89 | 90 | private validateAIProvider(provider: any): AIProvider { 91 | if (!provider || !provider.name) { 92 | return this.getDefaultConfig().aiProvider; 93 | } 94 | 95 | const validProviders: AIProviderType[] = [ 96 | "openai", 97 | "anthropic", 98 | "google", 99 | "xai", 100 | "ollama", 101 | "lmstudio", 102 | "local", 103 | ]; 104 | 105 | if (!validProviders.includes(provider.name)) { 106 | throw new ConfigurationError( 107 | `Invalid AI provider: ${provider.name}. Supported providers: ${validProviders.join(", ")}`, 108 | ); 109 | } 110 | 111 | const validatedProvider: AIProvider = { 112 | name: provider.name, 113 | model: provider.model || this.getDefaultModel(provider.name), 114 | temperature: provider.temperature, 115 | maxTokens: provider.maxTokens, 116 | }; 117 | 118 | // Local providers require baseURL 119 | if (["ollama", "lmstudio", "local"].includes(provider.name)) { 120 | if (!provider.baseURL) { 121 | validatedProvider.baseURL = this.getDefaultBaseURL(provider.name); 122 | } else { 123 | validatedProvider.baseURL = provider.baseURL; 124 | } 125 | } else { 126 | // Cloud providers require apiKey 127 | validatedProvider.apiKey = 128 | provider.apiKey || this.getApiKeyFromEnv(provider.name); 129 | if (provider.baseURL) { 130 | validatedProvider.baseURL = provider.baseURL; 131 | } 132 | } 133 | 134 | return validatedProvider; 135 | } 136 | 137 | private getDefaultModel(provider: AIProviderType): string { 138 | const defaults: Record = { 139 | openai: "gpt-4-turbo-preview", 140 | anthropic: "claude-3-opus-20240229", 141 | google: "gemini-1.5-pro", 142 | xai: "grok-beta", 143 | ollama: "llama3.2", 144 | lmstudio: "local-model", 145 | local: "local-model", 146 | }; 147 | return defaults[provider] || "gpt-4"; 148 | } 149 | 150 | private getDefaultBaseURL(provider: AIProviderType): string { 151 | const defaults: Record = { 152 | ollama: "http://localhost:11434", 153 | lmstudio: "http://localhost:1234", 154 | local: "http://localhost:8080", 155 | }; 156 | return defaults[provider] || ""; 157 | } 158 | 159 | private getApiKeyFromEnv(provider: AIProviderType): string { 160 | const envKeys: Record = { 161 | openai: "OPENAI_API_KEY", 162 | anthropic: "ANTHROPIC_API_KEY", 163 | google: "GOOGLE_API_KEY", 164 | xai: "XAI_API_KEY", 165 | }; 166 | 167 | const envKey = envKeys[provider]; 168 | return envKey ? process.env[envKey] || "" : ""; 169 | } 170 | 171 | public async saveConfig(config: Config): Promise { 172 | try { 173 | this.config = config; 174 | 175 | // Determine where to save 176 | let configPath: string; 177 | if (this.configResult?.filepath) { 178 | configPath = this.configResult.filepath; 179 | } else { 180 | // Default to .cyrusrc.json in the current directory 181 | configPath = path.join(process.cwd(), ".cyrusrc.json"); 182 | } 183 | 184 | // Add schema for better IDE support 185 | const configWithSchema = { 186 | $schema: "https://cyrus.dev/schema.json", 187 | ...config, 188 | }; 189 | 190 | await fs.writeFile( 191 | configPath, 192 | JSON.stringify(configWithSchema, null, 2), 193 | "utf-8", 194 | ); 195 | } catch (error) { 196 | handleFileError(error as Error, this.getConfigPath()!); 197 | throw new ConfigurationError( 198 | `Failed to save configuration: ${(error as Error).message}`, 199 | ); 200 | } 201 | } 202 | 203 | public async updateAIProvider(provider: Partial): Promise { 204 | const config = (await this.getConfig()) || this.getDefaultConfig(); 205 | config.aiProvider = { ...config.aiProvider, ...provider }; 206 | await this.saveConfig(config); 207 | } 208 | 209 | public getDefaultConfig(): Config { 210 | return { 211 | $schema: "https://cyrus.dev/schema.json", 212 | aiProvider: { 213 | name: "openai", 214 | model: "gpt-4-turbo-preview", 215 | apiKey: process.env.OPENAI_API_KEY || "", 216 | temperature: 0.7, 217 | maxTokens: 4096, 218 | }, 219 | features: { 220 | securityScan: true, 221 | performanceAnalysis: true, 222 | codeGeneration: true, 223 | refactorSuggestions: true, 224 | mentorMode: true, 225 | }, 226 | languages: ["javascript", "typescript", "python", "java", "go", "rust"], 227 | outputFormat: "text", 228 | detectLanguage: { 229 | enabled: true, 230 | confidence: 0.7, 231 | }, 232 | }; 233 | } 234 | 235 | public async hasValidConfig(): Promise { 236 | const config = await this.getConfig(); 237 | if (!config) return false; 238 | 239 | // Check if it's a local provider (no API key needed) 240 | if (["ollama", "lmstudio", "local"].includes(config.aiProvider.name)) { 241 | return !!config.aiProvider.baseURL; 242 | } 243 | 244 | // For cloud providers, check API key 245 | return !!config.aiProvider.apiKey; 246 | } 247 | 248 | public async initializeConfig(): Promise { 249 | if (!(await this.hasValidConfig())) { 250 | const defaultConfig = this.getDefaultConfig(); 251 | await this.saveConfig(defaultConfig); 252 | return defaultConfig; 253 | } 254 | return (await this.getConfig())!; 255 | } 256 | 257 | public async deleteConfig(): Promise { 258 | try { 259 | if (this.configResult?.filepath) { 260 | await fs.unlink(this.configResult.filepath); 261 | this.config = null; 262 | this.configResult = null; 263 | } 264 | } catch (error) { 265 | handleFileError(error as Error, this.getConfigPath()!); 266 | throw new ConfigurationError( 267 | `Failed to delete configuration: ${(error as Error).message}`, 268 | ); 269 | } 270 | } 271 | 272 | public getConfigPath(): string | null { 273 | return this.configResult?.filepath || null; 274 | } 275 | 276 | public async detectLocalModels(): Promise<{ 277 | ollama: string[]; 278 | lmstudio: string[]; 279 | }> { 280 | const detectedModels = { 281 | ollama: [] as string[], 282 | lmstudio: [] as string[], 283 | }; 284 | 285 | // Try to detect Ollama models 286 | try { 287 | const response = await fetch("http://localhost:11434/api/tags"); 288 | if (response.ok) { 289 | const data = await response.json(); 290 | detectedModels.ollama = data.models?.map((m: any) => m.name) || []; 291 | } 292 | } catch { 293 | // Ollama not running or not installed 294 | } 295 | 296 | // Try to detect LM Studio models 297 | try { 298 | const response = await fetch("http://localhost:1234/v1/models"); 299 | if (response.ok) { 300 | const data = await response.json(); 301 | detectedModels.lmstudio = data.data?.map((m: any) => m.id) || []; 302 | } 303 | } catch { 304 | // LM Studio not running or not installed 305 | } 306 | 307 | return detectedModels; 308 | } 309 | 310 | public async generateConfigFile(targetPath?: string): Promise { 311 | const configPath = targetPath || path.join(process.cwd(), ".cyrusrc.json"); 312 | const config = this.getDefaultConfig(); 313 | 314 | // Try to detect local models 315 | const localModels = await this.detectLocalModels(); 316 | if (localModels.ollama.length > 0 || localModels.lmstudio.length > 0) { 317 | config.localModels = {}; 318 | 319 | if (localModels.ollama.length > 0) { 320 | config.localModels.ollama = { 321 | models: localModels.ollama, 322 | defaultModel: localModels.ollama[0], 323 | }; 324 | } 325 | 326 | if (localModels.lmstudio.length > 0) { 327 | config.localModels.lmstudio = { 328 | models: localModels.lmstudio, 329 | defaultModel: localModels.lmstudio[0], 330 | }; 331 | } 332 | } 333 | 334 | await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8"); 335 | 336 | return configPath; 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/utils/error-handler.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { logger } from "./logger"; 3 | 4 | export class CyrusError extends Error { 5 | public readonly code: string; 6 | public readonly category: string; 7 | public readonly metadata?: any; 8 | 9 | constructor( 10 | message: string, 11 | code: string = "UNKNOWN_ERROR", 12 | category: string = "general", 13 | metadata?: any, 14 | ) { 15 | super(message); 16 | this.name = "CyrusError"; 17 | this.code = code; 18 | this.category = category; 19 | this.metadata = metadata; 20 | } 21 | } 22 | 23 | export class ConfigurationError extends CyrusError { 24 | constructor(message: string, metadata?: any) { 25 | super(message, "CONFIG_ERROR", "configuration", metadata); 26 | this.name = "ConfigurationError"; 27 | } 28 | } 29 | 30 | export class AIServiceError extends CyrusError { 31 | constructor(message: string, provider?: string, metadata?: any) { 32 | super(message, "AI_SERVICE_ERROR", "ai", { provider, ...metadata }); 33 | this.name = "AIServiceError"; 34 | } 35 | } 36 | 37 | export class AnalysisError extends CyrusError { 38 | constructor(message: string, filePath?: string, metadata?: any) { 39 | super(message, "ANALYSIS_ERROR", "analysis", { filePath, ...metadata }); 40 | this.name = "AnalysisError"; 41 | } 42 | } 43 | 44 | export class FileSystemError extends CyrusError { 45 | constructor(message: string, filePath?: string, metadata?: any) { 46 | super(message, "FILESYSTEM_ERROR", "filesystem", { filePath, ...metadata }); 47 | this.name = "FileSystemError"; 48 | } 49 | } 50 | 51 | export class ValidationError extends CyrusError { 52 | constructor(message: string, field?: string, metadata?: any) { 53 | super(message, "VALIDATION_ERROR", "validation", { field, ...metadata }); 54 | this.name = "ValidationError"; 55 | } 56 | } 57 | 58 | export interface ErrorHandlerOptions { 59 | showStackTrace?: boolean; 60 | logToFile?: boolean; 61 | exitOnError?: boolean; 62 | showHelp?: boolean; 63 | } 64 | 65 | export class ErrorHandler { 66 | private static instance: ErrorHandler; 67 | private options: ErrorHandlerOptions = { 68 | showStackTrace: process.env.NODE_ENV === "development", 69 | logToFile: true, 70 | exitOnError: process.env.NODE_ENV !== "development", 71 | showHelp: false, 72 | }; 73 | 74 | private constructor() {} 75 | 76 | public static getInstance(): ErrorHandler { 77 | if (!ErrorHandler.instance) { 78 | ErrorHandler.instance = new ErrorHandler(); 79 | } 80 | return ErrorHandler.instance; 81 | } 82 | 83 | public configure(options: Partial): void { 84 | this.options = { ...this.options, ...options }; 85 | } 86 | 87 | public handle(error: Error, context?: string): void { 88 | // Log the error 89 | logger.logError(error, context); 90 | 91 | // Display user-friendly error message 92 | this.displayError(error, context); 93 | 94 | // Exit if configured to do so 95 | if (this.options.exitOnError) { 96 | process.exit(1); 97 | } 98 | } 99 | 100 | public handleAsync( 101 | operation: () => Promise, 102 | context?: string, 103 | fallback?: T, 104 | ): Promise { 105 | return operation().catch((error) => { 106 | this.handle(error, context); 107 | return fallback; 108 | }); 109 | } 110 | 111 | public wrap( 112 | fn: (...args: T) => R, 113 | context?: string, 114 | ): (...args: T) => R | undefined { 115 | return (...args: T) => { 116 | try { 117 | return fn(...args); 118 | } catch (error) { 119 | this.handle(error as Error, context); 120 | return undefined; 121 | } 122 | }; 123 | } 124 | 125 | public wrapAsync( 126 | fn: (...args: T) => Promise, 127 | context?: string, 128 | ): (...args: T) => Promise { 129 | return async (...args: T) => { 130 | try { 131 | return await fn(...args); 132 | } catch (error) { 133 | this.handle(error as Error, context); 134 | return undefined; 135 | } 136 | }; 137 | } 138 | 139 | private displayError(error: Error, context?: string): void { 140 | console.error(); // Empty line for spacing 141 | 142 | if (error instanceof CyrusError) { 143 | this.displayCyrusError(error, context); 144 | } else { 145 | this.displayGenericError(error, context); 146 | } 147 | 148 | if (this.options.showStackTrace && error.stack) { 149 | console.error(chalk.gray("\nStack trace:")); 150 | console.error(chalk.gray(error.stack)); 151 | } 152 | 153 | if (this.options.showHelp) { 154 | this.displayHelp(); 155 | } 156 | 157 | console.error(); // Empty line for spacing 158 | } 159 | 160 | private displayCyrusError(error: CyrusError, context?: string): void { 161 | const categoryIcons = { 162 | configuration: "⚙️", 163 | ai: "🤖", 164 | analysis: "🔍", 165 | filesystem: "📁", 166 | validation: "✅", 167 | general: "❌", 168 | }; 169 | 170 | const icon = 171 | categoryIcons[error.category as keyof typeof categoryIcons] || "❌"; 172 | 173 | console.error( 174 | chalk.red(`${icon} ${error.name}${context ? ` (${context})` : ""}:`), 175 | ); 176 | console.error(chalk.white(` ${error.message}`)); 177 | 178 | if (error.code !== "UNKNOWN_ERROR") { 179 | console.error(chalk.gray(` Error Code: ${error.code}`)); 180 | } 181 | 182 | // Display helpful metadata 183 | if (error.metadata) { 184 | this.displayErrorMetadata(error.metadata); 185 | } 186 | 187 | // Provide specific guidance based on error type 188 | this.displayErrorGuidance(error); 189 | } 190 | 191 | private displayGenericError(error: Error, context?: string): void { 192 | console.error( 193 | chalk.red(`❌ ${error.name}${context ? ` (${context})` : ""}:`), 194 | ); 195 | console.error(chalk.white(` ${error.message}`)); 196 | } 197 | 198 | private displayErrorMetadata(metadata: any): void { 199 | if (metadata.filePath) { 200 | console.error(chalk.gray(` File: ${metadata.filePath}`)); 201 | } 202 | if (metadata.provider) { 203 | console.error(chalk.gray(` AI Provider: ${metadata.provider}`)); 204 | } 205 | if (metadata.field) { 206 | console.error(chalk.gray(` Field: ${metadata.field}`)); 207 | } 208 | } 209 | 210 | private displayErrorGuidance(error: CyrusError): void { 211 | const guidance = this.getErrorGuidance(error); 212 | if (guidance.length > 0) { 213 | console.error(chalk.yellow("\n💡 Suggestions:")); 214 | guidance.forEach((suggestion, index) => { 215 | console.error(chalk.white(` ${index + 1}. ${suggestion}`)); 216 | }); 217 | } 218 | } 219 | 220 | private getErrorGuidance(error: CyrusError): string[] { 221 | const guidance: string[] = []; 222 | 223 | switch (error.category) { 224 | case "configuration": 225 | guidance.push('Run "cyrus config init" to set up your configuration'); 226 | guidance.push( 227 | "Check if your API key is valid and has sufficient permissions", 228 | ); 229 | guidance.push("Verify your AI provider settings"); 230 | break; 231 | 232 | case "ai": 233 | guidance.push("Check your internet connection"); 234 | guidance.push("Verify your API key is valid and not expired"); 235 | guidance.push( 236 | "Try reducing the input size if the request is too large", 237 | ); 238 | guidance.push( 239 | "Check if your AI provider has rate limits or usage quotas", 240 | ); 241 | break; 242 | 243 | case "analysis": 244 | guidance.push("Ensure the file exists and is readable"); 245 | guidance.push("Check if the file type is supported"); 246 | guidance.push("Try with a smaller file to isolate the issue"); 247 | break; 248 | 249 | case "filesystem": 250 | guidance.push("Check if the file or directory exists"); 251 | guidance.push("Verify you have read/write permissions"); 252 | guidance.push("Ensure the file is not locked by another process"); 253 | break; 254 | 255 | case "validation": 256 | guidance.push("Check the input format and requirements"); 257 | guidance.push("Refer to the documentation for valid values"); 258 | break; 259 | } 260 | 261 | // Add common guidance 262 | if (guidance.length > 0) { 263 | guidance.push('Use "cyrus --help" for usage information'); 264 | } 265 | 266 | return guidance; 267 | } 268 | 269 | private displayHelp(): void { 270 | console.error(chalk.cyan("\n📚 Need help?")); 271 | console.error( 272 | chalk.white( 273 | " Documentation: https://github.com/ali-master/cyrus#readme", 274 | ), 275 | ); 276 | console.error( 277 | chalk.white(" Issues: https://github.com/ali-master/cyrus/issues"), 278 | ); 279 | console.error(chalk.white(" Discord: [Coming soon]")); 280 | } 281 | } 282 | 283 | // Global error handler instance 284 | export const errorHandler = ErrorHandler.getInstance(); 285 | 286 | // Setup global error handling 287 | process.on("uncaughtException", (error) => { 288 | errorHandler.handle(error, "uncaughtException"); 289 | }); 290 | 291 | process.on("unhandledRejection", (reason) => { 292 | const error = reason instanceof Error ? reason : new Error(String(reason)); 293 | errorHandler.handle(error, "unhandledRejection"); 294 | }); 295 | 296 | // Helper functions for common error scenarios 297 | export const handleFileError = (error: Error, filePath: string): void => { 298 | if (error.message.includes("ENOENT")) { 299 | throw new FileSystemError(`File not found: ${filePath}`, filePath); 300 | } else if (error.message.includes("EACCES")) { 301 | throw new FileSystemError(`Permission denied: ${filePath}`, filePath); 302 | } else if (error.message.includes("EISDIR")) { 303 | throw new FileSystemError( 304 | `Expected file but found directory: ${filePath}`, 305 | filePath, 306 | ); 307 | } else { 308 | throw new FileSystemError(`File system error: ${error.message}`, filePath); 309 | } 310 | }; 311 | 312 | export const handleAIError = (error: Error, provider: string): void => { 313 | if (error.message.includes("API key")) { 314 | throw new AIServiceError("Invalid or missing API key", provider); 315 | } else if (error.message.includes("rate limit")) { 316 | throw new AIServiceError("API rate limit exceeded", provider); 317 | } else if (error.message.includes("quota")) { 318 | throw new AIServiceError("API quota exceeded", provider); 319 | } else if ( 320 | error.message.includes("network") || 321 | error.message.includes("ENOTFOUND") 322 | ) { 323 | throw new AIServiceError("Network connection failed", provider); 324 | } else { 325 | throw new AIServiceError(`AI service error: ${error.message}`, provider); 326 | } 327 | }; 328 | 329 | export const validateRequired = (value: any, fieldName: string): void => { 330 | if (value === undefined || value === null || value === "") { 331 | throw new ValidationError(`${fieldName} is required`, fieldName); 332 | } 333 | }; 334 | 335 | export const validateFileExists = (filePath: string): void => { 336 | try { 337 | // eslint-disable-next-line ts/no-require-imports 338 | const fs = require("fs"); 339 | if (!fs.existsSync(filePath)) { 340 | throw new FileSystemError(`File does not exist: ${filePath}`, filePath); 341 | } 342 | } catch (error) { 343 | handleFileError(error as Error, filePath); 344 | } 345 | }; 346 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cyrus 2 | 3 | First off, thank you for considering contributing to Cyrus! It's people like you that make Cyrus such a great tool for AI-powered code analysis and debugging. 4 | 5 | ## Code of Conduct 6 | 7 | This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | Before creating bug reports, please check existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: 14 | 15 | * Use a clear and descriptive title 16 | * Describe the exact steps which reproduce the problem 17 | * Provide specific examples to demonstrate the steps 18 | * Describe the behavior you observed after following the steps 19 | * Explain which behavior you expected to see instead and why 20 | * Include your system details (OS, Node.js/Bun version, AI provider, etc.) 21 | * Include configuration details (run `cyrus config show` to get current config) 22 | * If it's an AI-related error, include the provider and model you're using 23 | 24 | ### Suggesting Enhancements 25 | 26 | Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: 27 | 28 | * Use a clear and descriptive title 29 | * Provide a step-by-step description of the suggested enhancement 30 | * Provide specific examples to demonstrate the steps 31 | * Describe the current behavior and explain which behavior you expected to see instead 32 | * Explain why this enhancement would be useful 33 | * Consider if the enhancement fits with Cyrus's focus on AI-powered code analysis 34 | 35 | ### Pull Requests 36 | 37 | * Fork the repo and create your branch from `main` 38 | * If you've added code that should be tested, add tests 39 | * Ensure the test suite passes 40 | * Make sure your code lints 41 | * Update documentation as needed 42 | * Follow the error handling patterns established in the codebase 43 | * Issue that pull request! 44 | 45 | ## Development Process 46 | 47 | 1. **Fork the repository** 48 | ```bash 49 | git clone https://github.com/your-username/cyrus.git 50 | cd cyrus 51 | ``` 52 | 53 | 2. **Install dependencies** 54 | ```bash 55 | bun install 56 | ``` 57 | 58 | 3. **Set up development environment** 59 | ```bash 60 | # Initialize configuration for development 61 | bun run start:cli:dev config init 62 | 63 | # You can use local AI models for development (no API keys needed) 64 | # Or set up with your preferred AI provider 65 | ``` 66 | 67 | 4. **Create a branch** 68 | ```bash 69 | git checkout -b feature/your-feature-name 70 | ``` 71 | 72 | 5. **Make your changes** 73 | * Write or update tests as needed 74 | * Follow the existing code style and patterns 75 | * Use the established error handling system 76 | * Update documentation as needed 77 | 78 | 6. **Test your changes** 79 | ```bash 80 | # Type checking 81 | bun run test:types 82 | 83 | # Linting and formatting 84 | bun run lint 85 | bun run format 86 | 87 | # Test CLI commands manually 88 | bun run start:cli:dev --help 89 | bun run start:cli:dev detect src/ 90 | bun run start:cli:dev analyze src/cli.ts 91 | ``` 92 | 93 | 7. **Commit your changes** 94 | ```bash 95 | git commit -m "feat: add some amazing feature" 96 | ``` 97 | 98 | We follow [Conventional Commits](https://www.conventionalcommits.org/) specification: 99 | * `feat:` new feature 100 | * `fix:` bug fix 101 | * `docs:` documentation changes 102 | * `style:` formatting, missing semi colons, etc 103 | * `refactor:` code change that neither fixes a bug nor adds a feature 104 | * `test:` adding missing tests 105 | * `chore:` maintenance 106 | 107 | 8. **Push to your fork** 108 | ```bash 109 | git push origin feature/your-feature-name 110 | ``` 111 | 112 | 9. **Open a Pull Request** 113 | 114 | ## Architecture Overview 115 | 116 | ### Core Components 117 | 118 | - **CLI (`src/cli.ts`)**: Main entry point and command routing 119 | - **Commands (`src/commands/`)**: Individual command implementations 120 | - **Analyzers (`src/analyzers/`)**: Language detection and code analysis 121 | - **Services (`src/services/`)**: AI service integration 122 | - **Config (`src/config/`)**: Configuration management with cosmiconfig 123 | - **Utils (`src/utils/`)**: Shared utilities including error handling 124 | 125 | ### Error Handling System 126 | 127 | Cyrus uses a comprehensive error handling system located in `src/utils/error-handler.ts`. When contributing: 128 | 129 | #### Use Specific Error Types 130 | 131 | ```typescript 132 | import { AnalysisError, ConfigurationError, ValidationError } from "../utils/error-handler.js"; 133 | 134 | // Instead of generic Error 135 | throw new AnalysisError("Failed to analyze file", filePath); 136 | 137 | // For configuration issues 138 | throw new ConfigurationError("Invalid AI provider configuration"); 139 | 140 | // For validation failures 141 | throw new ValidationError("Required field missing", fieldName); 142 | ``` 143 | 144 | #### Use Error Handler Wrappers 145 | 146 | ```typescript 147 | import { errorHandler } from "../utils/error-handler.js"; 148 | 149 | // For async operations 150 | return await errorHandler.handleAsync(async () => { 151 | // Your async code here 152 | }, 'operation-context'); 153 | 154 | // For file operations 155 | import { handleFileError, validateFileExists } from "../utils/error-handler.js"; 156 | 157 | try { 158 | validateFileExists(filePath); 159 | const content = fs.readFileSync(filePath, 'utf-8'); 160 | } catch (error) { 161 | handleFileError(error as Error, filePath); 162 | } 163 | ``` 164 | 165 | #### Error Handling Guidelines 166 | 167 | 1. Always use specific error types instead of generic `Error` 168 | 2. Provide meaningful error messages with context 169 | 3. Include relevant metadata (file paths, provider names, etc.) 170 | 4. Use `errorHandler.handleAsync()` for command-level error management 171 | 5. Never use `console.error()` directly - let the error handler manage output 172 | 6. Don't call `process.exit()` directly - configure the error handler instead 173 | 174 | ## Style Guide 175 | 176 | ### TypeScript Style Guide 177 | 178 | * Use TypeScript for all new code 179 | * Follow the existing code style (enforced by ESLint) 180 | * Use meaningful variable names 181 | * Add types to all function parameters and return values 182 | * Avoid `any` types - use proper interfaces and unions 183 | * Use import aliases consistently (`.js` extensions for local imports) 184 | 185 | ### Code Organization 186 | 187 | * Keep functions focused and single-purpose 188 | * Use descriptive function and variable names 189 | * Group related functionality in appropriate modules 190 | * Follow the established directory structure 191 | * Export types from `src/types/index.ts` 192 | 193 | ### Error Handling Patterns 194 | 195 | ```typescript 196 | // ✅ Good - Specific error with context 197 | throw new AnalysisError( 198 | `Failed to analyze ${language} code: ${error.message}`, 199 | filePath, 200 | { language, provider: aiProvider.name } 201 | ); 202 | 203 | // ❌ Bad - Generic error without context 204 | throw new Error(`Analysis failed: ${error}`); 205 | 206 | // ✅ Good - Using error handler wrapper 207 | return await errorHandler.handleAsync(async () => { 208 | // Command implementation 209 | }, 'command-name'); 210 | 211 | // ❌ Bad - Manual error handling 212 | try { 213 | // Command implementation 214 | } catch (error) { 215 | console.error("Error:", error); 216 | process.exit(1); 217 | } 218 | ``` 219 | 220 | ### AI Integration Guidelines 221 | 222 | * Always validate AI provider configuration before use 223 | * Handle AI service errors gracefully with `handleAIError()` 224 | * Provide fallback behavior when AI services are unavailable 225 | * Support both cloud and local AI providers 226 | * Include provider context in error messages 227 | 228 | ### CLI UX Guidelines 229 | 230 | * Use consistent color coding (follow Claude Code style) 231 | * Provide progress indicators for long operations 232 | * Include helpful suggestions in error messages 233 | * Support both interactive and non-interactive modes 234 | * Offer JSON output options for automation 235 | 236 | ## Testing 237 | 238 | ### Manual Testing 239 | 240 | Test your changes with various scenarios: 241 | 242 | ```bash 243 | # Test language detection 244 | bun run start:cli:dev detect src/ 245 | bun run start:cli:dev detect src/cli.ts --detailed 246 | 247 | # Test analysis with different providers 248 | bun run start:cli:dev analyze src/commands/analyze.ts --security --metrics 249 | 250 | # Test configuration management 251 | bun run start:cli:dev config show 252 | bun run start:cli:dev config set provider ollama 253 | 254 | # Test error scenarios 255 | bun run start:cli:dev analyze nonexistent-file.js 256 | bun run start:cli:dev config set provider invalid-provider 257 | ``` 258 | 259 | ### Error Handling Testing 260 | 261 | * Test error scenarios (invalid files, network issues, invalid configs) 262 | * Verify error messages are helpful and actionable 263 | * Check that errors include appropriate context and suggestions 264 | * Ensure graceful degradation when AI services are unavailable 265 | 266 | ## Commit Messages 267 | 268 | * Use the present tense ("Add feature" not "Added feature") 269 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 270 | * Limit the first line to 72 characters or less 271 | * Reference issues and pull requests liberally after the first line 272 | * Include scope when applicable: `feat(analyze): add security scanning` 273 | 274 | ## Additional Notes 275 | 276 | ### Issue and Pull Request Labels 277 | 278 | * `bug` - Something isn't working 279 | * `enhancement` - New feature or request 280 | * `good first issue` - Good for newcomers 281 | * `help wanted` - Extra attention is needed 282 | * `question` - Further information is requested 283 | * `documentation` - Improvements or additions to documentation 284 | * `ai-integration` - Related to AI service integration 285 | * `language-support` - Adding support for new programming languages 286 | * `error-handling` - Related to error handling improvements 287 | * `performance` - Performance improvements 288 | * `security` - Security-related changes 289 | 290 | ### Development Tips 291 | 292 | 1. **Local AI Development**: Use Ollama or LM Studio for development to avoid API costs 293 | 2. **Configuration**: Keep multiple config files for testing different scenarios 294 | 3. **Debugging**: Use `--verbose` flags and check logs in the logger output 295 | 4. **Performance**: Test with large codebases to ensure scalability 296 | 5. **Cross-platform**: Test on different operating systems when possible 297 | 298 | ### Areas We Need Help 299 | 300 | - 🌍 **Language Support**: Add more programming languages and frameworks 301 | - 🤖 **AI Providers**: Integrate additional AI services and local models 302 | - 🧪 **Test Coverage**: Expand automated test suite 303 | - 📖 **Documentation**: Improve guides, examples, and API documentation 304 | - 🎨 **CLI UX**: Enhance user experience and output formatting 305 | - 🔌 **Integrations**: VS Code extension, GitHub Actions, CI/CD tools 306 | - 🛡️ **Security**: Enhance security analysis capabilities 307 | - ⚡ **Performance**: Optimize analysis speed and memory usage 308 | 309 | ### Resources 310 | 311 | - [Cyrus Documentation](https://github.com/ali-master/cyrus#readme) 312 | - [AI SDK Documentation](https://sdk.vercel.ai/) 313 | - [Cosmiconfig Documentation](https://github.com/davidtheclark/cosmiconfig) 314 | - [Commander.js Documentation](https://github.com/tj/commander.js/) 315 | 316 | [See open issues →](https://github.com/ali-master/cyrus/issues) 317 | 318 | --- 319 | 320 | Thank you for contributing! Your efforts help make Cyrus a better tool for developers worldwide. 🎉 321 | 322 | **Built with ❤️ by the community for developers who demand excellence** -------------------------------------------------------------------------------- /src/utils/progress-bar.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | interface ProgressStage { 4 | name: string; 5 | emoji?: string; 6 | weight?: number; // Relative weight for timing (default: 1) 7 | } 8 | 9 | interface ProgressOptions { 10 | total?: number; 11 | showPercentage?: boolean; 12 | showETA?: boolean; 13 | showSpeed?: boolean; 14 | barLength?: number; 15 | theme?: "default" | "modern" | "minimal" | "gradient"; 16 | } 17 | 18 | export class ProgressBar { 19 | private stages: ProgressStage[]; 20 | private currentStage: number = 0; 21 | private currentProgress: number = 0; 22 | private startTime: number; 23 | private lastUpdateTime: number; 24 | private processedItems: number = 0; 25 | private options: Required; 26 | private intervalId?: NodeJS.Timeout; 27 | private isActive: boolean = false; 28 | 29 | // Animation frames for different themes 30 | private readonly themes = { 31 | default: { 32 | frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], 33 | completed: "█", 34 | incomplete: "░", 35 | colors: { 36 | progress: chalk.cyan, 37 | percentage: chalk.yellow, 38 | eta: chalk.gray, 39 | stage: chalk.green, 40 | }, 41 | }, 42 | modern: { 43 | frames: ["●", "○", "◐", "◓", "◑", "◒"], 44 | completed: "▓", 45 | incomplete: "▒", 46 | colors: { 47 | progress: chalk.magenta, 48 | percentage: chalk.cyan, 49 | eta: chalk.gray, 50 | stage: chalk.blue, 51 | }, 52 | }, 53 | minimal: { 54 | frames: ["-", "\\", "|", "/"], 55 | completed: "=", 56 | incomplete: "-", 57 | colors: { 58 | progress: chalk.white, 59 | percentage: chalk.white, 60 | eta: chalk.gray, 61 | stage: chalk.white, 62 | }, 63 | }, 64 | gradient: { 65 | frames: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"], 66 | completed: "🟩", 67 | incomplete: "⬜", 68 | colors: { 69 | progress: chalk.green, 70 | percentage: chalk.yellow, 71 | eta: chalk.gray, 72 | stage: chalk.blue, 73 | }, 74 | }, 75 | }; 76 | 77 | private frameIndex: number = 0; 78 | 79 | constructor(stages: ProgressStage[], options: ProgressOptions = {}) { 80 | this.stages = stages.map((stage) => ({ 81 | weight: 1, 82 | ...stage, 83 | })); 84 | 85 | this.options = { 86 | total: this.calculateTotalWeight(), 87 | showPercentage: true, 88 | showETA: true, 89 | showSpeed: false, 90 | barLength: 30, 91 | theme: "gradient", 92 | ...options, 93 | }; 94 | 95 | this.startTime = Date.now(); 96 | this.lastUpdateTime = this.startTime; 97 | } 98 | 99 | private calculateTotalWeight(): number { 100 | return this.stages.reduce((sum, stage) => sum + (stage.weight || 1), 0); 101 | } 102 | 103 | public start(): void { 104 | if (this.isActive) return; 105 | 106 | this.isActive = true; 107 | this.startTime = Date.now(); 108 | this.lastUpdateTime = this.startTime; 109 | 110 | // Start animation 111 | this.intervalId = setInterval(() => { 112 | this.frameIndex = 113 | (this.frameIndex + 1) % this.themes[this.options.theme].frames.length; 114 | this.render(); 115 | }, 100); 116 | 117 | this.render(); 118 | } 119 | 120 | public updateStage(stageName: string, progress: number = 0): void { 121 | const stageIndex = this.stages.findIndex((s) => s.name === stageName); 122 | if (stageIndex === -1) return; 123 | 124 | this.currentStage = stageIndex; 125 | this.currentProgress = Math.max(0, Math.min(100, progress)); 126 | this.lastUpdateTime = Date.now(); 127 | 128 | if (this.isActive) { 129 | this.render(); 130 | } 131 | } 132 | 133 | public updateProgress(progress: number, itemsProcessed?: number): void { 134 | this.currentProgress = Math.max(0, Math.min(100, progress)); 135 | if (itemsProcessed !== undefined) { 136 | this.processedItems = itemsProcessed; 137 | } 138 | this.lastUpdateTime = Date.now(); 139 | 140 | if (this.isActive) { 141 | this.render(); 142 | } 143 | } 144 | 145 | public incrementStage(progress: number = 100): void { 146 | if (this.currentStage < this.stages.length - 1) { 147 | this.currentStage++; 148 | this.currentProgress = Math.max(0, Math.min(100, progress)); 149 | this.lastUpdateTime = Date.now(); 150 | 151 | if (this.isActive) { 152 | this.render(); 153 | } 154 | } 155 | } 156 | 157 | public complete(message?: string): void { 158 | if (!this.isActive) return; 159 | 160 | this.isActive = false; 161 | if (this.intervalId) { 162 | clearInterval(this.intervalId); 163 | } 164 | 165 | // Clear current line and show completion 166 | process.stdout.write("\r\x1B[K"); 167 | 168 | const theme = this.themes[this.options.theme]; 169 | const completionEmoji = this.getCompletionEmoji(); 170 | const finalMessage = message || "Complete!"; 171 | const totalTime = this.formatTime((Date.now() - this.startTime) / 1000); 172 | 173 | console.log( 174 | `${completionEmoji} ${theme.colors.stage.bold(finalMessage)} ${theme.colors.eta(`(${totalTime})`)}`, 175 | ); 176 | } 177 | 178 | public fail(message?: string): void { 179 | if (!this.isActive) return; 180 | 181 | this.isActive = false; 182 | if (this.intervalId) { 183 | clearInterval(this.intervalId); 184 | } 185 | 186 | // Clear current line and show failure 187 | process.stdout.write("\r\x1B[K"); 188 | 189 | const theme = this.themes[this.options.theme]; 190 | const failMessage = message || "Failed!"; 191 | const totalTime = this.formatTime((Date.now() - this.startTime) / 1000); 192 | 193 | console.log( 194 | `❌ ${chalk.red.bold(failMessage)} ${theme.colors.eta(`(${totalTime})`)}`, 195 | ); 196 | } 197 | 198 | private render(): void { 199 | if (!this.isActive) return; 200 | 201 | const theme = this.themes[this.options.theme]; 202 | const currentStage = this.stages[this.currentStage]; 203 | 204 | // Calculate overall progress 205 | const stageWeight = this.stages 206 | .slice(0, this.currentStage) 207 | .reduce((sum, s) => sum + (s.weight || 1), 0); 208 | const currentStageProgress = 209 | ((currentStage?.weight || 1) * this.currentProgress) / 100; 210 | const overallProgress = 211 | ((stageWeight + currentStageProgress) / this.options.total) * 100; 212 | 213 | // Build progress bar 214 | const completed = Math.floor( 215 | (overallProgress / 100) * this.options.barLength, 216 | ); 217 | const progressBar = 218 | theme.completed.repeat(completed) + 219 | theme.incomplete.repeat(this.options.barLength - completed); 220 | 221 | // Build components 222 | const spinner = theme.frames[this.frameIndex]; 223 | const stageEmoji = currentStage?.emoji || "📋"; 224 | const stageName = currentStage?.name || "Processing"; 225 | const percentage = this.options.showPercentage 226 | ? ` ${Math.floor(overallProgress)}%` 227 | : ""; 228 | const eta = this.options.showETA ? ` ${this.getETA()}` : ""; 229 | const speed = this.options.showSpeed ? ` ${this.getSpeed()}` : ""; 230 | 231 | // Create styled output 232 | const output = [ 233 | spinner, 234 | stageEmoji, 235 | theme.colors.stage(stageName), 236 | theme.colors.progress(`[${progressBar}]`), 237 | theme.colors.percentage(percentage), 238 | theme.colors.eta(eta), 239 | speed && theme.colors.eta(speed), 240 | ] 241 | .filter(Boolean) 242 | .join(" "); 243 | 244 | // Update current line 245 | process.stdout.write(`\r\x1B[K${output}`); 246 | } 247 | 248 | private getETA(): string { 249 | const elapsed = (Date.now() - this.startTime) / 1000; 250 | const currentStage = this.stages[this.currentStage]; 251 | 252 | if (elapsed < 1 || !currentStage) return ""; 253 | 254 | // Calculate progress-based ETA 255 | const stageWeight = this.stages 256 | .slice(0, this.currentStage) 257 | .reduce((sum, s) => sum + (s.weight || 1), 0); 258 | const currentStageProgress = 259 | ((currentStage.weight || 1) * this.currentProgress) / 100; 260 | const overallProgress = 261 | (stageWeight + currentStageProgress) / this.options.total; 262 | 263 | if (overallProgress <= 0) return ""; 264 | 265 | const totalEstimated = elapsed / overallProgress; 266 | const remaining = Math.max(0, totalEstimated - elapsed); 267 | 268 | return `ETA: ${this.formatTime(remaining)}`; 269 | } 270 | 271 | private getSpeed(): string { 272 | const elapsed = (Date.now() - this.startTime) / 1000; 273 | if (elapsed < 1 || this.processedItems === 0) return ""; 274 | 275 | const itemsPerSecond = this.processedItems / elapsed; 276 | if (itemsPerSecond < 1) { 277 | return `${(itemsPerSecond * 60).toFixed(1)}/min`; 278 | } 279 | return `${itemsPerSecond.toFixed(1)}/s`; 280 | } 281 | 282 | private formatTime(seconds: number): string { 283 | if (seconds < 60) return `${Math.floor(seconds)}s`; 284 | const minutes = Math.floor(seconds / 60); 285 | const remainingSeconds = Math.floor(seconds % 60); 286 | return `${minutes}m ${remainingSeconds}s`; 287 | } 288 | 289 | private getCompletionEmoji(): string { 290 | const emojis = ["✅", "🎉", "🚀", "⭐", "💯"]; 291 | return emojis[Math.floor(Math.random() * emojis.length)]; 292 | } 293 | } 294 | 295 | // Convenience function for simple progress bars 296 | export function createProgressBar( 297 | stages: (string | ProgressStage)[], 298 | options: ProgressOptions = {}, 299 | ): ProgressBar { 300 | const formattedStages: ProgressStage[] = stages.map((stage) => 301 | typeof stage === "string" ? { name: stage } : stage, 302 | ); 303 | 304 | return new ProgressBar(formattedStages, options); 305 | } 306 | 307 | // Convenience function for file processing progress 308 | export function createFileProcessingProgress( 309 | totalFiles: number, 310 | options: ProgressOptions = {}, 311 | ): ProgressBar { 312 | const stages: ProgressStage[] = [ 313 | { name: "Scanning files", emoji: "🔍", weight: 1 }, 314 | { name: "Analyzing code", emoji: "⚙️", weight: totalFiles * 2 }, 315 | { name: "Generating insights", emoji: "🧠", weight: 2 }, 316 | { name: "Finalizing report", emoji: "📊", weight: 1 }, 317 | ]; 318 | 319 | return new ProgressBar(stages, { 320 | theme: "modern", 321 | showETA: true, 322 | ...options, 323 | }); 324 | } 325 | 326 | // Convenience function for AI processing progress 327 | export function createAIProgress( 328 | operation: string, 329 | options: ProgressOptions = {}, 330 | ): ProgressBar { 331 | const stages: ProgressStage[] = [ 332 | { name: `Preparing ${operation}`, emoji: "🔧", weight: 1 }, 333 | { name: "Processing with AI", emoji: "🤖", weight: 5 }, 334 | { name: "Formatting results", emoji: "✨", weight: 1 }, 335 | ]; 336 | 337 | return new ProgressBar(stages, { 338 | theme: "gradient", 339 | showETA: true, 340 | ...options, 341 | }); 342 | } 343 | 344 | // Network/download progress 345 | export function createNetworkProgress( 346 | operation: string, 347 | options: ProgressOptions = {}, 348 | ): ProgressBar { 349 | const stages: ProgressStage[] = [ 350 | { name: "Connecting", emoji: "🔗", weight: 1 }, 351 | { name: `${operation}`, emoji: "⬇️", weight: 8 }, 352 | { name: "Completing", emoji: "✅", weight: 1 }, 353 | ]; 354 | 355 | return new ProgressBar(stages, { 356 | theme: "modern", 357 | showETA: true, 358 | showSpeed: true, 359 | ...options, 360 | }); 361 | } 362 | 363 | // Build/compilation progress 364 | export function createBuildProgress( 365 | _buildType: string = "build", 366 | options: ProgressOptions = {}, 367 | ): ProgressBar { 368 | const stages: ProgressStage[] = [ 369 | { name: "Preparing", emoji: "🔧", weight: 1 }, 370 | { name: "Compiling", emoji: "⚙️", weight: 6 }, 371 | { name: "Optimizing", emoji: "⚡", weight: 2 }, 372 | { name: "Finalizing", emoji: "📦", weight: 1 }, 373 | ]; 374 | 375 | return new ProgressBar(stages, { 376 | theme: "gradient", 377 | showETA: true, 378 | ...options, 379 | }); 380 | } 381 | -------------------------------------------------------------------------------- /src/commands/analyze.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chalk from "chalk"; 3 | import { CodeAnalyzer } from "../analyzers/code-analyzer"; 4 | import { AIService } from "../services/ai-service"; 5 | import { LanguageDetector } from "../analyzers/language-detector"; 6 | import { ConfigManager } from "../config/config"; 7 | import { createAIProgress } from "../utils/progress-bar"; 8 | import { 9 | validateFileExists, 10 | handleFileError, 11 | errorHandler, 12 | ConfigurationError, 13 | AnalysisError, 14 | } from "../utils/error-handler"; 15 | import type { CodeDiagnostic, AnalysisResult } from "../types"; 16 | import { renderMarkdown } from "../utils/render-markdown"; 17 | 18 | export class AnalyzeCommand { 19 | private codeAnalyzer: CodeAnalyzer; 20 | private aiService: AIService; 21 | private configManager: ConfigManager; 22 | 23 | constructor() { 24 | this.codeAnalyzer = CodeAnalyzer.getInstance(); 25 | this.aiService = AIService.getInstance(); 26 | this.configManager = ConfigManager.getInstance(); 27 | } 28 | 29 | public async handle(filePath: string, options: any = {}): Promise { 30 | try { 31 | // Validate configuration 32 | if (!(await this.configManager.hasValidConfig())) { 33 | errorHandler.handle( 34 | new ConfigurationError( 35 | "No valid configuration found. Please run: cyrus config init", 36 | ), 37 | "analyze-command", 38 | ); 39 | return; 40 | } 41 | 42 | // Validate file exists 43 | try { 44 | validateFileExists(filePath); 45 | } catch (error) { 46 | handleFileError(error as Error, filePath); 47 | return; 48 | } 49 | 50 | // Check if file is supported 51 | if (!LanguageDetector.isSupported(filePath)) { 52 | errorHandler.handle( 53 | new AnalysisError( 54 | `Unsupported file type: ${filePath}. Supported extensions: ${LanguageDetector.getSupportedExtensions().join(", ")}`, 55 | filePath, 56 | ), 57 | "analyze-command", 58 | ); 59 | return; 60 | } 61 | 62 | const progressBar = createAIProgress("code analysis", { 63 | theme: "default", 64 | showETA: true, 65 | }); 66 | 67 | progressBar.start(); 68 | 69 | try { 70 | // Step 1: Static analysis 71 | progressBar.updateStage("Preparing code analysis", 50); 72 | console.log(chalk.gray("\nStarting static analysis...")); 73 | const analysisResult = await this.codeAnalyzer.analyzeFile(filePath); 74 | console.log(chalk.gray("Static analysis completed.")); 75 | 76 | // Step 2: AI-powered analysis 77 | progressBar.incrementStage(25); 78 | console.log(chalk.gray("Starting AI analysis...")); 79 | let code: string; 80 | try { 81 | code = fs.readFileSync(filePath, "utf-8"); 82 | } catch (error) { 83 | progressBar.fail("Failed to read file"); 84 | handleFileError(error as Error, filePath); 85 | return; 86 | } 87 | console.log(chalk.gray("Calling AI service...")); 88 | progressBar.updateStage("Processing with AI", 80); 89 | const aiAnalysis = await this.aiService.analyzeCode(code, filePath); 90 | console.log(chalk.gray("\nAI analysis completed.")); 91 | 92 | progressBar.incrementStage(100); 93 | progressBar.complete("Analysis completed successfully"); 94 | console.log(); // Add spacing after progress bar 95 | 96 | // Display results 97 | await this.displayResults(filePath, analysisResult, aiAnalysis); 98 | 99 | // Handle specific options 100 | if (options.explain && analysisResult.diagnostics.length > 0) { 101 | await this.explainErrors(analysisResult.diagnostics, code); 102 | } 103 | 104 | if (options.security) { 105 | await this.runSecurityScan(code, filePath); 106 | } 107 | 108 | if (options.metrics) { 109 | this.displayMetrics(analysisResult); 110 | } 111 | } catch (error) { 112 | progressBar.fail("Analysis failed"); 113 | console.log(); // Add spacing after progress bar 114 | errorHandler.handle( 115 | new AnalysisError( 116 | `Analysis failed: ${(error as Error).message}`, 117 | filePath, 118 | ), 119 | "analyze-command", 120 | ); 121 | } 122 | } catch (error) { 123 | // Handle error and ensure process doesn't exit immediately 124 | errorHandler.handle(error as Error, "analyze-command"); 125 | // Give time for error output to be displayed 126 | await new Promise((resolve) => setTimeout(resolve, 100)); 127 | } 128 | } 129 | 130 | private async displayResults( 131 | filePath: string, 132 | analysisResult: AnalysisResult, 133 | aiAnalysis: string, 134 | ): Promise { 135 | const config = await this.configManager.getConfig(); 136 | 137 | console.log(chalk.cyan(`\n📊 Analysis Results for: ${filePath}`)); 138 | console.log(chalk.gray("═".repeat(60))); 139 | 140 | // Static analysis results 141 | if (analysisResult.diagnostics.length === 0) { 142 | console.log(chalk.green("✅ No syntax errors found")); 143 | } else { 144 | console.log( 145 | chalk.red(`❌ Found ${analysisResult.diagnostics.length} issue(s):`), 146 | ); 147 | this.displayDiagnostics(analysisResult.diagnostics); 148 | } 149 | 150 | // Code metrics 151 | if (analysisResult.metrics) { 152 | console.log(chalk.cyan("\n📈 Code Metrics:")); 153 | this.displayMetricsInline(analysisResult.metrics); 154 | } 155 | 156 | // AI analysis 157 | if (config?.outputFormat === "json") { 158 | console.log(chalk.cyan("\n🤖 AI Analysis:")); 159 | console.log(JSON.stringify({ aiAnalysis }, null, 2)); 160 | } else { 161 | // Render AI analysis with Markdown formatting 162 | const markdownContent = `## 🤖 AI Analysis\n\n${aiAnalysis}`; 163 | console.log(await renderMarkdown(markdownContent)); 164 | } 165 | } 166 | 167 | private displayDiagnostics(diagnostics: CodeDiagnostic[]): void { 168 | diagnostics.forEach((diagnostic, index) => { 169 | const severityColor = { 170 | error: chalk.red, 171 | warning: chalk.yellow, 172 | info: chalk.blue, 173 | }[diagnostic.severity]; 174 | 175 | console.log( 176 | `${index + 1}. ${severityColor(diagnostic.severity.toUpperCase())}: ${diagnostic.message}`, 177 | ); 178 | console.log( 179 | chalk.gray(` Line ${diagnostic.line}, Column ${diagnostic.column}`), 180 | ); 181 | if (diagnostic.source) { 182 | console.log(chalk.gray(` Source: ${diagnostic.source}`)); 183 | } 184 | console.log(); 185 | }); 186 | } 187 | 188 | private displayMetricsInline(metrics: any): void { 189 | console.log( 190 | chalk.white( 191 | ` Complexity: ${this.getComplexityColor(metrics.complexity)(metrics.complexity)}`, 192 | ), 193 | ); 194 | console.log( 195 | chalk.white( 196 | ` Maintainability: ${this.getMaintainabilityColor(metrics.maintainabilityIndex)(metrics.maintainabilityIndex.toFixed(1))}`, 197 | ), 198 | ); 199 | console.log( 200 | chalk.white(` Lines of Code: ${chalk.blue(metrics.linesOfCode)}`), 201 | ); 202 | console.log( 203 | chalk.white( 204 | ` Technical Debt: ${this.getTechnicalDebtColor(metrics.technicalDebt)(metrics.technicalDebt.toFixed(1))} minutes`, 205 | ), 206 | ); 207 | if (metrics.duplicateLines > 0) { 208 | console.log( 209 | chalk.white( 210 | ` Duplicate Lines: ${chalk.yellow(metrics.duplicateLines)}`, 211 | ), 212 | ); 213 | } 214 | } 215 | 216 | private displayMetrics(analysisResult: AnalysisResult): void { 217 | if (!analysisResult.metrics) return; 218 | 219 | const { metrics } = analysisResult; 220 | console.log(chalk.cyan("\n📊 Detailed Metrics:")); 221 | console.log(chalk.gray("─".repeat(40))); 222 | 223 | console.log(chalk.white("Code Quality Assessment:")); 224 | console.log( 225 | ` • Cyclomatic Complexity: ${this.getComplexityColor(metrics.complexity)(metrics.complexity)} ${this.getComplexityDescription(metrics.complexity)}`, 226 | ); 227 | console.log( 228 | ` • Maintainability Index: ${this.getMaintainabilityColor(metrics.maintainabilityIndex)(metrics.maintainabilityIndex.toFixed(1))}/100 ${this.getMaintainabilityDescription(metrics.maintainabilityIndex)}`, 229 | ); 230 | console.log( 231 | ` • Technical Debt: ${this.getTechnicalDebtColor(metrics.technicalDebt)(metrics.technicalDebt.toFixed(1))} minutes ${this.getTechnicalDebtDescription(metrics.technicalDebt)}`, 232 | ); 233 | 234 | console.log(chalk.white("\nCode Statistics:")); 235 | console.log(` • Total Lines: ${chalk.blue(metrics.linesOfCode)}`); 236 | console.log( 237 | ` • Duplicate Lines: ${metrics.duplicateLines > 0 ? chalk.yellow(metrics.duplicateLines) : chalk.green("0")}`, 238 | ); 239 | 240 | if (metrics.testCoverage !== undefined) { 241 | console.log( 242 | ` • Test Coverage: ${this.getCoverageColor(metrics.testCoverage)(metrics.testCoverage.toFixed(1))}%`, 243 | ); 244 | } 245 | } 246 | 247 | private async explainErrors( 248 | diagnostics: CodeDiagnostic[], 249 | code: string, 250 | ): Promise { 251 | console.log(chalk.cyan("\n🔍 Error Explanations:")); 252 | console.log(chalk.gray("─".repeat(40))); 253 | 254 | for (let i = 0; i < Math.min(3, diagnostics.length); i++) { 255 | const diagnostic = diagnostics[i]; 256 | console.log( 257 | chalk.yellow(`\nExplaining error ${i + 1}: ${diagnostic.message}`), 258 | ); 259 | 260 | try { 261 | const explanation = await this.aiService.explainError( 262 | diagnostic.message, 263 | code, 264 | ); 265 | console.log(chalk.white(explanation)); 266 | } catch (error) { 267 | console.error( 268 | chalk.red(`Failed to explain error: ${(error as Error).message}`), 269 | ); 270 | } 271 | } 272 | 273 | if (diagnostics.length > 3) { 274 | console.log( 275 | chalk.gray( 276 | `\n... and ${diagnostics.length - 3} more errors. Use --explain-all to see all explanations.`, 277 | ), 278 | ); 279 | } 280 | } 281 | 282 | private async runSecurityScan(code: string, filePath: string): Promise { 283 | const progressBar = createAIProgress("security scan", { 284 | theme: "default", 285 | showETA: false, 286 | }); 287 | 288 | progressBar.start(); 289 | 290 | try { 291 | progressBar.updateStage("Preparing security scan", 30); 292 | const detection = await LanguageDetector.detectLanguage(filePath, code); 293 | if (!detection.language) { 294 | progressBar.fail("Security scan failed: Unknown language"); 295 | return; 296 | } 297 | 298 | progressBar.updateStage("Processing with AI", 80); 299 | const vulnerabilities = await this.aiService.scanForSecurity( 300 | code, 301 | detection.language, 302 | ); 303 | progressBar.complete( 304 | `Security scan completed - found ${vulnerabilities.length} potential issues`, 305 | ); 306 | 307 | if (vulnerabilities.length > 0) { 308 | console.log(chalk.cyan("\n🔒 Security Analysis:")); 309 | console.log(chalk.gray("─".repeat(40))); 310 | 311 | vulnerabilities.forEach((vuln, index) => { 312 | const severityColor = { 313 | low: chalk.blue, 314 | medium: chalk.yellow, 315 | high: chalk.red, 316 | critical: chalk.bgRed.white, 317 | }[vuln.severity]; 318 | 319 | console.log( 320 | `${index + 1}. ${severityColor(vuln.severity.toUpperCase())}: ${vuln.title}`, 321 | ); 322 | console.log(chalk.white(` ${vuln.description}`)); 323 | console.log(chalk.gray(` Line ${vuln.line} in ${vuln.file}`)); 324 | if (vuln.cwe) { 325 | console.log(chalk.gray(` CWE: ${vuln.cwe}`)); 326 | } 327 | if (vuln.fix) { 328 | console.log(chalk.green(` Fix: ${vuln.fix}`)); 329 | } 330 | console.log(); 331 | }); 332 | } else { 333 | console.log(chalk.green("\n✅ No security vulnerabilities detected")); 334 | } 335 | } catch (error) { 336 | progressBar.fail("Security scan failed"); 337 | console.error( 338 | chalk.red(`Security scan failed: ${(error as Error).message}`), 339 | ); 340 | } 341 | } 342 | 343 | private getComplexityColor(complexity: number) { 344 | if (complexity <= 10) return chalk.green; 345 | if (complexity <= 20) return chalk.yellow; 346 | return chalk.red; 347 | } 348 | 349 | private getComplexityDescription(complexity: number): string { 350 | if (complexity <= 10) return "(Low - Easy to maintain)"; 351 | if (complexity <= 20) return "(Medium - Moderate complexity)"; 352 | if (complexity <= 30) return "(High - Consider refactoring)"; 353 | return "(Very High - Needs refactoring)"; 354 | } 355 | 356 | private getMaintainabilityColor(index: number) { 357 | if (index >= 70) return chalk.green; 358 | if (index >= 50) return chalk.yellow; 359 | return chalk.red; 360 | } 361 | 362 | private getMaintainabilityDescription(index: number): string { 363 | if (index >= 70) return "(Good)"; 364 | if (index >= 50) return "(Fair)"; 365 | return "(Poor)"; 366 | } 367 | 368 | private getTechnicalDebtColor(debt: number) { 369 | if (debt <= 30) return chalk.green; 370 | if (debt <= 60) return chalk.yellow; 371 | return chalk.red; 372 | } 373 | 374 | private getTechnicalDebtDescription(debt: number): string { 375 | if (debt <= 30) return "(Low debt)"; 376 | if (debt <= 60) return "(Moderate debt)"; 377 | return "(High debt)"; 378 | } 379 | 380 | private getCoverageColor(coverage: number) { 381 | if (coverage >= 80) return chalk.green; 382 | if (coverage >= 60) return chalk.yellow; 383 | return chalk.red; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from "commander"; 4 | import figlet from "figlet"; 5 | import { vice } from "gradient-string"; 6 | import chalk from "chalk"; 7 | import { ConfigCommand } from "./commands/config"; 8 | import { AnalyzeCommand } from "./commands/analyze"; 9 | import { MentorCommand } from "./commands/mentor"; 10 | import { GenerateCommand } from "./commands/generate"; 11 | import { HealthCommand } from "./commands/health"; 12 | import { DetectCommand } from "./commands/detect"; 13 | import { QualityCommand } from "./commands/quality"; 14 | import { CompareCommand } from "./commands/compare"; 15 | import { LearnCommand } from "./commands/learn"; 16 | import { ConfigManager } from "./config/config"; 17 | 18 | // Handle process termination gracefully 19 | process.on("SIGINT", () => { 20 | console.log(chalk.yellow("\n\nOperation cancelled by user")); 21 | process.exit(0); 22 | }); 23 | 24 | process.on("SIGTERM", () => { 25 | process.exit(0); 26 | }); 27 | 28 | const program = new Command(); 29 | 30 | // Display banner only if not in quiet mode 31 | if (!process.argv.includes("--quiet") && !process.argv.includes("-q")) { 32 | console.log(); 33 | console.log( 34 | vice( 35 | figlet.textSync("CYRUS", { 36 | font: "ANSI Shadow", 37 | horizontalLayout: "default", 38 | verticalLayout: "default", 39 | }), 40 | ), 41 | ); 42 | console.log(); 43 | console.log(chalk.dim(" The Code Empire Analyzer")); 44 | console.log( 45 | chalk.dim(" AI-Powered Debugging & Analysis CLI for Modern Developers"), 46 | ); 47 | console.log(); 48 | } 49 | 50 | // Configure CLI 51 | program 52 | .name("cyrus") 53 | .description("AI-Powered Code Debugging & Analysis for Developers") 54 | .version("1.0.0"); 55 | 56 | // Configuration command 57 | program 58 | .command("config") 59 | .description("Configure Cyrus settings") 60 | .argument("[action]", "Action to perform: init, set, get, show, delete") 61 | .argument("[type]", "Configuration type (e.g., apikey, model, provider)") 62 | .argument("[value]", "Value to set") 63 | .action(async (action, type, value) => { 64 | const configCommand = new ConfigCommand(); 65 | await configCommand.handle([action, type, value].filter(Boolean)); 66 | }); 67 | 68 | // Detect command - NEW 69 | program 70 | .command("detect ") 71 | .description("Detect programming languages, frameworks, and tools") 72 | .option("-d, --detailed", "Show detailed language features") 73 | .option("--json", "Output results in JSON format") 74 | .option("-o, --output ", "Output file for JSON results") 75 | .action(async (targetPath, options) => { 76 | const detectCommand = new DetectCommand(); 77 | await detectCommand.handle(targetPath, options); 78 | }); 79 | 80 | // Analyze command 81 | program 82 | .command("analyze ") 83 | .description("Analyze a source file for bugs, issues, and improvements") 84 | .option("-e, --explain", "Get AI explanation for errors") 85 | .option("-s, --security", "Include security vulnerability scan") 86 | .option("-m, --metrics", "Show detailed code metrics") 87 | .option("--json", "Output results in JSON format") 88 | .action(async (file, options) => { 89 | await checkConfig("analyze"); 90 | const analyzeCommand = new AnalyzeCommand(); 91 | await analyzeCommand.handle(file, options); 92 | }); 93 | 94 | // Mentor command 95 | program 96 | .command("mentor ") 97 | .description("Get personalized code mentoring and learning guidance") 98 | .option("-i, --interactive", "Start interactive mentoring session") 99 | .option("--skip-setup", "Skip mentor session setup") 100 | .action(async (file, options) => { 101 | await checkConfig("mentor"); 102 | const mentorCommand = new MentorCommand(); 103 | await mentorCommand.handle(file, options); 104 | }); 105 | 106 | // Generate command group 107 | const generateCommand = program 108 | .command("generate") 109 | .description("Generate code, tests, documentation, and more"); 110 | 111 | generateCommand 112 | .command("tests ") 113 | .description("Generate comprehensive unit tests") 114 | .option("--dry-run", "Preview without saving") 115 | .action(async (file, options) => { 116 | await checkConfig("generate"); 117 | const generateCmd = new GenerateCommand(); 118 | await generateCmd.handle("tests", file, options); 119 | }); 120 | 121 | generateCommand 122 | .command("docs ") 123 | .description("Generate comprehensive documentation") 124 | .option("--dry-run", "Preview without saving") 125 | .action(async (file, options) => { 126 | await checkConfig("generate"); 127 | const generateCmd = new GenerateCommand(); 128 | await generateCmd.handle("docs", file, options); 129 | }); 130 | 131 | generateCommand 132 | .command("refactor ") 133 | .description("Generate refactoring suggestions with before/after examples") 134 | .option("--dry-run", "Preview without saving") 135 | .option("-i, --interactive", "Interactively apply suggestions") 136 | .action(async (file, options) => { 137 | await checkConfig("generate"); 138 | const generateCmd = new GenerateCommand(); 139 | await generateCmd.handle("refactor", file, options); 140 | }); 141 | 142 | generateCommand 143 | .command("project ") 144 | .description("Generate complete project structure from description") 145 | .option("--dry-run", "Preview without creating files") 146 | .action(async (description, options) => { 147 | await checkConfig("generate"); 148 | const generateCmd = new GenerateCommand(); 149 | await generateCmd.handle("project", description, options); 150 | }); 151 | 152 | generateCommand 153 | .command("component ") 154 | .description("Generate code components (React, Vue, etc.)") 155 | .option("--dry-run", "Preview without saving") 156 | .action(async (name, options) => { 157 | await checkConfig("generate"); 158 | const generateCmd = new GenerateCommand(); 159 | await generateCmd.handle("component", name, options); 160 | }); 161 | 162 | generateCommand 163 | .command("config ") 164 | .description("Generate configuration files (eslint, prettier, etc.)") 165 | .option("--dry-run", "Preview without saving") 166 | .action(async (type, options) => { 167 | const generateCmd = new GenerateCommand(); 168 | await generateCmd.handle("config", type, options); 169 | }); 170 | 171 | // Health scan command 172 | program 173 | .command("health") 174 | .description("Comprehensive codebase health analysis") 175 | .option("-p, --path ", "Path to analyze (default: current directory)") 176 | .option("--detailed", "Show detailed analysis for each file") 177 | .option("--security", "Include security vulnerability scanning") 178 | .option("--parallel", "Process files in parallel for faster analysis") 179 | .option("--save", "Save health report to file") 180 | .option( 181 | "-o, --output ", 182 | "Output file for health report", 183 | "health-report.json", 184 | ) 185 | .option("--trends", "Show health trends (requires historical data)") 186 | .action(async (options) => { 187 | await checkConfig("health"); 188 | const healthCommand = new HealthCommand(); 189 | await healthCommand.handle(options); 190 | }); 191 | 192 | // Quality score command 193 | program 194 | .command("quality ") 195 | .description( 196 | "Calculate comprehensive code quality score with AI-powered recommendations", 197 | ) 198 | .option("--max-files ", "Maximum number of files to analyze", "50") 199 | .option("--json", "Output results in JSON format") 200 | .action(async (target, options) => { 201 | await checkConfig("quality"); 202 | const qualityCommand = new QualityCommand(); 203 | await qualityCommand.handle(target, options); 204 | }); 205 | 206 | // Compare command - Compare two code files or snippets 207 | program 208 | .command("compare ") 209 | .description("Compare two code files or snippets with AI-powered analysis") 210 | .option("-w, --words", "Show word-by-word diff instead of line-by-line") 211 | .option("-d, --detailed", "Show detailed metrics comparison") 212 | .option("-s, --security", "Include security comparison analysis") 213 | .option("--json", "Output results in JSON format") 214 | .action(async (first, second, options) => { 215 | await checkConfig("compare"); 216 | const compareCommand = new CompareCommand(); 217 | await compareCommand.handle(first, second, options); 218 | }); 219 | 220 | // Learning Assistant command 221 | program 222 | .command("learn") 223 | .description( 224 | "Interactive AI-powered learning assistant with tutorials and challenges", 225 | ) 226 | .option("-a, --assessment", "Start with skill assessment") 227 | .option( 228 | "-c, --challenge [difficulty]", 229 | "Start coding challenge (easy/medium/hard)", 230 | ) 231 | .option("-t, --tutorial [topic]", "Start interactive tutorial") 232 | .option("-p, --progress", "Show learning progress and achievements") 233 | .action(async (options) => { 234 | await checkConfig("learn"); 235 | const learnCommand = new LearnCommand(); 236 | await learnCommand.handle(options); 237 | }); 238 | 239 | // Quick commands for common tasks 240 | program 241 | .command("fix ") 242 | .description("Quick fix: analyze and explain the most critical issues") 243 | .action(async (file) => { 244 | await checkConfig("fix"); 245 | const analyzeCommand = new AnalyzeCommand(); 246 | await analyzeCommand.handle(file, { explain: true, metrics: true }); 247 | }); 248 | 249 | program 250 | .command("review ") 251 | .description("Code review: comprehensive analysis with suggestions") 252 | .action(async (file) => { 253 | await checkConfig("review"); 254 | console.log(chalk.cyan("🔍 Comprehensive Code Review\n")); 255 | 256 | const analyzeCommand = new AnalyzeCommand(); 257 | const generateCommand = new GenerateCommand(); 258 | 259 | await analyzeCommand.handle(file, { security: true, metrics: true }); 260 | console.log(`\n${chalk.gray("─".repeat(60))}\n`); 261 | await generateCommand.handle("refactor", file, { dryRun: true }); 262 | }); 263 | 264 | program 265 | .command("study ") 266 | .description("Study mode: detailed mentoring with explanations") 267 | .action(async (file) => { 268 | await checkConfig("study"); 269 | const mentorCommand = new MentorCommand(); 270 | await mentorCommand.handle(file, { interactive: true }); 271 | }); 272 | 273 | // Add version flag with custom handler 274 | program 275 | .option("-q, --quiet", "Suppress banner output") 276 | .option("--no-color", "Disable colored output"); 277 | 278 | // Global error handler 279 | program.on("command:*", () => { 280 | console.error( 281 | chalk.red("Invalid command. Use 'cyrus --help' for available commands."), 282 | ); 283 | process.exit(1); 284 | }); 285 | 286 | // Configuration validation function 287 | async function checkConfig(commandName: string) { 288 | // Skip config check for config command itself and help 289 | if (commandName === "config" || commandName === "help") { 290 | return; 291 | } 292 | 293 | const configManager = ConfigManager.getInstance(); 294 | if (!(await configManager.hasValidConfig())) { 295 | console.log(chalk.yellow("⚠️ No valid configuration found.")); 296 | console.log(chalk.white("\nRun the following command to get started:\n")); 297 | console.log(chalk.cyan("cyrus config init")); 298 | console.log(); 299 | process.exit(1); 300 | } 301 | } 302 | 303 | // Enhanced help 304 | program.configureHelp({ 305 | sortSubcommands: true, 306 | subcommandTerm: (cmd) => cmd.name(), 307 | }); 308 | 309 | // Add examples to help 310 | program.addHelpText( 311 | "after", 312 | ` 313 | ${chalk.bold("Examples:")} 314 | 315 | ${chalk.dim("Setup and configuration")} 316 | $ cyrus config init ${chalk.dim("# Setup Cyrus with your AI provider")} 317 | $ cyrus config set apikey sk-xxx... ${chalk.dim("# Set your API key")} 318 | 319 | ${chalk.dim("Language detection")} 320 | $ cyrus detect . ${chalk.dim("# Detect languages in current directory")} 321 | $ cyrus detect src/app.ts --detailed ${chalk.dim("# Detailed analysis of a file")} 322 | $ cyrus detect . --json -o langs.json ${chalk.dim("# Export project language info")} 323 | 324 | ${chalk.dim("Code analysis")} 325 | $ cyrus analyze src/app.js ${chalk.dim("# Analyze a JavaScript file")} 326 | $ cyrus fix src/buggy.py ${chalk.dim("# Quick fix most critical issues")} 327 | $ cyrus review src/component.tsx ${chalk.dim("# Comprehensive code review")} 328 | 329 | ${chalk.dim("Learning and mentoring")} 330 | $ cyrus mentor src/complex.ts ${chalk.dim("# Get personalized code mentoring")} 331 | $ cyrus study src/algorithm.js ${chalk.dim("# Study mode with detailed explanations")} 332 | $ cyrus learn ${chalk.dim("# Interactive learning assistant")} 333 | $ cyrus learn --challenge medium ${chalk.dim("# Start coding challenge")} 334 | $ cyrus learn --tutorial ${chalk.dim("# Interactive tutorial session")} 335 | 336 | ${chalk.dim("Code generation")} 337 | $ cyrus generate tests src/utils.js ${chalk.dim("# Generate comprehensive unit tests")} 338 | $ cyrus generate docs src/api.py ${chalk.dim("# Generate detailed documentation")} 339 | $ cyrus generate refactor src/old.js ${chalk.dim("# Get refactoring suggestions")} 340 | 341 | ${chalk.dim("Code comparison")} 342 | $ cyrus compare old.js new.js ${chalk.dim("# Compare two code files")} 343 | $ cyrus compare file1.py file2.py -d ${chalk.dim("# Detailed comparison with metrics")} 344 | $ cyrus compare v1.ts v2.ts --security ${chalk.dim("# Include security analysis")} 345 | $ cyrus compare "old code" "new code" ${chalk.dim("# Compare code snippets directly")} 346 | 347 | ${chalk.dim("Project health and quality")} 348 | $ cyrus health ${chalk.dim("# Scan entire codebase health")} 349 | $ cyrus health --detailed ${chalk.dim("# Detailed health report")} 350 | $ cyrus quality . ${chalk.dim("# Calculate project quality score")} 351 | $ cyrus quality src/app.ts ${chalk.dim("# Quality score for specific file")} 352 | 353 | ${chalk.dim("For more information:")} ${chalk.underline("https://github.com/ali-master/cyrus")} 354 | `, 355 | ); 356 | 357 | // Parse command line arguments 358 | program 359 | .parseAsync(process.argv) 360 | .then(() => { 361 | // If no command was provided, show help 362 | if (!process.argv.slice(2).length) { 363 | program.outputHelp(); 364 | } 365 | }) 366 | .catch((error) => { 367 | console.error(chalk.red("Error:", error.message)); 368 | process.exit(1); 369 | }); 370 | -------------------------------------------------------------------------------- /src/services/ai-service.ts: -------------------------------------------------------------------------------- 1 | import { generateText } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import { anthropic } from "@ai-sdk/anthropic"; 4 | import { google } from "@ai-sdk/google"; 5 | import { xai } from "@ai-sdk/xai"; 6 | import { createOpenAI } from "@ai-sdk/openai"; 7 | import chalk from "chalk"; 8 | import type { 9 | SecurityVulnerability, 10 | RefactorSuggestion, 11 | GeneratedCode, 12 | Config, 13 | } from "../types"; 14 | import { ConfigManager } from "../config/config"; 15 | import { handleAIError, ConfigurationError } from "../utils/error-handler"; 16 | 17 | export class AIService { 18 | private static instance: AIService; 19 | private config!: Config; 20 | 21 | private constructor() { 22 | // Config will be initialized before use 23 | } 24 | 25 | private async ensureConfig() { 26 | if (!this.config) { 27 | try { 28 | this.config = 29 | (await ConfigManager.getInstance().getConfig()) || 30 | ConfigManager.getInstance().getDefaultConfig(); 31 | } catch (error) { 32 | throw new ConfigurationError( 33 | `Failed to load AI service configuration: ${(error as Error).message}`, 34 | ); 35 | } 36 | } 37 | } 38 | 39 | public static getInstance(): AIService { 40 | if (!AIService.instance) { 41 | AIService.instance = new AIService(); 42 | } 43 | return AIService.instance; 44 | } 45 | 46 | private getProvider() { 47 | const { aiProvider } = this.config; 48 | 49 | if (!aiProvider || !aiProvider.name) { 50 | throw new ConfigurationError( 51 | "AI provider not configured. Please run: cyrus config init", 52 | ); 53 | } 54 | 55 | switch (aiProvider.name.toLowerCase()) { 56 | case "anthropic": 57 | return anthropic(aiProvider.model); 58 | case "google": 59 | return google(aiProvider.model); 60 | case "xai": 61 | return xai(aiProvider.model); 62 | case "ollama": 63 | case "lmstudio": 64 | case "local": { 65 | // Use OpenAI-compatible provider for local models 66 | const localProvider = createOpenAI({ 67 | baseURL: 68 | aiProvider.baseURL || this.getDefaultLocalURL(aiProvider.name), 69 | apiKey: aiProvider.apiKey || "not-required", // Some local providers don't need API keys 70 | }); 71 | return localProvider(aiProvider.model); 72 | } 73 | case "openai": 74 | default: 75 | return openai(aiProvider.model); 76 | } 77 | } 78 | 79 | private getDefaultLocalURL(provider: string): string { 80 | const urls: Record = { 81 | ollama: "http://localhost:11434/v1", 82 | lmstudio: "http://localhost:1234/v1", 83 | local: "http://localhost:8080/v1", 84 | }; 85 | return urls[provider] || "http://localhost:8080/v1"; 86 | } 87 | 88 | public async analyzeCode(code: string, filePath: string): Promise { 89 | await this.ensureConfig(); 90 | 91 | // Check if the AI service is available for local providers 92 | if (["ollama", "lmstudio", "local"].includes(this.config.aiProvider.name)) { 93 | try { 94 | const testUrl = `${this.config.aiProvider.baseURL}/v1/models`; 95 | const controller = new AbortController(); 96 | const timeout = setTimeout(() => controller.abort(), 3000); // 3 second test 97 | 98 | await fetch(testUrl, { signal: controller.signal }); 99 | clearTimeout(timeout); 100 | } catch { 101 | console.log( 102 | chalk.yellow( 103 | `\n⚠️ AI service not available at ${this.config.aiProvider.baseURL}`, 104 | ), 105 | ); 106 | console.log( 107 | chalk.gray("Returning mock analysis for demonstration purposes\n"), 108 | ); 109 | 110 | // Return a mock analysis 111 | return `Code Analysis for ${filePath}: 112 | 113 | 1. Code Quality: The code appears to be well-structured. 114 | 2. Potential Issues: Found console.log statement that should be replaced with proper logging. 115 | 3. Best Practices: Consider adding more type annotations for better type safety. 116 | 4. Performance: No major performance concerns detected. 117 | 5. Security: No obvious security vulnerabilities found. 118 | 119 | Note: This is a mock analysis. Please ensure your AI service is running at ${this.config.aiProvider.baseURL}`; 120 | } 121 | } 122 | 123 | const prompt = ` 124 | You are an expert code analyst. Analyze the following code for: 125 | 1. Logic errors and potential bugs 126 | 2. Performance issues 127 | 3. Code quality concerns 128 | 4. Best practice violations 129 | 5. Security vulnerabilities 130 | 131 | File: ${filePath} 132 | Code: 133 | \`\`\` 134 | ${code} 135 | \`\`\` 136 | 137 | Provide a detailed analysis with specific line references where applicable. 138 | Format your response using markdown for better readability in terminal output. 139 | `; 140 | 141 | try { 142 | const controller = new AbortController(); 143 | const timeout = setTimeout(() => controller.abort(), 30000); // 30 second timeout 144 | 145 | const { text } = await generateText({ 146 | model: this.getProvider(), 147 | prompt, 148 | maxTokens: 1000, 149 | temperature: 0.3, 150 | abortSignal: controller.signal, 151 | }); 152 | 153 | clearTimeout(timeout); 154 | return text; 155 | } catch (error) { 156 | if ((error as any).name === "AbortError") { 157 | throw new Error( 158 | "AI service request timed out after 30 seconds. Please check your AI provider configuration.", 159 | ); 160 | } 161 | handleAIError(error as Error, this.config.aiProvider.name); 162 | throw error; // Re-throw to maintain the calling pattern 163 | } 164 | } 165 | 166 | public async explainError( 167 | errorMessage: string, 168 | code: string, 169 | ): Promise { 170 | await this.ensureConfig(); 171 | const prompt = ` 172 | You are a helpful programming assistant. Explain this error and provide a solution: 173 | 174 | Error: ${errorMessage} 175 | 176 | Code: 177 | \`\`\` 178 | ${code} 179 | \`\`\` 180 | 181 | Provide: 182 | 1. What the error means 183 | 2. Why it occurred 184 | 3. How to fix it 185 | 4. How to prevent it in the future 186 | 5. A corrected code example 187 | 188 | Format as structured text without markdown. 189 | `; 190 | 191 | try { 192 | const { text } = await generateText({ 193 | model: this.getProvider(), 194 | prompt, 195 | maxTokens: 800, 196 | temperature: 0.3, 197 | }); 198 | 199 | return text; 200 | } catch (error) { 201 | handleAIError(error as Error, this.config.aiProvider.name); 202 | throw error; 203 | } 204 | } 205 | 206 | public async generateTests( 207 | code: string, 208 | language: string, 209 | ): Promise { 210 | await this.ensureConfig(); 211 | const prompt = ` 212 | Generate comprehensive unit tests for the following ${language} code: 213 | 214 | \`\`\`${language} 215 | ${code} 216 | \`\`\` 217 | 218 | Include: 219 | 1. Test setup and teardown 220 | 2. Happy path tests 221 | 3. Edge cases 222 | 4. Error cases 223 | 5. Mock dependencies if needed 224 | 225 | Use appropriate testing framework for ${language}. 226 | Provide the test code and explain what each test does. 227 | `; 228 | 229 | try { 230 | const { text } = await generateText({ 231 | model: this.getProvider(), 232 | prompt, 233 | maxTokens: 1500, 234 | temperature: 0.4, 235 | }); 236 | 237 | return { 238 | type: "test", 239 | content: text, 240 | language, 241 | explanation: 242 | "Generated comprehensive unit tests covering happy paths, edge cases, and error scenarios.", 243 | }; 244 | } catch (error) { 245 | handleAIError(error as Error, this.config.aiProvider.name); 246 | throw error; 247 | } 248 | } 249 | 250 | public async generateDocumentation( 251 | code: string, 252 | language: string, 253 | ): Promise { 254 | await this.ensureConfig(); 255 | const prompt = ` 256 | Generate comprehensive documentation for the following ${language} code: 257 | 258 | \`\`\`${language} 259 | ${code} 260 | \`\`\` 261 | 262 | Include: 263 | 1. Overview and purpose 264 | 2. Function/class descriptions 265 | 3. Parameter documentation 266 | 4. Return value descriptions 267 | 5. Usage examples 268 | 6. Error handling notes 269 | 270 | Use appropriate documentation format for ${language} (JSDoc, docstrings, etc.). 271 | `; 272 | 273 | try { 274 | const { text } = await generateText({ 275 | model: this.getProvider(), 276 | prompt, 277 | maxTokens: 1200, 278 | temperature: 0.3, 279 | }); 280 | 281 | return { 282 | type: "documentation", 283 | content: text, 284 | language, 285 | explanation: 286 | "Generated comprehensive documentation with usage examples and parameter descriptions.", 287 | }; 288 | } catch (error) { 289 | handleAIError(error as Error, this.config.aiProvider.name); 290 | throw error; 291 | } 292 | } 293 | 294 | public async generateRefactorSuggestions( 295 | code: string, 296 | language: string, 297 | ): Promise { 298 | await this.ensureConfig(); 299 | const prompt = ` 300 | Analyze the following ${language} code and provide refactoring suggestions: 301 | 302 | \`\`\`${language} 303 | ${code} 304 | \`\`\` 305 | 306 | For each suggestion, provide: 307 | 1. A clear title 308 | 2. Detailed description 309 | 3. Impact level (low/medium/high) 310 | 4. Category (performance/readability/maintainability/security) 311 | 5. Before and after code examples 312 | 6. Line number reference 313 | 7. Confidence score (0-100) 314 | 315 | Return as JSON array of suggestions. 316 | `; 317 | 318 | try { 319 | const { text } = await generateText({ 320 | model: this.getProvider(), 321 | prompt, 322 | maxTokens: 1500, 323 | temperature: 0.3, 324 | }); 325 | 326 | // Try to parse as JSON, fallback to text parsing 327 | try { 328 | return JSON.parse(text); 329 | } catch { 330 | // Fallback: create suggestions from text response 331 | return this.parseRefactorSuggestionsFromText(text); 332 | } 333 | } catch (error) { 334 | throw new Error(`Refactor suggestions failed: ${error}`); 335 | } 336 | } 337 | 338 | public async provideMentoring( 339 | code: string, 340 | language: string, 341 | userLevel: string, 342 | ): Promise { 343 | await this.ensureConfig(); 344 | const prompt = ` 345 | You are an expert programming mentor. Provide detailed, educational guidance for this ${language} code. 346 | User level: ${userLevel} 347 | 348 | Code: 349 | \`\`\`${language} 350 | ${code} 351 | \`\`\` 352 | 353 | Provide: 354 | 1. Line-by-line explanation appropriate for ${userLevel} level 355 | 2. Concept explanations 356 | 3. Best practices highlighted 357 | 4. Learning opportunities 358 | 5. Suggested improvements with explanations 359 | 6. Related concepts to explore 360 | 361 | Be encouraging and educational in tone. 362 | `; 363 | 364 | try { 365 | const { text } = await generateText({ 366 | model: this.getProvider(), 367 | prompt, 368 | maxTokens: 1500, 369 | temperature: 0.4, 370 | }); 371 | 372 | return text; 373 | } catch (error) { 374 | throw new Error(`Mentoring failed: ${error}`); 375 | } 376 | } 377 | 378 | public async scanForSecurity( 379 | code: string, 380 | language: string, 381 | ): Promise { 382 | await this.ensureConfig(); 383 | const prompt = ` 384 | Perform a security analysis of the following ${language} code: 385 | 386 | \`\`\`${language} 387 | ${code} 388 | \`\`\` 389 | 390 | Identify: 391 | 1. Security vulnerabilities 392 | 2. Common attack vectors 393 | 3. Input validation issues 394 | 4. Authentication/authorization problems 395 | 5. Data exposure risks 396 | 397 | For each vulnerability, provide: 398 | - Severity level 399 | - Description 400 | - Line number 401 | - CWE/OWASP category if applicable 402 | - Remediation advice 403 | 404 | Return as JSON array of vulnerabilities. 405 | `; 406 | 407 | try { 408 | const { text } = await generateText({ 409 | model: this.getProvider(), 410 | prompt, 411 | maxTokens: 1200, 412 | temperature: 0.2, 413 | }); 414 | 415 | try { 416 | return JSON.parse(text); 417 | } catch { 418 | return this.parseSecurityVulnerabilitiesFromText(text); 419 | } 420 | } catch (error) { 421 | throw new Error(`Security scan failed: ${error}`); 422 | } 423 | } 424 | 425 | public async generateProject(description: string): Promise { 426 | await this.ensureConfig(); 427 | const prompt = ` 428 | Generate a complete project structure and implementation for: 429 | ${description} 430 | 431 | Include: 432 | 1. Project structure (folders and files) 433 | 2. Package.json/requirements.txt 434 | 3. Main implementation files 435 | 4. Configuration files 436 | 5. README with setup instructions 437 | 6. Basic tests 438 | 7. Documentation 439 | 440 | Provide comprehensive, production-ready code. 441 | `; 442 | 443 | try { 444 | const { text } = await generateText({ 445 | model: this.getProvider(), 446 | prompt, 447 | maxTokens: 3000, 448 | temperature: 0.4, 449 | }); 450 | 451 | return { 452 | type: "implementation", 453 | content: text, 454 | language: "multi", 455 | explanation: 456 | "Generated complete project structure with implementation, tests, and documentation.", 457 | }; 458 | } catch (error) { 459 | throw new Error(`Project generation failed: ${error}`); 460 | } 461 | } 462 | 463 | private parseRefactorSuggestionsFromText(text: string): RefactorSuggestion[] { 464 | // Fallback parser for non-JSON responses 465 | const suggestions: RefactorSuggestion[] = []; 466 | const lines = text.split("\n"); 467 | 468 | // Simple parsing logic - this could be enhanced 469 | let currentSuggestion: Partial = {}; 470 | 471 | lines.forEach((line) => { 472 | if ( 473 | line.toLowerCase().includes("suggestion") || 474 | line.toLowerCase().includes("refactor") 475 | ) { 476 | if (currentSuggestion.title) { 477 | suggestions.push(currentSuggestion as RefactorSuggestion); 478 | } 479 | currentSuggestion = { 480 | id: `suggestion-${suggestions.length + 1}`, 481 | title: line.trim(), 482 | description: "", 483 | impact: "medium", 484 | category: "maintainability", 485 | before: "", 486 | after: "", 487 | line: 1, 488 | confidence: 80, 489 | }; 490 | } else if (currentSuggestion.title) { 491 | currentSuggestion.description += `${line} `; 492 | } 493 | }); 494 | 495 | if (currentSuggestion.title) { 496 | suggestions.push(currentSuggestion as RefactorSuggestion); 497 | } 498 | 499 | return suggestions; 500 | } 501 | 502 | private parseSecurityVulnerabilitiesFromText( 503 | text: string, 504 | ): SecurityVulnerability[] { 505 | // Fallback parser for non-JSON responses 506 | const vulnerabilities: SecurityVulnerability[] = []; 507 | const lines = text.split("\n"); 508 | 509 | lines.forEach((line) => { 510 | if ( 511 | line.toLowerCase().includes("vulnerability") || 512 | line.toLowerCase().includes("security") || 513 | line.toLowerCase().includes("risk") 514 | ) { 515 | vulnerabilities.push({ 516 | id: `vuln-${vulnerabilities.length + 1}`, 517 | title: line.trim(), 518 | severity: "medium", 519 | description: line.trim(), 520 | line: 1, 521 | file: "current", 522 | }); 523 | } 524 | }); 525 | 526 | return vulnerabilities; 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/analyzers/code-analyzer.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import * as ts from "typescript"; 3 | import type { 4 | SupportedLanguage, 5 | CodeMetrics, 6 | CodeDiagnostic, 7 | AnalysisResult, 8 | } from "../types"; 9 | import { LanguageDetector } from "./language-detector"; 10 | 11 | export class CodeAnalyzer { 12 | private static instance: CodeAnalyzer; 13 | 14 | private constructor() {} 15 | 16 | public static getInstance(): CodeAnalyzer { 17 | if (!CodeAnalyzer.instance) { 18 | CodeAnalyzer.instance = new CodeAnalyzer(); 19 | } 20 | return CodeAnalyzer.instance; 21 | } 22 | 23 | public async analyzeFile(filePath: string): Promise { 24 | try { 25 | const content = fs.readFileSync(filePath, "utf-8"); 26 | const detection = await LanguageDetector.detectLanguage( 27 | filePath, 28 | content, 29 | ); 30 | 31 | if (!detection.language) { 32 | throw new Error(`Unsupported file type: ${filePath}`); 33 | } 34 | 35 | const diagnostics = await this.getDiagnostics( 36 | content, 37 | filePath, 38 | detection.language, 39 | ); 40 | const metrics = this.calculateMetrics(content, detection.language); 41 | 42 | return { 43 | diagnostics, 44 | metrics, 45 | suggestions: [], // Will be populated by AI service 46 | }; 47 | } catch (error) { 48 | throw new Error(`Failed to analyze file ${filePath}: ${error}`); 49 | } 50 | } 51 | 52 | private async getDiagnostics( 53 | content: string, 54 | filePath: string, 55 | language: SupportedLanguage, 56 | ): Promise { 57 | switch (language) { 58 | case "javascript": 59 | case "jsx": 60 | return this.getJavaScriptDiagnostics(content, filePath); 61 | case "typescript": 62 | case "tsx": 63 | return this.getTypeScriptDiagnostics(content, filePath); 64 | case "python": 65 | return this.getPythonDiagnostics(content, filePath); 66 | case "java": 67 | return this.getJavaDiagnostics(content, filePath); 68 | default: 69 | return this.getGenericDiagnostics(content, language); 70 | } 71 | } 72 | 73 | private getJavaScriptDiagnostics( 74 | content: string, 75 | filePath: string, 76 | ): CodeDiagnostic[] { 77 | const diagnostics: CodeDiagnostic[] = []; 78 | 79 | try { 80 | // Use TypeScript compiler for JavaScript analysis 81 | const compilerOptions: ts.CompilerOptions = { 82 | allowJs: true, 83 | checkJs: true, 84 | noEmit: true, 85 | target: ts.ScriptTarget.Latest, 86 | module: ts.ModuleKind.ESNext, 87 | }; 88 | 89 | const sourceFile = ts.createSourceFile( 90 | filePath, 91 | content, 92 | ts.ScriptTarget.Latest, 93 | true, 94 | ); 95 | const compilerHost = this.createCompilerHost( 96 | filePath, 97 | content, 98 | compilerOptions, 99 | ); 100 | const program = ts.createProgram( 101 | [filePath], 102 | compilerOptions, 103 | compilerHost, 104 | ); 105 | 106 | const tsDiagnostics = [ 107 | ...program.getSyntacticDiagnostics(sourceFile), 108 | ...program.getSemanticDiagnostics(sourceFile), 109 | ]; 110 | 111 | for (const diagnostic of tsDiagnostics) { 112 | if (diagnostic.file) { 113 | const { line, character } = 114 | diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); 115 | diagnostics.push({ 116 | message: ts.flattenDiagnosticMessageText( 117 | diagnostic.messageText, 118 | "\n", 119 | ), 120 | line: line + 1, 121 | column: character + 1, 122 | severity: 123 | diagnostic.category === ts.DiagnosticCategory.Error 124 | ? "error" 125 | : "warning", 126 | source: "typescript", 127 | }); 128 | } 129 | } 130 | } catch (error) { 131 | diagnostics.push({ 132 | message: `Analysis error: ${error}`, 133 | line: 1, 134 | column: 1, 135 | severity: "error", 136 | source: "analyzer", 137 | }); 138 | } 139 | 140 | return diagnostics; 141 | } 142 | 143 | private getTypeScriptDiagnostics( 144 | content: string, 145 | _filePath: string, 146 | ): CodeDiagnostic[] { 147 | const diagnostics: CodeDiagnostic[] = []; 148 | 149 | try { 150 | // For now, skip compiler-based diagnostics to avoid hanging 151 | // Just use pattern-based checks 152 | 153 | // Basic pattern-based checks for common issues 154 | const lines = content.split("\n"); 155 | for (let i = 0; i < lines.length; i++) { 156 | const line = lines[i]; 157 | const trimmed = line.trim(); 158 | 159 | // Check for console.log statements 160 | if (trimmed.includes("console.log")) { 161 | diagnostics.push({ 162 | message: 163 | "console.log statement found - consider using proper logging", 164 | line: i + 1, 165 | column: line.indexOf("console.log") + 1, 166 | severity: "warning", 167 | source: "typescript", 168 | }); 169 | } 170 | 171 | // Check for any type 172 | if (trimmed.includes(": any") || trimmed.includes("")) { 173 | diagnostics.push({ 174 | message: "Avoid using 'any' type - use specific types instead", 175 | line: i + 1, 176 | column: line.indexOf("any") + 1, 177 | severity: "warning", 178 | source: "typescript", 179 | }); 180 | } 181 | } 182 | } catch (error) { 183 | diagnostics.push({ 184 | message: `TypeScript analysis error: ${error}`, 185 | line: 1, 186 | column: 1, 187 | severity: "error", 188 | source: "analyzer", 189 | }); 190 | } 191 | 192 | return diagnostics; 193 | } 194 | 195 | private getPythonDiagnostics( 196 | content: string, 197 | _filePath: string, 198 | ): CodeDiagnostic[] { 199 | const diagnostics: CodeDiagnostic[] = []; 200 | 201 | try { 202 | // Basic Python syntax checking 203 | const lines = content.split("\n"); 204 | 205 | for (let i = 0; i < lines.length; i++) { 206 | const line = lines[i]; 207 | const trimmed = line.trim(); 208 | 209 | if (trimmed === "" || trimmed.startsWith("#")) { 210 | continue; 211 | } 212 | 213 | // Check for syntax patterns 214 | if (trimmed.includes("print ") && !trimmed.includes("print(")) { 215 | diagnostics.push({ 216 | message: 217 | "Use print() function instead of print statement (Python 3 syntax)", 218 | line: i + 1, 219 | column: line.indexOf("print") + 1, 220 | severity: "error", 221 | source: "python", 222 | }); 223 | } 224 | 225 | // Check for common issues 226 | if (trimmed.match(/^(if|for|while|def|class).*[^:]$/)) { 227 | diagnostics.push({ 228 | message: "Missing colon after control statement", 229 | line: i + 1, 230 | column: line.length, 231 | severity: "error", 232 | source: "python", 233 | }); 234 | } 235 | } 236 | } catch (error) { 237 | diagnostics.push({ 238 | message: `Python analysis error: ${error}`, 239 | line: 1, 240 | column: 1, 241 | severity: "error", 242 | source: "analyzer", 243 | }); 244 | } 245 | 246 | return diagnostics; 247 | } 248 | 249 | private getJavaDiagnostics( 250 | content: string, 251 | _filePath: string, 252 | ): CodeDiagnostic[] { 253 | const diagnostics: CodeDiagnostic[] = []; 254 | 255 | try { 256 | const lines = content.split("\n"); 257 | 258 | for (let i = 0; i < lines.length; i++) { 259 | const line = lines[i]; 260 | const trimmed = line.trim(); 261 | 262 | // Check for common Java syntax issues 263 | if ( 264 | trimmed.match(/^(if|for|while)\\s*\\([^)]*\\)\\s*[^{]/) && 265 | !trimmed.endsWith(";") 266 | ) { 267 | diagnostics.push({ 268 | message: "Missing braces around control statement body", 269 | line: i + 1, 270 | column: 1, 271 | severity: "warning", 272 | source: "java", 273 | }); 274 | } 275 | 276 | if (trimmed.includes("System.out.println") && !trimmed.endsWith(";")) { 277 | diagnostics.push({ 278 | message: "Missing semicolon", 279 | line: i + 1, 280 | column: line.length, 281 | severity: "error", 282 | source: "java", 283 | }); 284 | } 285 | } 286 | } catch (error) { 287 | diagnostics.push({ 288 | message: `Java analysis error: ${error}`, 289 | line: 1, 290 | column: 1, 291 | severity: "error", 292 | source: "analyzer", 293 | }); 294 | } 295 | 296 | return diagnostics; 297 | } 298 | 299 | private getGenericDiagnostics( 300 | content: string, 301 | _language: SupportedLanguage, 302 | ): CodeDiagnostic[] { 303 | const diagnostics: CodeDiagnostic[] = []; 304 | 305 | // Basic checks that apply to most languages 306 | const lines = content.split("\n"); 307 | 308 | for (let i = 0; i < lines.length; i++) { 309 | const line = lines[i]; 310 | 311 | // Check for very long lines 312 | if (line.length > 120) { 313 | diagnostics.push({ 314 | message: "Line too long (>120 characters)", 315 | line: i + 1, 316 | column: 121, 317 | severity: "warning", 318 | source: "style", 319 | }); 320 | } 321 | 322 | // Check for trailing whitespace 323 | if (line.endsWith(" ") || line.endsWith("\\t")) { 324 | diagnostics.push({ 325 | message: "Trailing whitespace", 326 | line: i + 1, 327 | column: line.length, 328 | severity: "info", 329 | source: "style", 330 | }); 331 | } 332 | } 333 | 334 | return diagnostics; 335 | } 336 | 337 | private calculateMetrics( 338 | content: string, 339 | language: SupportedLanguage, 340 | ): CodeMetrics { 341 | const lines = content.split("\n"); 342 | const nonEmptyLines = lines.filter((line) => line.trim() !== "").length; 343 | const commentLines = this.countCommentLines(content, language); 344 | const complexity = this.calculateComplexity(content, language); 345 | 346 | return { 347 | complexity, 348 | maintainabilityIndex: this.calculateMaintainabilityIndex( 349 | nonEmptyLines, 350 | complexity, 351 | commentLines, 352 | ), 353 | linesOfCode: nonEmptyLines, 354 | technicalDebt: this.calculateTechnicalDebt(complexity, nonEmptyLines), 355 | duplicateLines: this.findDuplicateLines(lines), 356 | }; 357 | } 358 | 359 | private countCommentLines( 360 | content: string, 361 | language: SupportedLanguage, 362 | ): number { 363 | const lines = content.split("\n"); 364 | let commentLines = 0; 365 | 366 | const commentPatterns = { 367 | javascript: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 368 | typescript: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 369 | jsx: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 370 | tsx: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 371 | python: [/^\s*#/], 372 | java: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 373 | go: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 374 | rust: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 375 | csharp: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/], 376 | php: [/^\s*\/\//, /^\s*\/\*/, /^\s*\*/, /^\s*#/], 377 | ruby: [/^\s*#/], 378 | }; 379 | 380 | const patterns = commentPatterns[language] || [/^\s*\/\//, /^\s*#/]; 381 | 382 | for (const line of lines) { 383 | if (patterns.some((pattern) => pattern.test(line))) { 384 | commentLines++; 385 | } 386 | } 387 | 388 | return commentLines; 389 | } 390 | 391 | private calculateComplexity( 392 | content: string, 393 | language: SupportedLanguage, 394 | ): number { 395 | let complexity = 1; // Base complexity 396 | 397 | const complexityPatterns = { 398 | javascript: [ 399 | /\\bif\\b/, 400 | /\\belse\\b/, 401 | /\\bfor\\b/, 402 | /\\bwhile\\b/, 403 | /\\bswitch\\b/, 404 | /\\bcatch\\b/, 405 | /\\b\\?\\.*:\\s*/, 406 | ], 407 | typescript: [ 408 | /\\bif\\b/, 409 | /\\belse\\b/, 410 | /\\bfor\\b/, 411 | /\\bwhile\\b/, 412 | /\\bswitch\\b/, 413 | /\\bcatch\\b/, 414 | /\\b\\?\\.*:\\s*/, 415 | ], 416 | python: [ 417 | /\\bif\\b/, 418 | /\\belif\\b/, 419 | /\\belse\\b/, 420 | /\\bfor\\b/, 421 | /\\bwhile\\b/, 422 | /\\btry\\b/, 423 | /\\bexcept\\b/, 424 | ], 425 | java: [ 426 | /\\bif\\b/, 427 | /\\belse\\b/, 428 | /\\bfor\\b/, 429 | /\\bwhile\\b/, 430 | /\\bswitch\\b/, 431 | /\\bcatch\\b/, 432 | /\\b\\?\\.*:\\s*/, 433 | ], 434 | }; 435 | 436 | const patterns = 437 | complexityPatterns[language as keyof typeof complexityPatterns] || 438 | complexityPatterns.javascript; 439 | 440 | for (const pattern of patterns) { 441 | const matches = content.match(new RegExp(pattern.source, "g")); 442 | if (matches) { 443 | complexity += matches.length; 444 | } 445 | } 446 | 447 | return complexity; 448 | } 449 | 450 | private calculateMaintainabilityIndex( 451 | loc: number, 452 | complexity: number, 453 | commentLines: number, 454 | ): number { 455 | // Simplified maintainability index calculation 456 | const commentRatio = commentLines / loc; 457 | const complexityPenalty = complexity * 2; 458 | const sizePenalty = Math.log(loc) * 5; 459 | 460 | const index = 100 - complexityPenalty - sizePenalty + commentRatio * 10; 461 | return Math.max(0, Math.min(100, index)); 462 | } 463 | 464 | private calculateTechnicalDebt(complexity: number, loc: number): number { 465 | // Estimate technical debt in minutes 466 | const complexityDebt = (complexity - 10) * 5; // 5 minutes per complexity point above 10 467 | const sizeDebt = Math.max(0, loc - 200) * 0.1; // 0.1 minutes per line above 200 468 | 469 | return Math.max(0, complexityDebt + sizeDebt); 470 | } 471 | 472 | private findDuplicateLines(lines: string[]): number { 473 | const lineMap = new Map(); 474 | let duplicates = 0; 475 | 476 | for (const line of lines) { 477 | const trimmed = line.trim(); 478 | if (trimmed.length > 10) { 479 | // Only consider substantial lines 480 | const count = lineMap.get(trimmed) || 0; 481 | lineMap.set(trimmed, count + 1); 482 | if (count === 1) { 483 | // First duplicate found 484 | duplicates += 2; // Count original + duplicate 485 | } else if (count > 1) { 486 | // Additional duplicates 487 | duplicates += 1; 488 | } 489 | } 490 | } 491 | 492 | return duplicates; 493 | } 494 | 495 | private createCompilerHost( 496 | filePath: string, 497 | content: string, 498 | _options: ts.CompilerOptions, 499 | ): ts.CompilerHost { 500 | return { 501 | getSourceFile: (fileName, languageVersion) => { 502 | if (fileName === filePath) { 503 | return ts.createSourceFile(fileName, content, languageVersion, true); 504 | } 505 | // Return undefined for other files to avoid external dependencies 506 | return undefined; 507 | }, 508 | writeFile: () => {}, 509 | getDefaultLibFileName: () => "lib.d.ts", 510 | useCaseSensitiveFileNames: () => false, 511 | getCanonicalFileName: (fileName) => fileName, 512 | getCurrentDirectory: () => "", 513 | getNewLine: () => "\n", 514 | fileExists: (fileName) => fileName === filePath, 515 | readFile: (fileName) => (fileName === filePath ? content : undefined), 516 | directoryExists: () => true, 517 | getDirectories: () => [], 518 | }; 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /assets/social-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | </> 86 | {} 87 | [] 88 | () 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 106 | 107 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | [ 153 | ] 154 | [ 155 | ] 156 | 157 | </> 158 | {} 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | CYRUS 172 | 173 | The Code Empire Analyzer 174 | 175 | AI-Powered Security Analysis & Code Intelligence 176 | 177 | 178 | 179 | 180 | 181 | 🛡️ Security Scanning 182 | 183 | 184 | 🤖 AI Code Analysis 185 | 186 | 187 | 🏠 Privacy-First Local AI 188 | 189 | 190 | 🌐 10+ Languages 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | cyrus analyze --security --ai src/auth.ts 203 | 204 | 205 | 🛡️ Security scan initiated... 206 | 🔍 AI detecting: TypeScript (98.7% confidence) 207 | ✓ No vulnerabilities detected 208 | 🤖 AI insights: Code follows security best practices 209 | 🔐 Authentication patterns: Secure 210 | ✅ Security analysis complete - All systems protected 211 | 📊 Confidence: 99.2% | Runtime: 0.8s 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 🔒 github.com/ali-master/cyrus 274 | 275 | 276 | -------------------------------------------------------------------------------- /src/commands/quality.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import chalk from "chalk"; 4 | import { glob } from "glob"; 5 | import { AIService } from "../services/ai-service"; 6 | import { CodeAnalyzer } from "../analyzers/code-analyzer"; 7 | import { LanguageDetector } from "../analyzers/language-detector"; 8 | import { ConfigManager } from "../config/config"; 9 | import { renderMarkdown } from "../utils/render-markdown"; 10 | import { createFileProcessingProgress } from "../utils/progress-bar"; 11 | import { 12 | validateFileExists, 13 | errorHandler, 14 | ConfigurationError, 15 | AnalysisError, 16 | } from "../utils/error-handler"; 17 | import type { CodeMetrics, AnalysisResult } from "../types"; 18 | 19 | interface QualityMetrics { 20 | overallScore: number; 21 | codeHealth: number; 22 | maintainability: number; 23 | complexity: number; 24 | testCoverage: number; 25 | documentation: number; 26 | security: number; 27 | recommendations: string[]; 28 | files: FileQuality[]; 29 | } 30 | 31 | interface FileQuality { 32 | path: string; 33 | score: number; 34 | issues: number; 35 | metrics?: CodeMetrics; 36 | } 37 | 38 | export class QualityCommand { 39 | private aiService: AIService; 40 | private codeAnalyzer: CodeAnalyzer; 41 | private configManager: ConfigManager; 42 | 43 | constructor() { 44 | this.aiService = AIService.getInstance(); 45 | this.codeAnalyzer = CodeAnalyzer.getInstance(); 46 | this.configManager = ConfigManager.getInstance(); 47 | } 48 | 49 | public async handle(target: string, options: any = {}): Promise { 50 | try { 51 | // Validate configuration 52 | if (!(await this.configManager.hasValidConfig())) { 53 | errorHandler.handle( 54 | new ConfigurationError( 55 | "No valid configuration found. Please run: cyrus config init", 56 | ), 57 | "quality-command", 58 | ); 59 | return; 60 | } 61 | 62 | try { 63 | const qualityMetrics = await this.analyzeQuality(target, options); 64 | await this.displayQualityReport(qualityMetrics); 65 | } catch (error) { 66 | console.log(); // Add newline after progress bar 67 | errorHandler.handle( 68 | new AnalysisError( 69 | `Quality analysis failed: ${(error as Error).message}`, 70 | target, 71 | ), 72 | "quality-command", 73 | ); 74 | } 75 | } catch (error) { 76 | errorHandler.handle(error as Error, "quality-command"); 77 | await new Promise((resolve) => setTimeout(resolve, 100)); 78 | } 79 | } 80 | 81 | private async analyzeQuality( 82 | target: string, 83 | options: any, 84 | ): Promise { 85 | const isDirectory = 86 | fs.existsSync(target) && fs.statSync(target).isDirectory(); 87 | let files: string[] = []; 88 | 89 | // Create progress bar 90 | const progressBar = createFileProcessingProgress(1, { 91 | // Initial estimate, will update 92 | theme: "modern", 93 | showETA: true, 94 | showSpeed: true, 95 | }); 96 | 97 | progressBar.start(); 98 | progressBar.updateStage("Scanning files", 0); 99 | 100 | if (isDirectory) { 101 | // Analyze directory 102 | const pattern = path.join( 103 | target, 104 | "**/*.{ts,js,tsx,jsx,py,java,cpp,c,cs,go,rs,rb,php}", 105 | ); 106 | files = await glob(pattern, { 107 | ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"], 108 | }); 109 | } else { 110 | // Analyze single file 111 | validateFileExists(target); 112 | if (!LanguageDetector.isSupported(target)) { 113 | throw new AnalysisError( 114 | `Unsupported file type: ${target}. Supported extensions: ${LanguageDetector.getSupportedExtensions().join(", ")}`, 115 | target, 116 | ); 117 | } 118 | files = [target]; 119 | } 120 | 121 | if (files.length === 0) { 122 | progressBar.fail("No supported files found for analysis"); 123 | throw new AnalysisError("No supported files found for analysis", target); 124 | } 125 | 126 | const filesToAnalyze = files.slice(0, options.maxFiles || 50); 127 | progressBar.updateStage("Scanning files", 100); 128 | 129 | // Analyze each file 130 | const fileQualities: FileQuality[] = []; 131 | let totalLines = 0; 132 | let totalIssues = 0; 133 | let totalComplexity = 0; 134 | 135 | progressBar.incrementStage(0); // Move to "Analyzing code" stage 136 | 137 | for (let i = 0; i < filesToAnalyze.length; i++) { 138 | const file = filesToAnalyze[i]; 139 | const progress = ((i + 1) / filesToAnalyze.length) * 100; 140 | 141 | progressBar.updateProgress(progress, i + 1); 142 | 143 | try { 144 | const result = await this.codeAnalyzer.analyzeFile(file); 145 | const fileScore = this.calculateFileScore(result); 146 | 147 | fileQualities.push({ 148 | path: path.relative(process.cwd(), file), 149 | score: fileScore, 150 | issues: result.diagnostics.length, 151 | metrics: result.metrics, 152 | }); 153 | 154 | if (result.metrics) { 155 | totalLines += result.metrics.linesOfCode; 156 | totalComplexity += result.metrics.complexity; 157 | } 158 | totalIssues += result.diagnostics.length; 159 | } catch { 160 | // Skip files that can't be analyzed 161 | } 162 | } 163 | 164 | // Move to insights generation stage 165 | progressBar.incrementStage(25); 166 | 167 | // Calculate overall metrics 168 | const averageScore = 169 | fileQualities.reduce((sum, f) => sum + f.score, 0) / fileQualities.length; 170 | const codeHealth = Math.max(0, 100 - (totalIssues / files.length) * 10); 171 | const maintainability = this.calculateMaintainability(fileQualities); 172 | const complexity = this.calculateComplexityScore( 173 | totalComplexity, 174 | totalLines, 175 | ); 176 | 177 | progressBar.updateStage("Generating insights", 50); 178 | const testCoverage = await this.estimateTestCoverage(target, files); 179 | const documentation = this.calculateDocumentationScore(target); 180 | const security = await this.calculateSecurityScore(files.slice(0, 10)); 181 | 182 | progressBar.updateStage("Generating insights", 80); 183 | 184 | // Generate AI recommendations 185 | const recommendations = await this.generateRecommendations({ 186 | overallScore: averageScore, 187 | codeHealth, 188 | maintainability, 189 | complexity, 190 | testCoverage, 191 | documentation, 192 | security, 193 | fileCount: files.length, 194 | totalIssues, 195 | }); 196 | 197 | // Finalize report 198 | progressBar.updateStage("Finalizing report", 100); 199 | 200 | const result = { 201 | overallScore: Math.round( 202 | averageScore * 0.3 + 203 | codeHealth * 0.2 + 204 | maintainability * 0.15 + 205 | complexity * 0.15 + 206 | testCoverage * 0.1 + 207 | documentation * 0.05 + 208 | security * 0.05, 209 | ), 210 | codeHealth, 211 | maintainability, 212 | complexity, 213 | testCoverage, 214 | documentation, 215 | security, 216 | recommendations, 217 | files: fileQualities.sort((a, b) => a.score - b.score), 218 | }; 219 | 220 | progressBar.complete( 221 | `Quality analysis completed for ${files.length} files`, 222 | ); 223 | console.log(); // Add spacing after progress bar 224 | 225 | return result; 226 | } 227 | 228 | private calculateFileScore(result: AnalysisResult): number { 229 | let score = 100; 230 | 231 | // Deduct points for issues 232 | score -= result.diagnostics.length * 5; 233 | 234 | // Consider complexity 235 | if (result.metrics) { 236 | const complexity = result.metrics.complexity; 237 | if (complexity > 20) score -= 20; 238 | else if (complexity > 10) score -= 10; 239 | 240 | // Consider maintainability index 241 | if (result.metrics.maintainabilityIndex < 50) score -= 15; 242 | else if (result.metrics.maintainabilityIndex < 70) score -= 5; 243 | } 244 | 245 | return Math.max(0, Math.min(100, score)); 246 | } 247 | 248 | private calculateMaintainability(files: FileQuality[]): number { 249 | const avgComplexity = 250 | files.reduce((sum, f) => sum + (f.metrics?.complexity || 0), 0) / 251 | files.length; 252 | 253 | if (avgComplexity <= 5) return 100; 254 | if (avgComplexity <= 10) return 80; 255 | if (avgComplexity <= 20) return 60; 256 | return 40; 257 | } 258 | 259 | private calculateComplexityScore( 260 | totalComplexity: number, 261 | totalLines: number, 262 | ): number { 263 | if (totalLines === 0) return 100; 264 | const complexityRatio = totalComplexity / totalLines; 265 | 266 | if (complexityRatio <= 0.1) return 100; 267 | if (complexityRatio <= 0.2) return 80; 268 | if (complexityRatio <= 0.3) return 60; 269 | return 40; 270 | } 271 | 272 | private async estimateTestCoverage( 273 | target: string, 274 | files: string[], 275 | ): Promise { 276 | const isDirectory = fs.statSync(target).isDirectory(); 277 | if (!isDirectory) return 50; // Default for single files 278 | 279 | const testFiles = await glob( 280 | path.join(target, "**/*.{test,spec}.{ts,js,tsx,jsx}"), 281 | { 282 | ignore: ["**/node_modules/**"], 283 | }, 284 | ); 285 | 286 | const sourceFiles = files.filter( 287 | (f) => !f.includes("test") && !f.includes("spec"), 288 | ); 289 | const coverage = Math.min( 290 | 100, 291 | (testFiles.length / sourceFiles.length) * 100, 292 | ); 293 | 294 | return Math.round(coverage); 295 | } 296 | 297 | private calculateDocumentationScore(target: string): number { 298 | let score = 0; 299 | 300 | // Check for README 301 | const readmeExists = 302 | fs.existsSync(path.join(target, "README.md")) || 303 | fs.existsSync(path.join(target, "readme.md")); 304 | if (readmeExists) score += 40; 305 | 306 | // Check for package.json with description 307 | const packageJsonPath = path.join(target, "package.json"); 308 | if (fs.existsSync(packageJsonPath)) { 309 | try { 310 | const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); 311 | if (pkg.description && pkg.description.length > 10) score += 30; 312 | } catch { 313 | // Ignore parse errors 314 | } 315 | } 316 | 317 | // Check for docs directory 318 | const docsExists = 319 | fs.existsSync(path.join(target, "docs")) || 320 | fs.existsSync(path.join(target, "documentation")); 321 | if (docsExists) score += 30; 322 | 323 | return Math.min(100, score); 324 | } 325 | 326 | private async calculateSecurityScore(files: string[]): Promise { 327 | let score = 100; 328 | let securityIssues = 0; 329 | 330 | for (const file of files) { 331 | try { 332 | const content = fs.readFileSync(file, "utf-8"); 333 | 334 | // Check for common security issues 335 | if (content.includes("eval(") || content.includes("innerHTML")) 336 | securityIssues++; 337 | if (content.includes("password") && content.includes("=")) 338 | securityIssues++; 339 | if (content.includes("api_key") || content.includes("secret")) 340 | securityIssues++; 341 | if (content.includes("http://") && !content.includes("localhost")) 342 | securityIssues++; 343 | } catch { 344 | // Skip files that can't be read 345 | } 346 | } 347 | 348 | score -= securityIssues * 15; 349 | return Math.max(0, score); 350 | } 351 | 352 | private async generateRecommendations(metrics: any): Promise { 353 | try { 354 | const prompt = ` 355 | Based on the following code quality metrics, provide 5-7 specific, actionable recommendations for improvement: 356 | 357 | Overall Score: ${metrics.overallScore}/100 358 | Code Health: ${metrics.codeHealth}/100 359 | Maintainability: ${metrics.maintainability}/100 360 | Complexity: ${metrics.complexity}/100 361 | Test Coverage: ${metrics.testCoverage}% 362 | Documentation: ${metrics.documentation}/100 363 | Security: ${metrics.security}/100 364 | 365 | File Count: ${metrics.fileCount} 366 | Total Issues: ${metrics.totalIssues} 367 | 368 | Format as a bullet list with specific, actionable items. Focus on the lowest scoring areas. 369 | `; 370 | 371 | const response = await this.aiService.analyzeCode( 372 | prompt, 373 | "quality-analysis", 374 | ); 375 | return response 376 | .split("\n") 377 | .filter( 378 | (line: string) => 379 | line.trim().startsWith("•") || 380 | line.trim().startsWith("-") || 381 | line.trim().startsWith("*"), 382 | ) 383 | .map((line: string) => line.replace(/^[•\-*]\s*/, "")) 384 | .filter((line: string) => line.length > 10); 385 | } catch { 386 | return [ 387 | "Address code issues to improve overall health", 388 | "Reduce complexity in high-complexity functions", 389 | "Add more comprehensive test coverage", 390 | "Improve code documentation", 391 | "Review security practices", 392 | ]; 393 | } 394 | } 395 | 396 | private async displayQualityReport(metrics: QualityMetrics): Promise { 397 | const getScoreColor = (score: number) => { 398 | if (score >= 80) return chalk.green; 399 | if (score >= 60) return chalk.yellow; 400 | return chalk.red; 401 | }; 402 | 403 | const getGrade = (score: number): string => { 404 | if (score >= 90) return "A+"; 405 | if (score >= 80) return "A"; 406 | if (score >= 70) return "B"; 407 | if (score >= 60) return "C"; 408 | if (score >= 50) return "D"; 409 | return "F"; 410 | }; 411 | 412 | // Overall score 413 | const overallColor = getScoreColor(metrics.overallScore); 414 | const grade = getGrade(metrics.overallScore); 415 | 416 | console.log(chalk.cyan("\n🎯 Code Quality Report")); 417 | console.log(chalk.gray("═".repeat(60))); 418 | 419 | console.log( 420 | `\n${overallColor.bold(`Overall Quality Score: ${metrics.overallScore}/100 (${grade})`)}`, 421 | ); 422 | console.log( 423 | overallColor( 424 | `${"█".repeat(Math.floor(metrics.overallScore / 5))}${"░".repeat(20 - Math.floor(metrics.overallScore / 5))}`, 425 | ), 426 | ); 427 | 428 | // Detailed metrics 429 | console.log(chalk.cyan("\n📊 Detailed Metrics:")); 430 | const metricsToShow = [ 431 | ["Code Health", metrics.codeHealth], 432 | ["Maintainability", metrics.maintainability], 433 | ["Complexity", metrics.complexity], 434 | ["Test Coverage", metrics.testCoverage], 435 | ["Documentation", metrics.documentation], 436 | ["Security", metrics.security], 437 | ]; 438 | 439 | metricsToShow.forEach(([name, score]) => { 440 | const nameStr = String(name); 441 | const scoreNum = Number(score); 442 | const color = getScoreColor(scoreNum); 443 | console.log( 444 | ` ${nameStr.padEnd(16)} ${color(`${scoreNum}/100`)} ${"█".repeat(Math.floor(scoreNum / 10))}${"░".repeat(10 - Math.floor(scoreNum / 10))}`, 445 | ); 446 | }); 447 | 448 | // File analysis summary 449 | if (metrics.files.length > 1) { 450 | console.log(chalk.cyan("\n📁 File Analysis:")); 451 | console.log(` Total files analyzed: ${metrics.files.length}`); 452 | 453 | const worstFiles = metrics.files.slice(0, 3); 454 | if (worstFiles.length > 0) { 455 | console.log(chalk.yellow("\n Files needing attention:")); 456 | worstFiles.forEach((file) => { 457 | const color = getScoreColor(file.score); 458 | console.log( 459 | ` ${color(file.path)} - Score: ${color(`${file.score}/100`)} (${file.issues} issues)`, 460 | ); 461 | }); 462 | } 463 | } 464 | 465 | // AI Recommendations 466 | if (metrics.recommendations.length > 0) { 467 | const recommendationsMarkdown = ` 468 | ## 🚀 Improvement Recommendations 469 | 470 | ${metrics.recommendations.map((rec) => `• ${rec}`).join("\n")} 471 | 472 | --- 473 | *Quality score calculated based on code health, maintainability, complexity, test coverage, documentation, and security factors.* 474 | `; 475 | console.log(await renderMarkdown(recommendationsMarkdown)); 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/commands/compare.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chalk from "chalk"; 3 | import type { Change } from "diff"; 4 | import { diffWords, diffLines } from "diff"; 5 | import { AIService } from "../services/ai-service"; 6 | import { LanguageDetector } from "../analyzers/language-detector"; 7 | import { ConfigManager } from "../config/config"; 8 | import { renderMarkdown } from "../utils/render-markdown"; 9 | import { createAIProgress } from "../utils/progress-bar"; 10 | import { 11 | validateFileExists, 12 | handleFileError, 13 | errorHandler, 14 | ConfigurationError, 15 | AnalysisError, 16 | } from "../utils/error-handler"; 17 | 18 | interface ComparisonResult { 19 | similarities: string[]; 20 | differences: string[]; 21 | improvements: string[]; 22 | securityImplications: string[]; 23 | performanceNotes: string[]; 24 | recommendations: string[]; 25 | } 26 | 27 | export class CompareCommand { 28 | private aiService: AIService; 29 | private configManager: ConfigManager; 30 | 31 | constructor() { 32 | this.aiService = AIService.getInstance(); 33 | this.configManager = ConfigManager.getInstance(); 34 | } 35 | 36 | public async handle( 37 | firstTarget: string, 38 | secondTarget: string, 39 | options: any = {}, 40 | ): Promise { 41 | try { 42 | // Validate configuration 43 | if (!(await this.configManager.hasValidConfig())) { 44 | errorHandler.handle( 45 | new ConfigurationError( 46 | "No valid configuration found. Please run: cyrus config init", 47 | ), 48 | "compare-command", 49 | ); 50 | return; 51 | } 52 | 53 | // Validate inputs 54 | const firstContent = await this.getContent(firstTarget); 55 | const secondContent = await this.getContent(secondTarget); 56 | 57 | if (!firstContent || !secondContent) { 58 | return; 59 | } 60 | 61 | const progressBar = createAIProgress("code comparison", { 62 | theme: "modern", 63 | showETA: true, 64 | }); 65 | 66 | progressBar.start(); 67 | 68 | try { 69 | // Step 1: Generate visual diff 70 | progressBar.updateStage("Generating visual diff", 25); 71 | this.displayVisualDiff( 72 | firstContent, 73 | secondContent, 74 | firstTarget, 75 | secondTarget, 76 | options, 77 | ); 78 | 79 | // Step 2: AI-powered analysis 80 | progressBar.updateStage("Analyzing with AI", 50); 81 | const aiAnalysis = await this.generateAIAnalysis( 82 | firstContent, 83 | secondContent, 84 | firstTarget, 85 | secondTarget, 86 | ); 87 | 88 | progressBar.updateStage("Processing results", 75); 89 | 90 | // Step 3: Display AI insights 91 | progressBar.updateStage("Finalizing comparison", 100); 92 | progressBar.complete("Comparison analysis completed"); 93 | console.log(); // Add spacing after progress bar 94 | 95 | await this.displayAIAnalysis(aiAnalysis, options); 96 | 97 | // Additional analysis based on options 98 | if (options.detailed) { 99 | await this.displayDetailedAnalysis( 100 | firstContent, 101 | secondContent, 102 | firstTarget, 103 | secondTarget, 104 | ); 105 | } 106 | 107 | if (options.security) { 108 | await this.displaySecurityComparison( 109 | firstContent, 110 | secondContent, 111 | firstTarget, 112 | secondTarget, 113 | ); 114 | } 115 | } catch (error) { 116 | progressBar.fail("Comparison failed"); 117 | console.log(); // Add spacing after progress bar 118 | errorHandler.handle( 119 | new AnalysisError( 120 | `Comparison failed: ${(error as Error).message}`, 121 | `${firstTarget} vs ${secondTarget}`, 122 | ), 123 | "compare-command", 124 | ); 125 | } 126 | } catch (error) { 127 | errorHandler.handle(error as Error, "compare-command"); 128 | await new Promise((resolve) => setTimeout(resolve, 100)); 129 | } 130 | } 131 | 132 | private async getContent(target: string): Promise { 133 | try { 134 | // Check if it's a file path 135 | if (fs.existsSync(target)) { 136 | validateFileExists(target); 137 | if (!LanguageDetector.isSupported(target)) { 138 | throw new AnalysisError( 139 | `Unsupported file type: ${target}. Supported extensions: ${LanguageDetector.getSupportedExtensions().join(", ")}`, 140 | target, 141 | ); 142 | } 143 | return fs.readFileSync(target, "utf-8"); 144 | } else { 145 | // Treat as raw code content 146 | return target; 147 | } 148 | } catch (error) { 149 | handleFileError(error as Error, target); 150 | return null; 151 | } 152 | } 153 | 154 | private displayVisualDiff( 155 | firstContent: string, 156 | secondContent: string, 157 | firstName: string, 158 | secondName: string, 159 | options: any, 160 | ): void { 161 | console.log( 162 | chalk.cyan(`\n🔍 Code Comparison: ${firstName} vs ${secondName}`), 163 | ); 164 | console.log(chalk.gray("═".repeat(80))); 165 | 166 | if (options.words) { 167 | this.displayWordDiff(firstContent, secondContent); 168 | } else { 169 | this.displayLineDiff(firstContent, secondContent); 170 | } 171 | } 172 | 173 | private displayLineDiff(firstContent: string, secondContent: string): void { 174 | const diff = diffLines(firstContent, secondContent); 175 | let lineNumber1 = 1; 176 | let lineNumber2 = 1; 177 | 178 | console.log(chalk.cyan("\n📋 Line-by-line Comparison:")); 179 | console.log(chalk.gray("─".repeat(60))); 180 | 181 | diff.forEach((part: Change) => { 182 | const lines = part.value.split("\n"); 183 | if (lines[lines.length - 1] === "") { 184 | lines.pop(); // Remove empty last line 185 | } 186 | 187 | lines.forEach((line) => { 188 | if (part.added) { 189 | console.log( 190 | chalk.green(`+ ${lineNumber2.toString().padStart(3)} │ ${line}`), 191 | ); 192 | lineNumber2++; 193 | } else if (part.removed) { 194 | console.log( 195 | chalk.red(`- ${lineNumber1.toString().padStart(3)} │ ${line}`), 196 | ); 197 | lineNumber1++; 198 | } else { 199 | console.log( 200 | chalk.gray(` ${lineNumber1.toString().padStart(3)} │ ${line}`), 201 | ); 202 | lineNumber1++; 203 | lineNumber2++; 204 | } 205 | }); 206 | }); 207 | } 208 | 209 | private displayWordDiff(firstContent: string, secondContent: string): void { 210 | const diff = diffWords(firstContent, secondContent); 211 | 212 | console.log(chalk.cyan("\n📝 Word-by-word Comparison:")); 213 | console.log(chalk.gray("─".repeat(60))); 214 | 215 | diff.forEach((part: Change) => { 216 | if (part.added) { 217 | process.stdout.write(chalk.green.bold(part.value)); 218 | } else if (part.removed) { 219 | process.stdout.write(chalk.red.strikethrough(part.value)); 220 | } else { 221 | process.stdout.write(chalk.white(part.value)); 222 | } 223 | }); 224 | console.log("\n"); 225 | } 226 | 227 | private async generateAIAnalysis( 228 | firstContent: string, 229 | secondContent: string, 230 | firstName: string, 231 | secondName: string, 232 | ): Promise { 233 | const prompt = ` 234 | You are an expert code analyst. Compare these two code snippets and provide detailed insights: 235 | 236 | **First Code (${firstName}):** 237 | \`\`\` 238 | ${firstContent} 239 | \`\`\` 240 | 241 | **Second Code (${secondName}):** 242 | \`\`\` 243 | ${secondContent} 244 | \`\`\` 245 | 246 | Analyze and provide: 247 | 248 | 1. **Key Similarities**: What patterns, structures, or approaches are shared? 249 | 2. **Key Differences**: What are the main differences in implementation, style, or logic? 250 | 3. **Improvements**: Which version is better and why? What improvements does one have over the other? 251 | 4. **Security Implications**: Any security-related differences between the two versions? 252 | 5. **Performance Notes**: Performance implications of the differences? 253 | 6. **Recommendations**: Specific actionable recommendations for improving either version? 254 | 255 | Format your response using markdown for better readability. 256 | Provide specific line references where applicable. 257 | Be detailed but concise, focusing on the most important insights. 258 | `; 259 | 260 | try { 261 | const response = await this.aiService.analyzeCode( 262 | prompt, 263 | "code-comparison", 264 | ); 265 | 266 | // Parse the response into structured format 267 | return this.parseAIResponse(response); 268 | } catch (error) { 269 | throw new Error(`AI analysis failed: ${(error as Error).message}`); 270 | } 271 | } 272 | 273 | private parseAIResponse(response: string): ComparisonResult { 274 | // Simple parsing - in a real implementation, you might want more sophisticated parsing 275 | const similarities = this.extractSection(response, "similarities") || []; 276 | const differences = this.extractSection(response, "differences") || []; 277 | const improvements = this.extractSection(response, "improvements") || []; 278 | const securityImplications = 279 | this.extractSection(response, "security") || []; 280 | const performanceNotes = this.extractSection(response, "performance") || []; 281 | const recommendations = 282 | this.extractSection(response, "recommendations") || []; 283 | 284 | return { 285 | similarities, 286 | differences, 287 | improvements, 288 | securityImplications, 289 | performanceNotes, 290 | recommendations, 291 | }; 292 | } 293 | 294 | private extractSection(text: string, sectionName: string): string[] { 295 | const lines = text.split("\n"); 296 | const items: string[] = []; 297 | let inSection = false; 298 | 299 | for (const line of lines) { 300 | const trimmedLine = line.trim(); 301 | 302 | // Check if we're entering the section 303 | if ( 304 | trimmedLine.toLowerCase().includes(sectionName.toLowerCase()) && 305 | (trimmedLine.startsWith("#") || trimmedLine.startsWith("**")) 306 | ) { 307 | inSection = true; 308 | continue; 309 | } 310 | 311 | // Check if we're leaving the section (another header) 312 | if ( 313 | inSection && 314 | (trimmedLine.startsWith("#") || trimmedLine.startsWith("**")) && 315 | !trimmedLine.toLowerCase().includes(sectionName.toLowerCase()) 316 | ) { 317 | inSection = false; 318 | continue; 319 | } 320 | 321 | // Extract bullet points or numbered items 322 | if ( 323 | inSection && 324 | (trimmedLine.startsWith("-") || 325 | trimmedLine.startsWith("*") || 326 | trimmedLine.startsWith("•") || 327 | /^\d+\./.test(trimmedLine)) 328 | ) { 329 | const cleanItem = trimmedLine 330 | .replace(/^[-*•\d.]\s*/, "") 331 | .replace(/^\*\*(.+?)\*\*:?\s*/, "$1: "); 332 | if (cleanItem.length > 5) { 333 | items.push(cleanItem); 334 | } 335 | } 336 | } 337 | 338 | return items; 339 | } 340 | 341 | private async displayAIAnalysis( 342 | analysis: ComparisonResult, 343 | options: any, 344 | ): Promise { 345 | if (options.json) { 346 | console.log(chalk.cyan("\n🤖 AI Comparison Analysis:")); 347 | console.log(JSON.stringify(analysis, null, 2)); 348 | return; 349 | } 350 | 351 | // Create comprehensive markdown report 352 | const markdownReport = ` 353 | ## 🤖 AI-Powered Comparison Analysis 354 | 355 | ### 🔗 Key Similarities 356 | ${analysis.similarities.map((item) => `• ${item}`).join("\n") || "• No significant similarities identified"} 357 | 358 | ### 🔄 Key Differences 359 | ${analysis.differences.map((item) => `• ${item}`).join("\n") || "• No significant differences identified"} 360 | 361 | ### 🚀 Improvements & Recommendations 362 | ${analysis.improvements.map((item) => `• ${item}`).join("\n") || "• No specific improvements identified"} 363 | 364 | ### 🔒 Security Implications 365 | ${analysis.securityImplications.map((item) => `• ${item}`).join("\n") || "• No security implications identified"} 366 | 367 | ### ⚡ Performance Notes 368 | ${analysis.performanceNotes.map((item) => `• ${item}`).join("\n") || "• No performance implications identified"} 369 | 370 | ### 💡 Expert Recommendations 371 | ${analysis.recommendations.map((item) => `• ${item}`).join("\n") || "• No specific recommendations"} 372 | 373 | --- 374 | *Analysis powered by AI - Review suggestions carefully before implementation* 375 | `; 376 | 377 | console.log(await renderMarkdown(markdownReport)); 378 | } 379 | 380 | private async displayDetailedAnalysis( 381 | firstContent: string, 382 | secondContent: string, 383 | _firstName: string, 384 | _secondName: string, 385 | ): Promise { 386 | console.log(chalk.cyan("\n📊 Detailed Code Metrics Comparison:")); 387 | console.log(chalk.gray("─".repeat(60))); 388 | 389 | // Basic metrics comparison 390 | const firstLines = firstContent.split("\n").length; 391 | const secondLines = secondContent.split("\n").length; 392 | const firstChars = firstContent.length; 393 | const secondChars = secondContent.length; 394 | 395 | console.log(chalk.white("Code Size Comparison:")); 396 | console.log( 397 | ` ${"First".padEnd(20)} │ ${firstLines.toString().padStart(6)} lines │ ${firstChars.toString().padStart(8)} chars`, 398 | ); 399 | console.log( 400 | ` ${"Second".padEnd(20)} │ ${secondLines.toString().padStart(6)} lines │ ${secondChars.toString().padStart(8)} chars`, 401 | ); 402 | 403 | const lineDiff = secondLines - firstLines; 404 | const charDiff = secondChars - firstChars; 405 | const diffColor = lineDiff > 0 ? chalk.red : chalk.green; 406 | 407 | console.log( 408 | ` ${"Difference".padEnd(20)} │ ${diffColor(`${lineDiff > 0 ? "+" : ""}${lineDiff}`).padStart(6)} lines │ ${diffColor(`${charDiff > 0 ? "+" : ""}${charDiff}`).padStart(8)} chars`, 409 | ); 410 | 411 | // Complexity indicators 412 | const firstComplexity = this.calculateSimpleComplexity(firstContent); 413 | const secondComplexity = this.calculateSimpleComplexity(secondContent); 414 | 415 | console.log(chalk.white("\nComplexity Indicators:")); 416 | console.log( 417 | ` ${"First".padEnd(20)} │ ${firstComplexity.functions} functions │ ${firstComplexity.conditions} conditions │ ${firstComplexity.loops} loops`, 418 | ); 419 | console.log( 420 | ` ${"Second".padEnd(20)} │ ${secondComplexity.functions} functions │ ${secondComplexity.conditions} conditions │ ${secondComplexity.loops} loops`, 421 | ); 422 | } 423 | 424 | private calculateSimpleComplexity(content: string): { 425 | functions: number; 426 | conditions: number; 427 | loops: number; 428 | } { 429 | const functions = (content.match(/function\s+\w+|=>\s*\{|def\s+\w+/g) || []) 430 | .length; 431 | const conditions = (content.match(/if\s*\(|switch\s*\(|case\s+/g) || []) 432 | .length; 433 | const loops = (content.match(/for\s*\(|while\s*\(|forEach/g) || []).length; 434 | 435 | return { functions, conditions, loops }; 436 | } 437 | 438 | private async displaySecurityComparison( 439 | firstContent: string, 440 | secondContent: string, 441 | _firstName: string, 442 | _secondName: string, 443 | ): Promise { 444 | console.log(chalk.cyan("\n🔒 Security Comparison Analysis:")); 445 | console.log(chalk.gray("─".repeat(60))); 446 | 447 | const firstSecurity = this.analyzeSecurityPatterns(firstContent); 448 | const secondSecurity = this.analyzeSecurityPatterns(secondContent); 449 | 450 | console.log(chalk.white("Security Pattern Detection:")); 451 | 452 | const patterns = [ 453 | "hardcodedSecrets", 454 | "sqlInjection", 455 | "xssVulnerable", 456 | "insecureHttp", 457 | "evalUsage", 458 | ]; 459 | 460 | patterns.forEach((pattern) => { 461 | const first = firstSecurity[pattern] || 0; 462 | const second = secondSecurity[pattern] || 0; 463 | 464 | if (first > 0 || second > 0) { 465 | const patternName = pattern.replace(/([A-Z])/g, " $1").toLowerCase(); 466 | console.log( 467 | ` ${patternName.padEnd(20)} │ ${first.toString().padStart(3)} │ ${second.toString().padStart(3)}`, 468 | ); 469 | } 470 | }); 471 | 472 | if ( 473 | Object.values(firstSecurity).every((v) => v === 0) && 474 | Object.values(secondSecurity).every((v) => v === 0) 475 | ) { 476 | console.log(chalk.green(" ✅ No obvious security patterns detected")); 477 | } 478 | } 479 | 480 | private analyzeSecurityPatterns(content: string): Record { 481 | return { 482 | hardcodedSecrets: ( 483 | content.match( 484 | /password\s*=\s*["'][^"']+["']|api_key\s*=\s*["'][^"']+["']|secret\s*=\s*["'][^"']+["']/gi, 485 | ) || [] 486 | ).length, 487 | sqlInjection: (content.match(/\+.*SELECT|query\s*\+|sql\s*\+/gi) || []) 488 | .length, 489 | xssVulnerable: (content.match(/innerHTML\s*=|eval\s*\(/gi) || []).length, 490 | insecureHttp: (content.match(/http:\/\/(?!localhost)/gi) || []).length, 491 | evalUsage: (content.match(/eval\s*\(/gi) || []).length, 492 | }; 493 | } 494 | } 495 | --------------------------------------------------------------------------------