├── .babelrc.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-commit.bat ├── .pre-commit-config.yaml ├── .prettierignore ├── CHANGELOG.md ├── LICENCE ├── Makefile ├── README.md ├── assets ├── ai_code_fusion_1.jpg ├── ai_code_fusion_2.jpg ├── ai_code_fusion_3.jpg └── ai_code_fusion_4.jpg ├── babel.config.js ├── docs ├── CONFIGURATION.md ├── DEVELOPMENT.md └── SONARQUBE.md ├── jest.config.js ├── make.bat ├── package.json ├── postcss.config.js ├── prettier.config.js ├── scripts ├── README.md ├── clean-dev-assets.js ├── ensure-build-dirs.js ├── generate-icons.js ├── index.js ├── lib │ ├── build.js │ ├── dev.js │ ├── release.js │ └── utils.js ├── prepare-build.js ├── prepare-release.js └── sonar-scan.js ├── sonar-project.properties ├── src ├── __tests__ │ ├── ConfigTab.test.jsx │ └── token-counter.test.js ├── assets │ ├── README.md │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icons │ │ ├── mac │ │ │ └── icon.icns │ │ ├── png │ │ │ ├── 1024x1024.png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ └── 64x64.png │ │ └── win │ │ │ └── icon.ico │ └── logo.png ├── main │ ├── index.js │ └── preload.js ├── renderer │ ├── components │ │ ├── App.jsx │ │ ├── ConfigTab.jsx │ │ ├── DarkModeToggle.jsx │ │ ├── FileTree.jsx │ │ ├── ProcessedTab.jsx │ │ ├── SourceTab.jsx │ │ └── TabBar.jsx │ ├── context │ │ └── DarkModeContext.jsx │ ├── icon.png │ ├── index.html │ ├── index.js.LICENSE.txt │ └── styles.css └── utils │ ├── config-manager.js │ ├── config.default.yaml │ ├── content-processor.js │ ├── file-analyzer.js │ ├── filter-utils.js │ ├── fnmatch.js │ ├── formatters │ └── list-formatter.js │ ├── gitignore-parser.js │ └── token-counter.js ├── tailwind.config.js ├── tests ├── .eslintrc.js ├── fixtures │ ├── configs │ │ ├── default.yaml │ │ └── minimal.yaml │ ├── files │ │ ├── .gitignore │ │ ├── sample.js │ │ └── sample.md │ └── projects │ │ ├── mock-gitignore.txt │ │ └── mock-project-structure.txt ├── integration │ ├── main-process │ │ └── handlers.test.js │ └── pattern-merging.test.js ├── mocks │ └── yaml-mock.js ├── setup.js └── unit │ ├── binary-detection.test.js │ ├── components │ ├── app.test.jsx │ ├── config-tab.test.jsx │ └── file-tree.test.jsx │ ├── file-analyzer.test.js │ ├── gitignore-parser.test.js │ └── utils │ ├── config-manager.test.js │ ├── content-processor.test.js │ ├── filter-utils.test.js │ ├── fnmatch.test.js │ └── token-counter.test.js └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | plugins: [], 4 | }; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore generated files 2 | src/renderer/bundle.js 3 | src/renderer/bundle.js.map 4 | src/renderer/index.js 5 | src/renderer/index.js.map 6 | src/renderer/bundle.js.LICENSE.txt 7 | src/renderer/index.js.LICENSE.txt 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | jest: true, 8 | }, 9 | extends: ['eslint:recommended', 'plugin:react/recommended'], 10 | parserOptions: { 11 | ecmaVersion: 2020, 12 | sourceType: 'module', 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | settings: { 18 | react: { 19 | version: 'detect', 20 | }, 21 | }, 22 | ignorePatterns: [ 23 | 'node_modules/**', 24 | 'dist/**', 25 | 'build/**', 26 | 'coverage/**', 27 | 'scripts/**', 28 | 'src/renderer/bundle.js', 29 | 'src/renderer/bundle.js.map', 30 | 'src/renderer/bundle.js.LICENSE.txt', 31 | 'src/renderer/index.js', 32 | 'src/renderer/index.js.map', 33 | 'src/renderer/output.css', 34 | ], 35 | rules: { 36 | 'react/react-in-jsx-scope': 'off', 37 | 'react/prop-types': 'warn', 38 | 'no-unused-vars': 'warn', 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings 2 | * text=auto 3 | 4 | # Shell scripts should use LF 5 | *.sh text eol=lf 6 | .husky/pre-commit text eol=lf 7 | 8 | # Batch and PowerShell scripts should use CRLF 9 | *.bat text eol=crlf 10 | *.ps1 text eol=crlf 11 | 12 | # Documentation and config 13 | *.md text 14 | *.json text 15 | *.yml text 16 | *.yaml text 17 | 18 | # JavaScript and web files 19 | *.js text 20 | *.jsx text 21 | *.ts text 22 | *.tsx text 23 | *.css text 24 | *.html text 25 | 26 | # Exclude binary files from line ending conversion 27 | *.png binary 28 | *.jpg binary 29 | *.jpeg binary 30 | *.gif binary 31 | *.ico binary 32 | *.ttf binary 33 | *.woff binary 34 | *.woff2 binary 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | packages: read 11 | 12 | jobs: 13 | build-windows: 14 | runs-on: windows-latest 15 | steps: 16 | - name: Check out Git repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Prepare build 28 | run: node scripts/prepare-build.js windows 29 | 30 | - name: Build CSS 31 | run: npm run build:css 32 | 33 | - name: Build Webpack 34 | run: npm run build:webpack 35 | 36 | - name: Build Windows Package 37 | run: npm run build:win -- --publish=never 38 | env: 39 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Upload Windows Artifacts 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: windows-artifacts 45 | path: dist/*.exe 46 | retention-days: 5 47 | 48 | build-linux: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Check out Git repository 52 | uses: actions/checkout@v4 53 | 54 | - name: Install Node.js 55 | uses: actions/setup-node@v4 56 | with: 57 | node-version: 18 58 | 59 | - name: Install dependencies 60 | run: npm install 61 | 62 | - name: Install required system packages 63 | run: | 64 | sudo apt-get update 65 | sudo apt-get install -y libgtk-3-dev libnotify-dev libnss3 libxss1 libgbm-dev 66 | 67 | # Skip prepare-build for Linux as it seems to be causing issues 68 | # Instead, directly modify package.json for Linux build 69 | 70 | - name: Configure Linux Build 71 | run: | 72 | # Create a minimal electron-builder config for Linux 73 | node -e " 74 | const fs = require('fs'); 75 | const path = require('path'); 76 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 77 | const packageJson = require(packageJsonPath); 78 | 79 | // Remove any existing Linux configuration 80 | if (packageJson.build && packageJson.build.linux) { 81 | delete packageJson.build.linux; 82 | } 83 | 84 | // Set minimal Linux config without any icon reference 85 | if (!packageJson.build) packageJson.build = {}; 86 | packageJson.build.linux = { 87 | target: ['AppImage'], 88 | category: 'Utility' 89 | }; 90 | 91 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); 92 | " 93 | 94 | - name: Build CSS 95 | run: npm run build:css 96 | 97 | - name: Build Webpack 98 | run: npm run build:webpack 99 | 100 | - name: Build Linux AppImage 101 | run: npm run build -- --linux AppImage --publish=never 102 | env: 103 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | 105 | - name: Upload Linux Artifacts 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: linux-artifacts 109 | path: dist/*.AppImage 110 | retention-days: 5 111 | 112 | build-macos: 113 | runs-on: macos-latest 114 | steps: 115 | - name: Check out Git repository 116 | uses: actions/checkout@v4 117 | 118 | - name: Install Node.js 119 | uses: actions/setup-node@v4 120 | with: 121 | node-version: 18 122 | 123 | - name: Install dependencies 124 | run: npm install 125 | 126 | - name: Prepare build 127 | run: node scripts/prepare-build.js mac 128 | 129 | - name: Build CSS 130 | run: npm run build:css 131 | 132 | - name: Build Webpack 133 | run: npm run build:webpack 134 | 135 | - name: Build macOS Universal Binary 136 | run: npm run build:mac-universal -- --publish=never 137 | env: 138 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 139 | 140 | - name: Upload macOS Artifacts 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: macos-artifacts 144 | path: | 145 | dist/*.dmg 146 | dist/*.zip 147 | retention-days: 5 148 | 149 | create-release: 150 | needs: [build-windows, build-linux, build-macos] 151 | runs-on: ubuntu-latest 152 | steps: 153 | - name: Check out Git repository 154 | uses: actions/checkout@v4 155 | with: 156 | fetch-depth: 0 157 | 158 | - name: Get version from tag 159 | id: get_version 160 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 161 | 162 | - name: Get Changelog Entry 163 | id: changelog_reader 164 | uses: mindsers/changelog-reader-action@v2 165 | with: 166 | validation_level: warn 167 | path: ./CHANGELOG.md 168 | version: ${{ steps.get_version.outputs.VERSION }} 169 | continue-on-error: true 170 | 171 | - name: Download Windows artifacts 172 | uses: actions/download-artifact@v4 173 | with: 174 | name: windows-artifacts 175 | path: artifacts 176 | 177 | - name: Download Linux artifacts 178 | uses: actions/download-artifact@v4 179 | with: 180 | name: linux-artifacts 181 | path: artifacts 182 | 183 | - name: Download macOS artifacts 184 | uses: actions/download-artifact@v4 185 | with: 186 | name: macos-artifacts 187 | path: artifacts 188 | 189 | - name: Create Release 190 | uses: softprops/action-gh-release@v1 191 | with: 192 | name: Release ${{ steps.get_version.outputs.VERSION }} 193 | body: ${{ steps.changelog_reader.outputs.changes || 'No changelog provided' }} 194 | draft: true 195 | files: | 196 | artifacts/* 197 | env: 198 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 199 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | /.yarn 6 | 7 | # Testing 8 | /coverage 9 | /.nyc_output 10 | 11 | # Production 12 | /dist 13 | /out 14 | /bin 15 | /build 16 | 17 | # Build outputs 18 | /src/renderer/index.js 19 | /src/renderer/index.js.map 20 | /src/renderer/output.css 21 | /.tmp* 22 | /temp 23 | 24 | # Package files 25 | package-lock.json 26 | yarn.lock 27 | 28 | # Logs 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | lerna-debug.log* 35 | 36 | # Editor directories and files 37 | .idea 38 | .vscode 39 | *.suo 40 | *.ntvs* 41 | *.njsproj 42 | *.sln 43 | *.sw? 44 | .project 45 | .classpath 46 | .settings/ 47 | *.sublime-* 48 | 49 | # Cache files 50 | .eslintcache 51 | .stylelintcache 52 | .parcel-cache 53 | .cache 54 | .npm 55 | .next 56 | 57 | # OS files 58 | .DS_Store 59 | Thumbs.db 60 | ehthumbs.db 61 | Desktop.ini 62 | $RECYCLE.BIN/ 63 | ._* 64 | 65 | # Environment variables and local config 66 | .env 67 | .env.local 68 | .env.* 69 | .scannerwork/ 70 | 71 | # Project-specific 72 | start/ 73 | start/* 74 | /signed/ 75 | src/renderer/bundle.js.LICENSE.txt 76 | src/renderer/bundle.js.map 77 | src/renderer/bundle.js 78 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # Run lint-staged (works on both Windows with Git Bash and Linux) 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.husky/pre-commit.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | npx lint-staged 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-json 9 | - id: mixed-line-ending 10 | args: ['--fix=lf'] 11 | description: Forces to replace line ending by the UNIX 'lf' character. 12 | 13 | - repo: https://github.com/pre-commit/mirrors-eslint 14 | rev: v8.56.0 15 | hooks: 16 | - id: eslint 17 | files: \.(js|jsx)$ 18 | types: [file] 19 | additional_dependencies: 20 | - eslint@8.56.0 21 | - eslint-plugin-react@7.33.2 22 | - eslint-plugin-react-hooks@4.6.0 23 | - eslint-plugin-tailwindcss@3.13.0 24 | - eslint-plugin-prettier@5.0.0 25 | - eslint-config-prettier@9.0.0 26 | - prettier@3.1.0 27 | 28 | - repo: https://github.com/pre-commit/mirrors-prettier 29 | rev: v3.1.0 30 | hooks: 31 | - id: prettier 32 | types_or: [javascript, jsx, json, css, html] 33 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | coverage 5 | .git 6 | .github 7 | .husky 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [v0.2.0] - 2025-03-31 6 | 7 | ### Added 8 | 9 | - Dark Mode support 10 | - Live token count updates during file selection 11 | - Separated include/exclude configuration boxes for better organization 12 | - Auto detect and exclude binary files 13 | - Use .gitignore to exclude files/folders 14 | 15 | ### Improved 16 | 17 | - Enhanced UX/UI with better spacing and visual hierarchy 18 | - Faster UI rendering and response times 19 | - Simplified text entry for file patterns (vs. YAML format) 20 | 21 | ### Fixed 22 | 23 | - Multiple bug fixes in file selection and processing 24 | - Added robust testing for file selection edge cases 25 | 26 | ## [v0.1.0] - 2025-03-11 27 | 28 | Initial release of the AI Code Fusion application with the following features: 29 | 30 | - Visual interface for selecting and analyzing source code directories 31 | - Custom file inclusion/exclusion patterns 32 | - Configurable token counting 33 | - Code content processing for AI systems 34 | - Multi-platform support (Windows, macOS, Linux) 35 | - Directory tree visualization 36 | - File analyzer with token estimation 37 | - Token counter with multiple models support 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------- 2 | # Repository AI Code Fusion - Makefile for Linux/Mac 3 | # ------------------------------------------------------------- 4 | 5 | # Make these targets phony (they don't create files with these names) 6 | .PHONY: all setup dev clean clean-all build build-win build-linux \ 7 | build-mac build-mac-arm build-mac-universal \ 8 | test css css-watch lint format validate setup-hooks sonar icons sample-logo release 9 | 10 | # Set executable permissions for scripts 11 | setup-scripts: 12 | @chmod +x scripts/index.js scripts/lib/*.js 13 | 14 | # Use Node.js to run commands through our unified script 15 | all: setup-scripts 16 | @node scripts/index.js 17 | 18 | setup: setup-scripts 19 | @node scripts/index.js setup 20 | 21 | dev: setup-scripts 22 | @node scripts/index.js dev 23 | 24 | clean: setup-scripts 25 | @node scripts/index.js clean 26 | 27 | clean-all: setup-scripts 28 | @node scripts/index.js clean-all 29 | 30 | build: setup-scripts 31 | @node scripts/index.js build 32 | 33 | build-win: setup-scripts 34 | @node scripts/index.js build-win 35 | 36 | build-linux: setup-scripts 37 | @node scripts/index.js build-linux 38 | 39 | build-mac: setup-scripts 40 | @node scripts/index.js build-mac 41 | 42 | build-mac-arm: setup-scripts 43 | @node scripts/index.js build-mac-arm 44 | 45 | build-mac-universal: setup-scripts 46 | @node scripts/index.js build-mac-universal 47 | 48 | test: setup-scripts 49 | @node scripts/index.js test 50 | 51 | css: setup-scripts 52 | @node scripts/index.js css 53 | 54 | css-watch: setup-scripts 55 | @node scripts/index.js css-watch 56 | 57 | lint: setup-scripts 58 | @node scripts/index.js lint 59 | 60 | format: setup-scripts 61 | @node scripts/index.js format 62 | 63 | validate: setup-scripts 64 | @node scripts/index.js validate 65 | 66 | setup-hooks: setup-scripts 67 | @node scripts/index.js hooks 68 | 69 | sonar: setup-scripts 70 | @node scripts/index.js sonar 71 | 72 | release: setup-scripts 73 | @node scripts/index.js release $(VERSION) 74 | 75 | # Support for version argument 76 | %: 77 | @: 78 | 79 | icons: setup-scripts 80 | @node scripts/index.js icons 81 | 82 | sample-logo: setup-scripts 83 | @node scripts/index.js run create-sample-logo 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Code Fusion 2 | 3 | ## Features 4 | 5 | A desktop application for preparing and optimizing code repositories for AI processing and analysis. It helps you select specific files, count tokens, and process source code for AI systems. 6 | 7 | - Visual directory explorer for selecting code files 8 | - Advanced file filtering with customizable patterns 9 | - Accurate token counting for various AI models 10 | - Code content processing with statistics 11 | - Cross-platform support (Windows, macOS, Linux) 12 | 13 | ## Installation 14 | 15 | ### Download 16 | 17 | Download the latest version for your platform from the [Releases page](https://github.com/codingworkflow/ai-code-fusion/releases). 18 | 19 | ### Windows 20 | 21 | 1. Download the `.exe` installer 22 | 2. Run the installer and follow the instructions 23 | 3. Launch from the Start Menu or desktop shortcut 24 | 25 | ### macOS 26 | 27 | 1. Download the `.dmg` file 28 | 2. Open the DMG and drag the application to your Applications folder 29 | 3. Launch from Applications 30 | 31 | ### Linux 32 | 33 | 1. Download the `.AppImage` file 34 | 2. Make it executable: `chmod +x AI.Code.Prep-*.AppImage` 35 | 3. Run it: `./AI.Code.Prep-*.AppImage` 36 | 37 | ## Usage Guide 38 | 39 | ### 1. Start and Filters 40 | 41 | The application now features both Dark and Light modes for improved user experience. 42 | ![Start Panel Dark Mode](assets/ai_code_fusion_1.jpg) 43 | 44 | ![Start Panel Light Mode](assets/ai_code_fusion_2.jpg) 45 | 46 | Extended file filtering options: 47 | 48 | - Exclude specific file types and patterns (using glob patterns) to remove build folders, venv, node_modules, .git from tree view and file selection 49 | - Automatically exclude files based on .gitignore files in your repository 50 | - Reduce selection to only the file extensions you specify 51 | - Display token count in real-time during selection (can be disabled for very large repositories) 52 | - Include file tree in output (recommended for better context in AI models) 53 | 54 | ### 2. File Selection 55 | 56 | Select specific files and directories to analyze and process. 57 | 58 | ![Analysis Panel](assets/ai_code_fusion_3.jpg) 59 | 60 | - Browse and select your root project directory 61 | - Use the tree view to select specific files or folders 62 | - See file counts and token sizes in real-time (when token display is enabled) 63 | 64 | ### 3. Final Processing 65 | 66 | Generate the processed output ready for use with AI systems. 67 | 68 | ![Processing Panel](assets/ai_code_fusion_4.jpg) 69 | 70 | - View the final processed content ready for AI systems 71 | - Copy content directly to clipboard for immediate use 72 | - Export to file for later reference 73 | - Review files by token count to help identify large files you may want to exclude 74 | 75 | ## Building from Source 76 | 77 | ### Prerequisites 78 | 79 | - Node.js (v14 or later) 80 | - npm 81 | - Git 82 | 83 | ### Setup 84 | 85 | ```bash 86 | # Clone the repository 87 | git clone https://github.com/codingworkflow/ai-code-fusion 88 | cd ai-code-fusion 89 | 90 | # Install dependencies 91 | make setup 92 | # or 93 | npm install 94 | ``` 95 | 96 | ### Development 97 | 98 | ```bash 99 | # Start the development server 100 | make dev 101 | # or 102 | npm run dev 103 | ``` 104 | 105 | #### Troubleshooting Development 106 | 107 | If you encounter issues with the development server: 108 | 109 | 1. For Windows users: The `make.bat` file has special handling for the `dev` command that properly sets environment variables 110 | 2. If you're still having issues: 111 | - Ensure all dependencies are installed: `npm install` 112 | - Try rebuilding: `npm run rebuild` 113 | - Close any running instances of the app 114 | - Restart your terminal/command prompt 115 | - As a last resort, try direct electron launch: `npm run dev:direct` 116 | 117 | ### Building 118 | 119 | ```bash 120 | # Build for current platform 121 | make build 122 | 123 | # Build for specific platforms 124 | make build-win # Windows 125 | make build-linux # Linux 126 | make build-mac # macOS (Intel) 127 | make build-mac-arm # macOS (Apple Silicon) 128 | make build-mac-universal # macOS (Universal) 129 | ``` 130 | 131 | ## License 132 | 133 | GPL 3.0 134 | -------------------------------------------------------------------------------- /assets/ai_code_fusion_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingworkflow/ai-code-fusion/e966c8085007299f239bf30597feea6e60672476/assets/ai_code_fusion_1.jpg -------------------------------------------------------------------------------- /assets/ai_code_fusion_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingworkflow/ai-code-fusion/e966c8085007299f239bf30597feea6e60672476/assets/ai_code_fusion_2.jpg -------------------------------------------------------------------------------- /assets/ai_code_fusion_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingworkflow/ai-code-fusion/e966c8085007299f239bf30597feea6e60672476/assets/ai_code_fusion_3.jpg -------------------------------------------------------------------------------- /assets/ai_code_fusion_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingworkflow/ai-code-fusion/e966c8085007299f239bf30597feea6e60672476/assets/ai_code_fusion_4.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }]], 3 | plugins: ['@babel/plugin-transform-modules-commonjs'], 4 | }; 5 | -------------------------------------------------------------------------------- /docs/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration Guide 2 | 3 | AI Code Fusion uses YAML configuration for file filtering. This document explains the available configuration options and best practices. 4 | 5 | ## Configuration Format 6 | 7 | The application uses YAML format for its configuration. Below is an example showing common configuration patterns: 8 | 9 | ### File extensions to include (with dot) 10 | 11 | ``` 12 | .py 13 | .ts 14 | .js 15 | .md 16 | .ini 17 | .yaml 18 | .yml 19 | .kt 20 | .go 21 | .scm 22 | .php 23 | 24 | # Patterns to exclude (using fnmatch syntax) 25 | # Version Control 26 | '**/.git/**' 27 | '**/.svn/**' 28 | '**/.hg/**' 29 | '**/vocab.txt' 30 | '**.onnx' 31 | '**/test*.py' 32 | 33 | # Dependencies 34 | '**/node_modules/**' 35 | '**/venv/**' 36 | '**/env/**' 37 | '**/.venv/**' 38 | '**/.github/**' 39 | '**/vendor/**' 40 | '**/website/**' 41 | 42 | # Build outputs 43 | '**/test/**' 44 | '**/dist/**' 45 | '**/build/**' 46 | '**/__pycache__/**' 47 | '**/*.pyc' 48 | 49 | # Config files 50 | '**/.DS_Store' 51 | '**/.env' 52 | '**/package-lock.json' 53 | '**/yarn.lock' 54 | '**/.prettierrc' 55 | '**/.prettierignore' 56 | '**/.gitignore' 57 | '**/.gitattributes' 58 | '**/.npmrc' 59 | 60 | # Documentation 61 | '**/LICENSE*' 62 | '**/LICENSE.*' 63 | '**/COPYING' 64 | '**/CODE_OF**' 65 | '**/CONTRIBUTING**' 66 | 67 | # Test files 68 | '**/tests/**' 69 | '**/test/**' 70 | '**/__tests__/**' 71 | ``` 72 | 73 | ## Configuration Options 74 | 75 | ### Include Extensions 76 | 77 | The `include_extensions` section specifies which file extensions should be processed. Only files with these extensions will be considered for processing. 78 | 79 | Example: 80 | 81 | ``` 82 | .py # Include Python files 83 | .js # Include JavaScript files 84 | .md # Include Markdown files 85 | ``` 86 | 87 | ### Exclude Patterns 88 | 89 | The `exclude_patterns` section defines patterns for files and directories that should be excluded from processing, even if they have a matching extension from the include list. 90 | 91 | Patterns use the fnmatch syntax: 92 | 93 | - `*` matches any sequence of characters 94 | - `**` matches any sequence of directories 95 | - `?` matches a single character 96 | 97 | Example: 98 | 99 | ``` 100 | '**/node_modules/**' # Exclude all node_modules directories 101 | '**/.git/**' # Exclude Git directories 102 | '**/test*.py' # Exclude Python files that start with 'test' 103 | ``` 104 | 105 | ## Best Practices 106 | 107 | 1. **Start with a broad configuration** and refine as needed 108 | 2. **Group related patterns** with comments for better organization 109 | 3. **Be specific with extensions** to avoid processing unnecessary files 110 | 4. **Use the file preview** to verify your configuration is working as expected 111 | 5. **Check token counts** to ensure you stay within your model's context limits 112 | 113 | ## Common Configurations 114 | 115 | Here are some typical configurations for different project types: 116 | 117 | ### For JavaScript/TypeScript Projects 118 | 119 | #### include_extensions: 120 | 121 | ``` 122 | .js 123 | .jsx 124 | .ts 125 | .tsx 126 | .md 127 | .json 128 | ``` 129 | 130 | #### #### exclude_patterns: 131 | 132 | ``` 133 | '**/node_modules/**' 134 | '**/dist/**' 135 | '**/build/**' 136 | '**/.cache/**' 137 | '**/coverage/**' 138 | '**/*.test.*' 139 | '**/*.spec.*' 140 | ``` 141 | 142 | ### For Python Projects 143 | 144 | #### include_extensions: 145 | 146 | ``` 147 | .py 148 | .md 149 | .yml 150 | .yaml 151 | .ini 152 | ``` 153 | 154 | #### #### exclude_patterns: 155 | 156 | ``` 157 | '**/venv/**' 158 | '**/.venv/**' 159 | '**/__pycache__/**' 160 | '**/*.pyc' 161 | '**/tests/**' 162 | '**/.pytest_cache/**' 163 | ``` 164 | 165 | ## Troubleshooting 166 | 167 | If you encounter issues with your configuration: 168 | 169 | - **No files are processed**: Verify that your include extensions match your project's file types 170 | - **Too many files are processed**: Add more specific exclude patterns to filter unwanted files 171 | - **Important files are excluded**: Check for conflicting exclude patterns that might be too broad 172 | - **Token count is too high**: Add more exclude patterns to reduce the number of processed files 173 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | This document provides detailed information for developers working on the AI Code Fusion project. 4 | 5 | ## Development Environment Setup 6 | 7 | ### Prerequisites 8 | 9 | - Node.js (v14 or later) 10 | - npm 11 | - Git 12 | 13 | ### Platform-Specific Build Instructions 14 | 15 | #### Windows 16 | 17 | Use the included `make.bat` file for all build commands: 18 | 19 | ```cmd 20 | make 21 | ``` 22 | 23 | #### Linux/macOS 24 | 25 | Use the included `Makefile`: 26 | 27 | ```bash 28 | make 29 | ``` 30 | 31 | ### Common Make Commands 32 | 33 | ```bash 34 | # Install dependencies and set up the project 35 | make setup 36 | 37 | # Start development server 38 | make dev 39 | 40 | # Build for current platform 41 | make build 42 | 43 | # Build for Windows 44 | make build-win 45 | 46 | # Build for Linux 47 | make build-linux 48 | 49 | # Run tests 50 | make test 51 | 52 | # Run tests in watch mode 53 | make test-watch 54 | 55 | # Run linter 56 | make lint 57 | 58 | # Format code 59 | make format 60 | 61 | # Run all code quality checks 62 | make validate 63 | ``` 64 | 65 | ### Manual Setup 66 | 67 | If you prefer not to use the make commands: 68 | 69 | ```bash 70 | # Install dependencies 71 | npm install 72 | 73 | # Build CSS 74 | npm run build:css 75 | 76 | # Start development server 77 | npm run dev 78 | ``` 79 | 80 | ## Troubleshooting 81 | 82 | If you encounter issues with the development server: 83 | 84 | 1. Clean the build outputs and reinstall dependencies: 85 | 86 | ```bash 87 | make clean 88 | make fix-deps 89 | ``` 90 | 91 | 2. Make sure the CSS is built before starting the dev server: 92 | 93 | ```bash 94 | npm run build:css 95 | ``` 96 | 97 | 3. Start the development server: 98 | 99 | ```bash 100 | npm run dev 101 | ``` 102 | 103 | If you encounter any issues with tiktoken or minimatch, you may need to install them separately: 104 | 105 | ```bash 106 | npm install tiktoken minimatch 107 | ``` 108 | 109 | ## Testing 110 | 111 | Tests are located in the `src/__tests__` directory. To add new tests: 112 | 113 | 1. Create a file with the `.test.js` or `.test.jsx` extension in the `src/__tests__` directory 114 | 2. Use Jest and React Testing Library for component tests 115 | 3. Run tests with `make test` or `npm run test` 116 | 117 | ```bash 118 | # Run a specific test file 119 | make test-file FILE=src/__tests__/token-counter.test.js 120 | ``` 121 | 122 | ## Release Process 123 | 124 | For project maintainers, follow these steps to create a new release: 125 | 126 | 1. Ensure all changes are committed to the main branch 127 | 2. Run the release preparation script using either method: 128 | 129 | ```bash 130 | # Using the scripts/index.js entry point (recommended) 131 | node scripts/index.js release 132 | 133 | # OR using the direct script 134 | node scripts/prepare-release.js 135 | ``` 136 | 137 | Where `` can be: 138 | 139 | - A specific version number (e.g., `1.0.0`) 140 | - `patch` - increment the patch version 141 | - `minor` - increment the minor version 142 | - `major` - increment the major version 143 | 144 | 3. Enter the changelog entries when prompted 145 | 4. Confirm git tag creation when prompted 146 | 5. Push the changes and tag to GitHub: 147 | 148 | ```bash 149 | git push && git push origin v 150 | ``` 151 | 152 | 6. The GitHub Actions workflow will automatically: 153 | - Build the application for Windows, macOS, and Linux 154 | - Create a GitHub Release 155 | - Upload the builds as release assets 156 | 7. Go to the GitHub releases page to review the draft release and publish it 157 | 158 | ## Project Structure 159 | 160 | - `/src/main` - Electron main process code 161 | - `/src/renderer` - React application for the renderer process 162 | - `/src/utils` - Shared utilities 163 | - `/src/assets` - Static assets 164 | -------------------------------------------------------------------------------- /docs/SONARQUBE.md: -------------------------------------------------------------------------------- 1 | # SonarQube Integration 2 | 3 | This document explains how to use the SonarQube integration with the AI Code Fusion. 4 | 5 | ## Prerequisites 6 | 7 | 1. Access to a SonarQube server (self-hosted or SonarCloud). 8 | 2. Generate an authentication token from your SonarQube instance. 9 | 10 | Note: The SonarQube Scanner is now included as a dependency in the project, so you don't need to install it separately. 11 | 12 | ## Configuration 13 | 14 | 1. Copy the `.env.sample` file to `.env`: 15 | 16 | ```bash 17 | cp .env.sample .env 18 | ``` 19 | 20 | 2. Edit the `.env` file and set your SonarQube server URL, authentication token, and project key: 21 | 22 | ```bash 23 | SONAR_URL=http://your-sonarqube-server:9000 24 | SONAR_TOKEN=your-sonar-auth-token 25 | SONAR_PROJECT_KEY=ai-code-fusion 26 | ``` 27 | 28 | The project key must match an existing project on your SonarQube server, or you need permissions to create new projects. 29 | 30 | ## Running a Scan 31 | 32 | Run the following command to perform a SonarQube scan: 33 | 34 | ```bash 35 | npm run sonar 36 | ``` 37 | 38 | This will: 39 | 40 | 1. Check if the required environment variables are set 41 | 2. Run tests with coverage if coverage data doesn't exist 42 | 3. Execute the SonarQube scanner with your server configuration 43 | 44 | ## Understanding Results 45 | 46 | After the scan completes, you can view the results by: 47 | 48 | 1. Opening your SonarQube instance in a web browser 49 | 2. Navigating to your project (ai-code-fusion) 50 | 3. Reviewing the code quality metrics, issues, and recommendations 51 | 52 | ## Customizing the Analysis 53 | 54 | To customize the SonarQube analysis configuration, edit the `sonar-project.properties` file in the root of the project. 55 | 56 | ## Continuous Integration 57 | 58 | In a CI/CD pipeline, set the `SONAR_URL` and `SONAR_TOKEN` as environment variables in your CI platform (GitHub Actions, Jenkins, etc.) and call the `npm run sonar` script in your workflow. 59 | 60 | ## Troubleshooting 61 | 62 | If you encounter issues: 63 | 64 | - Make sure you've run `npm install` to install the SonarQube Scanner dependency 65 | - Verify your authentication token has sufficient permissions 66 | - Check network connectivity to your SonarQube server 67 | - Review the console output for specific error messages 68 | - If you encounter Java-related errors, ensure you have Java installed (JRE 11 or newer is required) 69 | 70 | ### Authorization Errors 71 | 72 | If you see errors like "You're not authorized to analyze this project or the project doesn't exist": 73 | 74 | 1. **Project Permissions**: Ensure your token has permissions to the specific project or to create new projects 75 | 2. **Project Creation**: If the project doesn't exist already: 76 | - Create it manually in SonarQube first, or 77 | - Use a token from an account with "Create Projects" permission 78 | 3. **Token Scope**: Make sure the token is not limited to a different project 79 | 4. **Project Key**: Verify that SONAR_PROJECT_KEY in your .env matches the project key in SonarQube 80 | 81 | You can also ask your SonarQube administrator to: 82 | 83 | - Create the project manually with the key matching your SONAR_PROJECT_KEY 84 | - Grant your user the necessary permissions to the project 85 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | moduleNameMapper: { 4 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', 5 | // Mock yaml module to fix import issues 6 | '^yaml$': '/tests/mocks/yaml-mock.js', 7 | }, 8 | setupFilesAfterEnv: ['/tests/setup.js'], 9 | testPathIgnorePatterns: ['/node_modules/'], 10 | transform: { 11 | '^.+\\.(js|jsx)$': 'babel-jest', 12 | }, 13 | // Needed to transform ESM modules 14 | transformIgnorePatterns: ['/node_modules/(?!(yaml)/)'], 15 | // Test match patterns 16 | testMatch: ['/tests/**/*.{js,jsx,ts,tsx}'], 17 | // Set verbose mode for more information during test runs 18 | verbose: true, 19 | // Add test coverage reports 20 | collectCoverage: false, 21 | coverageDirectory: '/coverage', 22 | collectCoverageFrom: [ 23 | '/src/renderer/components/**/*.{js,jsx}', 24 | '/src/utils/**/*.js', 25 | '!/src/**/*.d.ts', 26 | '!**/node_modules/**', 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion enableextensions 3 | 4 | rem ------------------------------------------------------------- 5 | rem AI Code Fusion - Build Script for Windows 6 | rem ------------------------------------------------------------- 7 | 8 | rem Ensure we're in the correct directory 9 | set "MAKE_ROOT=%~dp0" 10 | cd /d "%MAKE_ROOT%" 11 | 12 | rem Make scripts executable if coming from Unix/WSL 13 | if exist ".git" ( 14 | git update-index --chmod=+x scripts/index.js >nul 2>&1 15 | git update-index --chmod=+x scripts/lib/*.js >nul 2>&1 16 | ) 17 | 18 | rem Special handling for release command to pass version 19 | if /i "%1"=="release" ( 20 | if "%2"=="" ( 21 | echo Error: Version argument is required for release command 22 | echo Usage: make release ^ 23 | echo Example: make release 1.0.0 24 | exit /b 1 25 | ) 26 | scripts\index.js release %2 27 | exit /b %errorlevel% 28 | ) 29 | 30 | rem Special handling for sonar command on Windows 31 | if /i "%1"=="sonar" ( 32 | echo Running: npm run sonar 33 | 34 | rem Check if .env file exists and load it 35 | if exist ".env" ( 36 | echo Loading environment variables from .env file 37 | for /F "tokens=*" %%A in (.env) do ( 38 | set line=%%A 39 | if not "!line:~0,1!"=="#" ( 40 | for /f "tokens=1,2 delims==" %%G in ("!line!") do ( 41 | set "%%G=%%H" 42 | ) 43 | ) 44 | ) 45 | ) else ( 46 | echo Warning: .env file not found 47 | ) 48 | 49 | npm run sonar 50 | exit /b %errorlevel% 51 | ) 52 | 53 | rem Special handling for dev command on Windows 54 | if /i "%1"=="dev" ( 55 | echo Starting development environment for Windows... 56 | 57 | rem Set environment variables 58 | set NODE_ENV=development 59 | 60 | rem Cleanup 61 | echo Cleanup... 62 | call npm run clean 63 | 64 | rem Build CSS if needed 65 | if not exist "src\renderer\output.css" ( 66 | echo Building CSS... 67 | call npm run build:css 68 | ) 69 | 70 | rem Build webpack bundle if needed 71 | if not exist "src\renderer\index.js" ( 72 | echo Building webpack bundle... 73 | call npm run build:webpack 74 | ) 75 | 76 | echo Starting development server... 77 | npx concurrently --kill-others "npm:watch:css" "npm:watch:webpack" "npx electron ." 78 | exit /b %errorlevel% 79 | ) 80 | 81 | rem Run the command through our unified Node.js script 82 | node scripts/index.js %* 83 | exit /b %errorlevel% 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-code-fusion", 3 | "version": "0.2.0", 4 | "description": "AI Code Fusion", 5 | "main": "src/main/index.js", 6 | "scripts": { 7 | "start": "node scripts/index.js dev", 8 | "postinstall": "electron-builder install-app-deps && electron-rebuild", 9 | "rebuild": "electron-rebuild", 10 | "build": "node scripts/index.js build", 11 | "prebuild:webpack": "node scripts/ensure-build-dirs.js", 12 | "build:webpack": "cross-env NODE_ENV=production webpack --mode production", 13 | "prewatch:webpack": "node scripts/ensure-build-dirs.js", 14 | "watch:webpack": "cross-env NODE_ENV=development webpack --mode development --watch", 15 | "predev": "node scripts/clean-dev-assets.js", 16 | "dev": "concurrently \"npm run watch:webpack\" \"npm run watch:css\" \"node scripts/index.js dev\"", 17 | "clear-assets": "rimraf src/renderer/bundle.js src/renderer/bundle.js.map src/renderer/bundle.js.LICENSE.txt src/renderer/output.css", 18 | "lint": "eslint src tests --ext .js,.jsx --cache", 19 | "lint:tests": "eslint tests --ext .js,.jsx --cache", 20 | "format": "prettier --write \"**/*.{js,jsx,json,md,html,css}\"", 21 | "test": "jest --config jest.config.js --passWithNoTests", 22 | "test:watch": "jest --watch --config jest.config.js --passWithNoTests", 23 | "test:gitignore": "jest --config jest.config.js --testMatch=\"**/tests/unit/gitignore-parser.test.js\" --verbose", 24 | "test:binary": "jest --config jest.config.js --testMatch=\"**/tests/unit/binary-detection.test.js\" --verbose", 25 | "test:patterns": "jest --config jest.config.js --testMatch=\"**/tests/**/*pattern*.test.js\" --verbose", 26 | "test:lint": "npm run test && npm run lint:tests", 27 | "watch:css": "npx tailwindcss -i ./src/renderer/styles.css -o ./src/renderer/output.css --watch", 28 | "build:css": "npx tailwindcss -i ./src/renderer/styles.css -o ./src/renderer/output.css", 29 | "prepare": "husky install", 30 | "sonar": "node scripts/sonar-scan.js", 31 | "release": "node scripts/index.js release", 32 | "clean": "node scripts/index.js clean", 33 | "setup": "node scripts/index.js setup", 34 | "prebuild:win": "node scripts/prepare-build.js windows", 35 | "build:win": "cross-env NODE_ENV=production electron-builder --win", 36 | "build:mac": "cross-env NODE_ENV=production electron-builder --mac", 37 | "build:mac-arm": "cross-env NODE_ENV=production electron-builder --mac --arm64", 38 | "build:mac-universal": "cross-env NODE_ENV=production electron-builder --mac --universal", 39 | "build:linux": "cross-env NODE_ENV=production electron-builder --linux" 40 | }, 41 | "author": "AI Code Fusion ", 42 | "license": "GPL-3.0", 43 | "lint-staged": { 44 | "{src,tests}/**/*.{js,jsx}": [ 45 | "eslint --fix" 46 | ], 47 | "*.{json,md,html,css}": [ 48 | "prettier --write" 49 | ] 50 | }, 51 | "build": { 52 | "appId": "com.ai.code.fusion", 53 | "directories": { 54 | "output": "dist" 55 | }, 56 | "files": [ 57 | "src/**/*", 58 | "package.json", 59 | "node_modules/**/*" 60 | ], 61 | "extraResources": [ 62 | "build/Release/*.node" 63 | ], 64 | "extraMetadata": { 65 | "main": "src/main/index.js" 66 | }, 67 | "asar": true, 68 | "icon": "src/assets/icon.ico", 69 | "win": { 70 | "target": [ 71 | "nsis" 72 | ], 73 | "icon": "src/assets/icon.ico" 74 | }, 75 | "nsis": { 76 | "oneClick": false, 77 | "allowToChangeInstallationDirectory": true 78 | }, 79 | "linux": { 80 | "target": [ 81 | "AppImage" 82 | ], 83 | "category": "Utility", 84 | "artifactName": "${productName}-${version}.${ext}", 85 | "icon": "build/icons" 86 | }, 87 | "mac": { 88 | "target": [ 89 | "dmg", 90 | "zip" 91 | ], 92 | "icon": "src/assets/icon.icns", 93 | "category": "public.app-category.utilities" 94 | } 95 | }, 96 | "dependencies": { 97 | "@headlessui/react": "^1.7.18", 98 | "@heroicons/react": "^2.1.1", 99 | "clsx": "^2.1.0", 100 | "electron-store": "^8.1.0", 101 | "minimatch": "^9.0.3", 102 | "path-browserify": "^1.0.1", 103 | "process": "^0.11.10", 104 | "prop-types": "^15.8.1", 105 | "react": "^18.2.0", 106 | "react-dom": "^18.2.0", 107 | "react-router-dom": "^6.22.0", 108 | "tiktoken": "^1.0.11", 109 | "yaml": "^2.3.4" 110 | }, 111 | "devDependencies": { 112 | "@babel/core": "^7.26.9", 113 | "@babel/plugin-transform-modules-commonjs": "^7.26.3", 114 | "@babel/preset-env": "^7.26.9", 115 | "@babel/preset-react": "^7.26.3", 116 | "@electron/rebuild": "^3.6.0", 117 | "@jest/globals": "^29.7.0", 118 | "@testing-library/jest-dom": "^6.6.3", 119 | "@testing-library/react": "^14.3.1", 120 | "@testing-library/user-event": "^14.6.1", 121 | "autoprefixer": "^10.4.17", 122 | "babel-jest": "^29.7.0", 123 | "babel-loader": "^9.1.3", 124 | "concurrently": "^8.2.2", 125 | "cross-env": "^7.0.3", 126 | "css-loader": "^6.10.0", 127 | "electron": "^29.0.0", 128 | "electron-builder": "^24.9.1", 129 | "electron-devtools-installer": "^3.2.0", 130 | "electron-icon-maker": "0.0.5", 131 | "eslint": "^8.56.0", 132 | "eslint-config-prettier": "^9.1.0", 133 | "eslint-plugin-prettier": "^5.1.3", 134 | "eslint-plugin-react": "^7.37.4", 135 | "eslint-plugin-react-hooks": "^5.1.0", 136 | "eslint-plugin-tailwindcss": "^3.15.1", 137 | "husky": "^8.0.3", 138 | "identity-obj-proxy": "^3.0.0", 139 | "jest": "^29.7.0", 140 | "jest-environment-jsdom": "^29.7.0", 141 | "lint-staged": "^15.2.2", 142 | "postcss": "^8.4.35", 143 | "postcss-loader": "^8.1.0", 144 | "prettier": "^3.2.5", 145 | "rimraf": "^5.0.5", 146 | "style-loader": "^3.3.4", 147 | "sonarqube-scanner": "^3.3.0", 148 | "tailwindcss": "^3.4.1", 149 | "webpack": "^5.90.1", 150 | "webpack-cli": "^5.1.4" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | embeddedLanguageFormatting: 'auto', 5 | htmlWhitespaceSensitivity: 'css', 6 | insertPragma: false, 7 | jsxBracketSameLine: false, 8 | jsxSingleQuote: true, 9 | printWidth: 100, 10 | proseWrap: 'preserve', 11 | semi: true, 12 | singleQuote: true, 13 | tabWidth: 2, 14 | trailingComma: 'es5', 15 | useTabs: false, 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # AI Code Fusion Scripts 2 | 3 | This directory contains scripts for development, building, and releasing the AI Code Fusion application. 4 | 5 | ## Script Organization 6 | 7 | - `index.js` - Main entry point for all scripts 8 | - `lib/` - Reusable script modules 9 | - `build.js` - Build-related functions 10 | - `dev.js` - Development server functions 11 | - `release.js` - Release preparation functions 12 | - `utils.js` - Shared utility functions 13 | - `prepare-release.js` - Standalone script for release preparation 14 | - Various utility scripts for specific tasks 15 | 16 | ## Usage 17 | 18 | The recommended way to run scripts is through the unified `index.js` entry point: 19 | 20 | ```bash 21 | node scripts/index.js [args...] 22 | ``` 23 | 24 | ### Available Commands 25 | 26 | #### Development 27 | 28 | - `dev` or `start` - Start the development server 29 | - `css` - Build CSS files 30 | - `css:watch` - Watch and rebuild CSS files on changes 31 | 32 | #### Building 33 | 34 | - `build` - Build for the current platform 35 | - `build-win` - Build for Windows 36 | - `build-linux` - Build for Linux 37 | - `build-mac` - Build for macOS (Intel) 38 | - `build-mac-arm` - Build for macOS (Apple Silicon) 39 | - `build-mac-universal` - Build for macOS (Universal) 40 | 41 | #### Testing and Quality 42 | 43 | - `test` - Run all tests 44 | - `test:watch` - Watch and run tests on changes 45 | - `lint` - Run linter 46 | - `format` - Run code formatter 47 | - `validate` - Run all validation (lint + test) 48 | - `sonar` - Run SonarQube analysis 49 | 50 | #### Release Management 51 | 52 | - `release ` - Prepare a new release 53 | - `` can be a specific version number or `patch`, `minor`, or `major` 54 | 55 | #### Utility Commands 56 | 57 | - `setup` or `init` - Setup project 58 | - `clean` - Clean build artifacts 59 | - `clean-all` - Clean all generated files (including node_modules) 60 | - `icons` - Generate application icons 61 | 62 | ## Release Process 63 | 64 | The release process is handled by the `release.js` module, which can be invoked in two ways: 65 | 66 | ```bash 67 | # Using the unified entry point 68 | node scripts/index.js release 69 | 70 | # Using the standalone script 71 | node scripts/prepare-release.js 72 | ``` 73 | 74 | The release preparation process: 75 | 76 | 1. Updates the version in `package.json` 77 | 2. Prompts for changelog entries and updates `CHANGELOG.md` 78 | 3. Creates a git commit with the changes 79 | 4. Creates a git tag for the release 80 | 81 | After running the script, you need to manually push the changes and tag: 82 | 83 | ```bash 84 | git push && git push origin v 85 | ``` 86 | 87 | This will trigger the GitHub Actions workflow to build the application and create a release. 88 | -------------------------------------------------------------------------------- /scripts/clean-dev-assets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script cleans development assets to ensure they're properly rebuilt. 3 | * It removes CSS output files and bundled JS files. 4 | */ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const { promisify } = require('util'); 9 | const rimraf = promisify(require('rimraf')); 10 | 11 | // Asset paths relative to project root 12 | const assetPaths = [ 13 | 'src/renderer/bundle.js', 14 | 'src/renderer/bundle.js.map', 15 | 'src/renderer/bundle.js.LICENSE.txt', 16 | 'src/renderer/output.css', 17 | ]; 18 | 19 | async function cleanDevAssets() { 20 | console.log('🧹 Cleaning development assets...'); 21 | 22 | for (const assetPath of assetPaths) { 23 | const fullPath = path.join(process.cwd(), assetPath); 24 | 25 | try { 26 | await rimraf(fullPath); 27 | console.log(` ✓ Removed: ${assetPath}`); 28 | } catch (err) { 29 | // Ignore errors for files that don't exist 30 | if (err.code !== 'ENOENT') { 31 | console.error(` ✗ Error removing ${assetPath}:`, err.message); 32 | } 33 | } 34 | } 35 | 36 | console.log('✅ Development assets cleaned successfully'); 37 | } 38 | 39 | // Run the cleaning process 40 | cleanDevAssets().catch((err) => { 41 | console.error('Error cleaning assets:', err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /scripts/ensure-build-dirs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script to ensure build directories exist for webpack 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | // Define build directory paths 8 | const buildDir = path.resolve(__dirname, '../build'); 9 | const rendererDir = path.resolve(buildDir, 'renderer'); 10 | 11 | // Create directories if they don't exist 12 | function ensureDirectoryExists(dirPath) { 13 | if (!fs.existsSync(dirPath)) { 14 | console.log(`Creating directory: ${dirPath}`); 15 | fs.mkdirSync(dirPath, { recursive: true }); 16 | } 17 | } 18 | 19 | // Ensure all required directories exist 20 | ensureDirectoryExists(buildDir); 21 | ensureDirectoryExists(rendererDir); 22 | 23 | console.log('Build directories ready'); 24 | -------------------------------------------------------------------------------- /scripts/generate-icons.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Icon Generator for Electron Application 4 | * 5 | * Generates all required platform-specific icons from a single 1024x1024 PNG image. 6 | * This script only needs to be run once or when updating the application icon. 7 | * 8 | * Usage: 9 | * node scripts/generate-icons.js 10 | */ 11 | 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | const { execSync } = require('child_process'); 15 | 16 | // Paths configuration 17 | const ROOT_DIR = path.join(__dirname, '..'); 18 | const SOURCE_LOGO = path.join(ROOT_DIR, 'src/assets/logo.png'); 19 | const TEMP_DIR = path.join(ROOT_DIR, '.tmp_icons'); 20 | const ASSETS_ICONS_DIR = path.join(ROOT_DIR, 'src/assets/icons'); 21 | const WINDOWS_ICON_DEST = path.join(ROOT_DIR, 'src/assets/icon.ico'); 22 | const MACOS_ICON_DEST = path.join(ROOT_DIR, 'src/assets/icon.icns'); 23 | const LINUX_ICONS_DIR = path.join(ROOT_DIR, 'build/icons'); 24 | 25 | // Helper functions 26 | function ensureDir(dir) { 27 | if (!fs.existsSync(dir)) { 28 | fs.mkdirSync(dir, { recursive: true }); 29 | console.log(`Created directory: ${dir}`); 30 | } 31 | } 32 | 33 | function validateSourceLogo() { 34 | if (!fs.existsSync(SOURCE_LOGO)) { 35 | console.error(`\nError: Source logo not found at ${SOURCE_LOGO}`); 36 | console.error('Please create a 1024x1024 PNG image file at this location.'); 37 | process.exit(1); 38 | } 39 | 40 | // Check if it's a valid PNG 41 | try { 42 | const buffer = Buffer.alloc(8); 43 | const fd = fs.openSync(SOURCE_LOGO, 'r'); 44 | fs.readSync(fd, buffer, 0, 8, 0); 45 | fs.closeSync(fd); 46 | 47 | // PNG signature: 89 50 4E 47 0D 0A 1A 0A 48 | const isPng = 49 | buffer[0] === 0x89 && 50 | buffer[1] === 0x50 && 51 | buffer[2] === 0x4e && 52 | buffer[3] === 0x47 && 53 | buffer[4] === 0x0d && 54 | buffer[5] === 0x0a && 55 | buffer[6] === 0x1a && 56 | buffer[7] === 0x0a; 57 | 58 | if (!isPng) { 59 | console.error(`\nError: ${SOURCE_LOGO} is not a valid PNG file.`); 60 | console.error('Please provide a proper PNG image file.'); 61 | process.exit(1); 62 | } 63 | } catch (error) { 64 | console.error(`\nError reading source logo: ${error.message}`); 65 | process.exit(1); 66 | } 67 | } 68 | 69 | function cleanup() { 70 | if (fs.existsSync(TEMP_DIR)) { 71 | try { 72 | fs.rmSync(TEMP_DIR, { recursive: true, force: true }); 73 | console.log(`Removed temporary directory: ${TEMP_DIR}`); 74 | } catch (error) { 75 | console.warn(`Warning: Could not remove temporary directory: ${error.message}`); 76 | } 77 | } 78 | } 79 | 80 | // Main icon generation function 81 | async function generateIcons() { 82 | console.log('\n=== ELECTRON ICON GENERATOR ===\n'); 83 | 84 | try { 85 | // Validate source logo 86 | validateSourceLogo(); 87 | 88 | // Ensure directories exist 89 | ensureDir(path.dirname(WINDOWS_ICON_DEST)); 90 | ensureDir(path.dirname(MACOS_ICON_DEST)); 91 | ensureDir(LINUX_ICONS_DIR); 92 | ensureDir(TEMP_DIR); 93 | 94 | // Install electron-icon-builder if not already available 95 | console.log('Installing icon generation tools...'); 96 | execSync('npm install --no-save electron-icon-builder', { 97 | stdio: 'inherit', 98 | cwd: ROOT_DIR, 99 | }); 100 | 101 | // Generate icons 102 | console.log('\nGenerating platform-specific icons...'); 103 | execSync(`npx electron-icon-builder --input="${SOURCE_LOGO}" --output="${TEMP_DIR}"`, { 104 | stdio: 'inherit', 105 | cwd: ROOT_DIR, 106 | }); 107 | 108 | // Copy icons to their final destinations 109 | console.log('\nCopying icons to their final destinations...'); 110 | 111 | // Create assets/icons structure 112 | ensureDir(ASSETS_ICONS_DIR); 113 | ensureDir(path.join(ASSETS_ICONS_DIR, 'win')); 114 | ensureDir(path.join(ASSETS_ICONS_DIR, 'mac')); 115 | ensureDir(path.join(ASSETS_ICONS_DIR, 'png')); 116 | 117 | // Windows icon 118 | const winIconSrc = path.join(TEMP_DIR, 'icons/win/icon.ico'); 119 | if (fs.existsSync(winIconSrc)) { 120 | // Copy to both locations 121 | fs.copyFileSync(winIconSrc, WINDOWS_ICON_DEST); 122 | fs.copyFileSync(winIconSrc, path.join(ASSETS_ICONS_DIR, 'win/icon.ico')); 123 | console.log(`✓ Windows icon created: ${WINDOWS_ICON_DEST}`); 124 | console.log(`✓ Windows icon copied to: ${path.join(ASSETS_ICONS_DIR, 'win/icon.ico')}`); 125 | } else { 126 | console.error(`✗ Windows icon generation failed`); 127 | } 128 | 129 | // macOS icon 130 | const macIconSrc = path.join(TEMP_DIR, 'icons/mac/icon.icns'); 131 | if (fs.existsSync(macIconSrc)) { 132 | // Copy to both locations 133 | fs.copyFileSync(macIconSrc, MACOS_ICON_DEST); 134 | fs.copyFileSync(macIconSrc, path.join(ASSETS_ICONS_DIR, 'mac/icon.icns')); 135 | console.log(`✓ macOS icon created: ${MACOS_ICON_DEST}`); 136 | console.log(`✓ macOS icon copied to: ${path.join(ASSETS_ICONS_DIR, 'mac/icon.icns')}`); 137 | } else { 138 | console.error(`✗ macOS icon generation failed`); 139 | } 140 | 141 | // Linux icons 142 | const pngDir = path.join(TEMP_DIR, 'icons/png'); 143 | if (fs.existsSync(pngDir)) { 144 | const pngFiles = fs.readdirSync(pngDir).filter((file) => file.endsWith('.png')); 145 | 146 | for (const file of pngFiles) { 147 | const sourcePath = path.join(pngDir, file); 148 | // Copy to both locations 149 | const destPath = path.join(LINUX_ICONS_DIR, file); 150 | const assetDestPath = path.join(ASSETS_ICONS_DIR, 'png', file); 151 | fs.copyFileSync(sourcePath, destPath); 152 | fs.copyFileSync(sourcePath, assetDestPath); 153 | } 154 | 155 | // Also copy 512x512 as icon.png (main icon) 156 | const png512 = path.join(pngDir, '512x512.png'); 157 | if (fs.existsSync(png512)) { 158 | fs.copyFileSync(png512, path.join(LINUX_ICONS_DIR, 'icon.png')); 159 | } 160 | 161 | console.log(`✓ Linux icons created in: ${LINUX_ICONS_DIR}`); 162 | console.log(`✓ Linux icons copied to: ${path.join(ASSETS_ICONS_DIR, 'png')}`); 163 | } else { 164 | console.error(`✗ Linux icon generation failed`); 165 | } 166 | 167 | console.log('\n=== ICON GENERATION COMPLETE ===\n'); 168 | console.log('The following icon files have been generated:'); 169 | console.log( 170 | `• Windows: ${WINDOWS_ICON_DEST} and ${path.join(ASSETS_ICONS_DIR, 'win/icon.ico')}` 171 | ); 172 | console.log(`• macOS: ${MACOS_ICON_DEST} and ${path.join(ASSETS_ICONS_DIR, 'mac/icon.icns')}`); 173 | console.log( 174 | `• Linux: ${LINUX_ICONS_DIR}/*.png and ${path.join(ASSETS_ICONS_DIR, 'png')}/*.png` 175 | ); 176 | console.log('\nThese files should be committed to your repository.'); 177 | } catch (error) { 178 | console.error(`\nIcon generation failed: ${error.message}`); 179 | process.exit(1); 180 | } finally { 181 | // Clean up temporary files 182 | cleanup(); 183 | } 184 | } 185 | 186 | // Run the icon generation 187 | generateIcons().catch((error) => { 188 | console.error(`Unexpected error: ${error.message}`); 189 | process.exit(1); 190 | }); 191 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * AI Code Fusion - Unified Script Runner 4 | * 5 | * This script provides a unified entry point for all build, dev, and utility scripts 6 | * 7 | * Usage: 8 | * node scripts/index.js [args...] 9 | * 10 | * Examples: 11 | * node scripts/index.js dev - Start dev environment 12 | * node scripts/index.js build - Build for current platform 13 | * node scripts/index.js release - Create a new release 14 | */ 15 | 16 | const path = require('path'); 17 | const { execSync } = require('child_process'); 18 | 19 | // Import script modules 20 | const utils = require('./lib/utils'); 21 | const build = require('./lib/build'); 22 | const dev = require('./lib/dev'); 23 | const release = require('./lib/release'); 24 | 25 | // Get the command from first argument 26 | const [command, ...args] = process.argv.slice(2); 27 | 28 | // Execute the command 29 | async function executeCommand() { 30 | if (!command) { 31 | utils.printHelp(); 32 | process.exit(0); 33 | } 34 | 35 | try { 36 | switch (command) { 37 | // Development commands 38 | case 'dev': 39 | case 'start': 40 | await dev.start(); 41 | break; 42 | 43 | // Build commands 44 | case 'build': 45 | await build.forCurrentPlatform(); 46 | break; 47 | 48 | case 'build:win': 49 | case 'build-win': 50 | await build.forPlatform('win'); 51 | break; 52 | 53 | case 'build:linux': 54 | case 'build-linux': 55 | await build.forPlatform('linux'); 56 | break; 57 | 58 | case 'build:mac': 59 | case 'build-mac': 60 | await build.forPlatform('mac'); 61 | break; 62 | 63 | case 'build:mac-arm': 64 | case 'build-mac-arm': 65 | await build.forPlatform('mac-arm'); 66 | break; 67 | 68 | case 'build:mac-universal': 69 | case 'build-mac-universal': 70 | await build.forPlatform('mac-universal'); 71 | break; 72 | 73 | // Setup and init commands 74 | case 'setup': 75 | case 'init': 76 | await utils.setupProject(); 77 | break; 78 | 79 | // Clean commands 80 | case 'clean': 81 | await utils.cleanBuildArtifacts(); 82 | break; 83 | 84 | case 'clean-all': 85 | case 'clean:all': 86 | await utils.cleanAll(); 87 | break; 88 | 89 | // CSS commands 90 | case 'css': 91 | await utils.runNpmScript('build:css'); 92 | console.log('CSS built successfully'); 93 | break; 94 | 95 | case 'css:watch': 96 | case 'css-watch': 97 | await utils.runNpmScript('watch:css'); 98 | break; 99 | 100 | // Testing commands 101 | case 'test': 102 | await utils.runNpmScript('test'); 103 | console.log('Tests completed successfully'); 104 | break; 105 | 106 | case 'test:watch': 107 | await utils.runNpmScript('test:watch'); 108 | break; 109 | 110 | // Code quality commands 111 | case 'lint': 112 | await utils.runNpmScript('lint'); 113 | console.log('Linting completed successfully'); 114 | break; 115 | 116 | case 'format': 117 | await utils.runNpmScript('format'); 118 | console.log('Formatting completed successfully'); 119 | break; 120 | 121 | case 'validate': 122 | console.log('Running all validations...'); 123 | await utils.runNpmScript('lint'); 124 | await utils.runNpmScript('test'); 125 | console.log('All validations passed!'); 126 | break; 127 | 128 | // Asset management commands 129 | case 'icons': 130 | await build.generateIcons(); 131 | break; 132 | 133 | // Release commands 134 | case 'release': 135 | if (args.length === 0) { 136 | console.error('Error: Version argument is required'); 137 | console.error('Usage: node scripts/index.js release '); 138 | console.error('Example: node scripts/index.js release 1.0.0'); 139 | console.error('or: node scripts/index.js release patch|minor|major'); 140 | process.exit(1); 141 | } 142 | // Filter out any empty strings that might come from make passing arguments 143 | const versionArg = args.filter((arg) => arg.trim() !== '')[0]; 144 | if (!versionArg) { 145 | console.error('Error: Version argument is required'); 146 | console.error('Usage: node scripts/index.js release '); 147 | console.error('Example: node scripts/index.js release 1.0.0'); 148 | console.error('or: node scripts/index.js release patch|minor|major'); 149 | process.exit(1); 150 | } 151 | await release.prepare(versionArg); 152 | break; 153 | 154 | // Git hooks 155 | case 'setup-hooks': 156 | case 'hooks': 157 | await utils.setupHooks(); 158 | break; 159 | 160 | // SonarQube analysis 161 | case 'sonar': 162 | await utils.runNpmScript('sonar'); 163 | console.log('SonarQube analysis completed successfully'); 164 | break; 165 | 166 | // Direct script execution for backward compatibility 167 | case 'run': 168 | if (args.length === 0) { 169 | console.error('Error: Script name is required'); 170 | console.error('Usage: node scripts/index.js run [args...]'); 171 | process.exit(1); 172 | } 173 | 174 | const scriptPath = path.join(__dirname, `${args[0]}.js`); 175 | if (!utils.fileExists(scriptPath)) { 176 | console.error(`Error: Script not found: ${scriptPath}`); 177 | process.exit(1); 178 | } 179 | 180 | console.log(`Running script: ${args[0]}`); 181 | execSync(`node ${scriptPath} ${args.slice(1).join(' ')}`, { 182 | stdio: 'inherit', 183 | cwd: utils.ROOT_DIR, 184 | }); 185 | break; 186 | 187 | default: 188 | console.error(`Error: Unknown command '${command}'`); 189 | utils.printHelp(); 190 | process.exit(1); 191 | } 192 | } catch (error) { 193 | console.error(`Error executing command: ${error.message}`); 194 | process.exit(1); 195 | } 196 | } 197 | 198 | // Execute the command and handle errors 199 | executeCommand().catch((error) => { 200 | console.error(`Unhandled error: ${error.message}`); 201 | process.exit(1); 202 | }); 203 | -------------------------------------------------------------------------------- /scripts/lib/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build functions for the application 3 | */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const { execSync } = require('child_process'); 8 | const utils = require('./utils'); 9 | 10 | /** 11 | * Prepare the build environment for a specific platform 12 | * @param {string} platform - Platform to prepare for (win, linux, mac) 13 | */ 14 | async function preparePlatform(platform) { 15 | console.log(`Preparing build environment for ${platform}...`); 16 | 17 | try { 18 | execSync(`node ${path.join(utils.ROOT_DIR, 'scripts/prepare-build.js')} ${platform}`, { 19 | stdio: 'inherit', 20 | cwd: utils.ROOT_DIR, 21 | }); 22 | return true; 23 | } catch (error) { 24 | console.error(`Build preparation failed: ${error.message}`); 25 | throw error; 26 | } 27 | } 28 | 29 | /** 30 | * Generate application icons 31 | */ 32 | async function generateIcons() { 33 | console.log('Generating application icons...'); 34 | 35 | try { 36 | // Ensure required directories exist 37 | const ASSETS_DIR = path.join(utils.ROOT_DIR, 'src/assets'); 38 | const ICONS_GENERATED_DIR = path.join(ASSETS_DIR, 'icons'); 39 | const BUILD_ICONS_DIR = path.join(utils.ROOT_DIR, 'build/icons'); 40 | 41 | // Create directories if they don't exist 42 | utils.ensureDir(ASSETS_DIR); 43 | utils.ensureDir(ICONS_GENERATED_DIR); 44 | utils.ensureDir(BUILD_ICONS_DIR); 45 | 46 | // Check for logo.png 47 | const logoPath = path.join(ASSETS_DIR, 'logo.png'); 48 | if (!fs.existsSync(logoPath)) { 49 | console.warn(`Warning: logo.png not found at ${logoPath}`); 50 | console.warn('Please add a 1024x1024 PNG logo file to this location'); 51 | } 52 | 53 | // Generate icons 54 | execSync(`node ${path.join(utils.ROOT_DIR, 'scripts/generate-icons.js')}`, { 55 | stdio: 'inherit', 56 | cwd: utils.ROOT_DIR, 57 | }); 58 | 59 | console.log('Icon generation completed successfully'); 60 | return true; 61 | } catch (error) { 62 | console.error(`Icon generation failed: ${error.message}`); 63 | throw error; 64 | } 65 | } 66 | 67 | /** 68 | * Build the application for a specific platform 69 | * @param {string} platform - Platform to build for (win, linux, mac, mac-arm, mac-universal) 70 | */ 71 | async function forPlatform(platform) { 72 | console.log(`Building for ${platform}...`); 73 | 74 | try { 75 | // Build CSS and webpack first 76 | utils.runNpmScript('build:css'); 77 | utils.runNpmScript('build:webpack'); 78 | 79 | // Prepare platform-specific assets and config 80 | let platformArg = platform; 81 | if (platform === 'win') platformArg = 'windows'; 82 | if (platform === 'mac' || platform === 'mac-arm' || platform === 'mac-universal') 83 | platformArg = 'mac'; 84 | 85 | await preparePlatform(platformArg); 86 | 87 | // Special handling for Linux to avoid icon issues 88 | if (platform === 'linux') { 89 | // Check if we're in a CI environment, use npm script if we are 90 | if (process.env.CI) { 91 | utils.runNpmScript('build:linux'); 92 | } else { 93 | // Use direct command to avoid recursion 94 | execSync('npx electron-builder --linux AppImage --publish=never --c.linux.icon=false', { 95 | stdio: 'inherit', 96 | cwd: utils.ROOT_DIR, 97 | env: { ...process.env, NODE_ENV: 'production' }, 98 | }); 99 | } 100 | console.log('Linux build completed successfully'); 101 | return true; 102 | } 103 | 104 | // For other platforms 105 | let scriptName = 'build'; 106 | let directCommand = 'npx electron-builder'; 107 | 108 | if (platform === 'win') { 109 | scriptName = 'build:win'; 110 | directCommand = 'npx electron-builder --win'; 111 | } else if (platform === 'mac') { 112 | scriptName = 'build:mac'; 113 | directCommand = 'npx electron-builder --mac'; 114 | } else if (platform === 'mac-arm') { 115 | scriptName = 'build:mac-arm'; 116 | directCommand = 'npx electron-builder --mac --arm64'; 117 | } else if (platform === 'mac-universal') { 118 | scriptName = 'build:mac-universal'; 119 | directCommand = 'npx electron-builder --mac --universal'; 120 | } 121 | 122 | // Use npm script in CI environments, direct command otherwise 123 | if (process.env.CI) { 124 | utils.runNpmScript(scriptName); 125 | } else { 126 | execSync(directCommand, { 127 | stdio: 'inherit', 128 | cwd: utils.ROOT_DIR, 129 | env: { ...process.env, NODE_ENV: 'production' }, 130 | }); 131 | } 132 | console.log(`${platform} build completed successfully`); 133 | return true; 134 | } catch (error) { 135 | console.error(`Build failed: ${error.message}`); 136 | throw error; 137 | } 138 | } 139 | 140 | /** 141 | * Build for the current platform 142 | */ 143 | async function forCurrentPlatform() { 144 | let platform; 145 | 146 | if (process.platform === 'win32') { 147 | platform = 'win'; 148 | } else if (process.platform === 'darwin') { 149 | // On macOS, auto-detect Intel vs ARM 150 | platform = process.arch === 'arm64' ? 'mac-arm' : 'mac'; 151 | } else { 152 | platform = 'linux'; 153 | } 154 | 155 | return forPlatform(platform); 156 | } 157 | 158 | module.exports = { 159 | preparePlatform, 160 | generateIcons, 161 | forPlatform, 162 | forCurrentPlatform, 163 | }; 164 | -------------------------------------------------------------------------------- /scripts/lib/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Development environment functions 3 | */ 4 | 5 | const { spawn } = require('child_process'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const utils = require('./utils'); 9 | 10 | // Colors for console output 11 | const colors = { 12 | reset: '\x1b[0m', 13 | green: '\x1b[32m', 14 | yellow: '\x1b[33m', 15 | blue: '\x1b[34m', 16 | red: '\x1b[31m', 17 | }; 18 | 19 | // Format log messages 20 | const log = (message, color = colors.reset) => { 21 | console.log(`${color}[dev] ${message}${colors.reset}`); 22 | }; 23 | 24 | /** 25 | * Start the development environment 26 | */ 27 | async function start() { 28 | log('Starting development environment...', colors.green); 29 | 30 | try { 31 | // Build CSS if it doesn't exist 32 | const cssFile = path.join(utils.ROOT_DIR, 'src', 'renderer', 'output.css'); 33 | if (!fs.existsSync(cssFile)) { 34 | log('CSS not found, building...', colors.yellow); 35 | try { 36 | // Try direct command execution first 37 | const { execSync } = require('child_process'); 38 | log('Running tailwindcss directly...', colors.blue); 39 | execSync('npx tailwindcss -i ./src/renderer/styles.css -o ./src/renderer/output.css', { 40 | stdio: 'inherit', 41 | cwd: utils.ROOT_DIR, 42 | }); 43 | } catch (err) { 44 | log(`Error running tailwindcss: ${err.message}`, colors.red); 45 | throw err; 46 | } 47 | } 48 | 49 | // Check if webpack output exists 50 | const webpackOutput = path.join(utils.ROOT_DIR, 'src', 'renderer', 'bundle.js'); 51 | if (!fs.existsSync(webpackOutput)) { 52 | log('Webpack bundle not found, building...', colors.yellow); 53 | utils.runNpmScript('build:webpack'); 54 | } 55 | 56 | // Start the dev server using concurrently to run all necessary processes 57 | log('Starting development server...', colors.blue); 58 | 59 | // Use concurrently directly to run all the required processes 60 | // Simplified approach without inline environment variable assignments 61 | 62 | // Use direct shell command to ensure proper argument parsing 63 | // This mimics exactly how make.bat does it successfully 64 | const concurrently = spawn( 65 | 'npx concurrently --kill-others "npm run watch:css" "npm run watch:webpack" "cross-env NODE_ENV=development electron ."', 66 | [], 67 | { 68 | stdio: 'inherit', 69 | shell: true, 70 | cwd: utils.ROOT_DIR, 71 | env: { 72 | ...process.env, 73 | // Explicitly set NODE_ENV - this is the proper way to set environment variables on all platforms 74 | NODE_ENV: 'development', 75 | }, 76 | } 77 | ); 78 | 79 | // Improved error handling 80 | concurrently.on('error', (error) => { 81 | log(`Concurrently process error: ${error.message}`, colors.red); 82 | log( 83 | 'This may be due to missing dependencies. Try running "npm install" first.', 84 | colors.yellow 85 | ); 86 | }); 87 | 88 | // Handle process exit with better error messaging 89 | concurrently.on('close', (code) => { 90 | if (code !== 0) { 91 | log(`Development process exited with code ${code}`, colors.red); 92 | log('Check the error messages above for more details.', colors.yellow); 93 | log('Common issues:', colors.yellow); 94 | log('1. Conflicting file locks - Try closing other instances first', colors.yellow); 95 | log('2. Missing dependencies - Run "npm install"', colors.yellow); 96 | log('3. Port conflicts - Check if another app is using the required ports', colors.yellow); 97 | process.exit(code); 98 | } 99 | }); 100 | 101 | // Forward process signals 102 | process.on('SIGINT', () => concurrently.kill('SIGINT')); 103 | process.on('SIGTERM', () => concurrently.kill('SIGTERM')); 104 | 105 | return true; 106 | } catch (error) { 107 | log(`Development server failed to start: ${error.message}`, colors.red); 108 | throw error; 109 | } 110 | } 111 | 112 | module.exports = { 113 | start, 114 | }; 115 | -------------------------------------------------------------------------------- /scripts/lib/release.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Release preparation functions 3 | */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const { execSync } = require('child_process'); 8 | const readline = require('readline'); 9 | const utils = require('./utils'); 10 | 11 | // Paths 12 | const PACKAGE_JSON_PATH = path.join(utils.ROOT_DIR, 'package.json'); 13 | const CHANGELOG_PATH = path.join(utils.ROOT_DIR, 'CHANGELOG.md'); 14 | 15 | /** 16 | * Update package.json version 17 | * @param {string} version - Version string or 'patch', 'minor', 'major' 18 | * @returns {string} The actual version that was set 19 | */ 20 | function updatePackageVersion(version) { 21 | console.log(`Updating package.json version to ${version}...`); 22 | 23 | const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')); 24 | 25 | // If using semver keywords, calculate the new version 26 | if (['patch', 'minor', 'major'].includes(version)) { 27 | const currentVersion = packageJson.version; 28 | const [major, minor, patch] = currentVersion.split('.').map((v) => parseInt(v, 10)); 29 | 30 | let newVersion; 31 | if (version === 'patch') { 32 | newVersion = `${major}.${minor}.${patch + 1}`; 33 | } else if (version === 'minor') { 34 | newVersion = `${major}.${minor + 1}.0`; 35 | } else if (version === 'major') { 36 | newVersion = `${major + 1}.0.0`; 37 | } 38 | 39 | console.log(`Incrementing ${version} version: ${currentVersion} → ${newVersion}`); 40 | version = newVersion; 41 | } 42 | 43 | // Update package.json 44 | packageJson.version = version; 45 | fs.writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + '\n'); 46 | 47 | return version; 48 | } 49 | 50 | /** 51 | * Update CHANGELOG.md with new release entries 52 | * @param {string} version - Version string 53 | * @returns {Promise} 54 | */ 55 | function updateChangelog(version) { 56 | return new Promise((resolve) => { 57 | console.log('\nPlease enter the changelog entries for this release.'); 58 | console.log('Enter a blank line when done.\n'); 59 | 60 | // Create a readline interface for user input 61 | const rl = readline.createInterface({ 62 | input: process.stdin, 63 | output: process.stdout, 64 | }); 65 | 66 | const entries = []; 67 | 68 | function promptForEntry() { 69 | rl.question('Entry: ', (entry) => { 70 | if (entry.trim() === '') { 71 | // Done entering entries 72 | let changelogContent; 73 | 74 | // Create the file if it doesn't exist 75 | if (!fs.existsSync(CHANGELOG_PATH)) { 76 | console.log('Creating new CHANGELOG.md file...'); 77 | changelogContent = 78 | '# Changelog\n\nAll notable changes to this project will be documented in this file.\n'; 79 | } else { 80 | changelogContent = fs.readFileSync(CHANGELOG_PATH, 'utf8'); 81 | } 82 | 83 | const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD 84 | 85 | // Format the new entry with [v] prefix for GitHub release automation 86 | const newEntry = `\n## [v${version}] - ${date}\n\n${entries 87 | .map((e) => `- ${e}`) 88 | .join('\n')}\n`; 89 | 90 | // Insert after "All notable changes" line if present, or after the first line 91 | const updatedChangelog = changelogContent.includes('All notable changes') 92 | ? changelogContent.replace( 93 | /All notable changes to this project will be documented in this file.\n/, 94 | `All notable changes to this project will be documented in this file.\n${newEntry}` 95 | ) 96 | : changelogContent.replace(/# Changelog\n/, `# Changelog\n${newEntry}`); 97 | 98 | fs.writeFileSync(CHANGELOG_PATH, updatedChangelog); 99 | console.log(`\nChangelog updated for version ${version}`); 100 | 101 | rl.close(); 102 | resolve(); 103 | } else { 104 | entries.push(entry); 105 | promptForEntry(); 106 | } 107 | }); 108 | } 109 | 110 | promptForEntry(); 111 | }); 112 | } 113 | 114 | /** 115 | * Create a git tag for the release 116 | * @param {string} version - Version string 117 | * @returns {Promise} 118 | */ 119 | function createGitTag(version) { 120 | return new Promise((resolve) => { 121 | // Create a readline interface for user input 122 | const rl = readline.createInterface({ 123 | input: process.stdin, 124 | output: process.stdout, 125 | }); 126 | 127 | rl.question(`\nCreate git tag v${version}? (y/n): `, (answer) => { 128 | if (answer.toLowerCase() === 'y') { 129 | try { 130 | // Add package.json and CHANGELOG.md 131 | execSync('git add package.json CHANGELOG.md', { stdio: 'inherit', cwd: utils.ROOT_DIR }); 132 | 133 | // Commit the changes 134 | execSync(`git commit -m "Release v${version}"`, { 135 | stdio: 'inherit', 136 | cwd: utils.ROOT_DIR, 137 | }); 138 | 139 | // Create the tag 140 | execSync(`git tag -a v${version} -m "Version ${version}"`, { 141 | stdio: 'inherit', 142 | cwd: utils.ROOT_DIR, 143 | }); 144 | 145 | console.log(`\nGit tag v${version} created. To push the tag, run:`); 146 | console.log(`git push && git push origin v${version}`); 147 | } catch (error) { 148 | console.error(`\nError creating git tag: ${error.message}`); 149 | } 150 | } else { 151 | console.log('\nSkipping git tag creation.'); 152 | } 153 | 154 | rl.close(); 155 | resolve(); 156 | }); 157 | }); 158 | } 159 | 160 | /** 161 | * Prepare a new release 162 | * @param {string} version - Version string or 'patch', 'minor', 'major' 163 | */ 164 | async function prepare(version) { 165 | try { 166 | console.log('Preparing release...'); 167 | 168 | // Update package.json version 169 | const finalVersion = updatePackageVersion(version); 170 | 171 | // Update changelog 172 | await updateChangelog(finalVersion); 173 | 174 | // Create git tag 175 | await createGitTag(finalVersion); 176 | 177 | console.log('\nRelease preparation complete!'); 178 | return true; 179 | } catch (error) { 180 | console.error(`\nError preparing release: ${error.message}`); 181 | throw error; 182 | } 183 | } 184 | 185 | module.exports = { 186 | updatePackageVersion, 187 | updateChangelog, 188 | createGitTag, 189 | prepare, 190 | }; 191 | -------------------------------------------------------------------------------- /scripts/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for build scripts 3 | */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const { execSync } = require('child_process'); 8 | 9 | // Root directory of the project 10 | const ROOT_DIR = path.join(__dirname, '../..'); 11 | 12 | // File/path utilities 13 | function fileExists(filePath) { 14 | return fs.existsSync(filePath); 15 | } 16 | 17 | function ensureDir(dirPath) { 18 | if (!fs.existsSync(dirPath)) { 19 | fs.mkdirSync(dirPath, { recursive: true }); 20 | console.log(`Created directory: ${dirPath}`); 21 | } 22 | } 23 | 24 | // Command execution helpers 25 | function runNpm(command, args = [], options = {}) { 26 | const fullCommand = `npm ${command}${args.length > 0 ? ' ' + args.join(' ') : ''}`; 27 | console.log(`Running: ${fullCommand}`); 28 | 29 | try { 30 | execSync(fullCommand, { 31 | stdio: 'inherit', 32 | cwd: ROOT_DIR, 33 | ...options, 34 | }); 35 | return true; 36 | } catch (error) { 37 | console.error(`Error running 'npm ${command}': ${error.message}`); 38 | throw error; 39 | } 40 | } 41 | 42 | function runNpmScript(script, args = [], options = {}) { 43 | const command = `npm run ${script}${args.length > 0 ? ' -- ' + args.join(' ') : ''}`; 44 | console.log(`Running: ${command}`); 45 | 46 | try { 47 | execSync(command, { 48 | stdio: 'inherit', 49 | cwd: ROOT_DIR, 50 | ...options, 51 | }); 52 | return true; 53 | } catch (error) { 54 | console.error(`Error running '${script}': ${error.message}`); 55 | throw error; 56 | } 57 | } 58 | 59 | // Setup functions 60 | async function setupProject() { 61 | console.log('Setting up the project...'); 62 | 63 | try { 64 | // Install dependencies 65 | runNpm('install'); 66 | 67 | // Build CSS 68 | runNpmScript('build:css'); 69 | 70 | // Setup hooks 71 | try { 72 | runNpmScript('prepare'); 73 | } catch (error) { 74 | console.warn('Warning: Pre-commit hooks setup had issues, but continuing with setup'); 75 | } 76 | 77 | console.log('Setup completed successfully'); 78 | console.log(''); 79 | console.log('You can now run "node scripts/index.js dev" to start the development server'); 80 | return true; 81 | } catch (error) { 82 | console.error(`Setup failed: ${error.message}`); 83 | throw error; 84 | } 85 | } 86 | 87 | async function setupHooks() { 88 | console.log(`Setting up Git hooks for ${process.platform}...`); 89 | 90 | try { 91 | runNpmScript('prepare'); 92 | console.log('Hooks setup completed successfully'); 93 | return true; 94 | } catch (error) { 95 | console.warn('Warning: Hooks setup had issues'); 96 | throw error; 97 | } 98 | } 99 | 100 | // Clean functions 101 | async function cleanBuildArtifacts() { 102 | const pathsToRemove = [ 103 | path.join(ROOT_DIR, 'dist'), 104 | path.join(ROOT_DIR, 'src', 'renderer', 'index.js'), 105 | path.join(ROOT_DIR, 'src', 'renderer', 'index.js.map'), 106 | path.join(ROOT_DIR, 'src', 'renderer', 'bundle.js'), 107 | path.join(ROOT_DIR, 'src', 'renderer', 'bundle.js.map'), 108 | path.join(ROOT_DIR, 'src', 'renderer', 'output.css'), 109 | ]; 110 | 111 | console.log('Cleaning build artifacts...'); 112 | 113 | for (const p of pathsToRemove) { 114 | if (fs.existsSync(p)) { 115 | console.log(`Removing: ${p}`); 116 | if (fs.lstatSync(p).isDirectory()) { 117 | fs.rmSync(p, { recursive: true, force: true }); 118 | } else { 119 | fs.rmSync(p, { force: true }); 120 | } 121 | } 122 | } 123 | 124 | console.log('Clean completed successfully'); 125 | console.log(''); 126 | console.log('NOTE: Run "node scripts/index.js css" before starting development'); 127 | return true; 128 | } 129 | 130 | async function cleanAll() { 131 | // First clean build artifacts 132 | await cleanBuildArtifacts(); 133 | 134 | console.log('Running comprehensive cleanup...'); 135 | 136 | // Clean node_modules 137 | const nodeModules = path.join(ROOT_DIR, 'node_modules'); 138 | if (fs.existsSync(nodeModules)) { 139 | console.log('Removing node_modules...'); 140 | fs.rmSync(nodeModules, { recursive: true, force: true }); 141 | } 142 | 143 | // Additional paths to clean 144 | const additionalPaths = [ 145 | path.join(ROOT_DIR, 'build'), 146 | path.join(ROOT_DIR, 'coverage'), 147 | path.join(ROOT_DIR, '.nyc_output'), 148 | path.join(ROOT_DIR, '.tmp'), 149 | path.join(ROOT_DIR, 'temp'), 150 | ]; 151 | 152 | // Clean additional paths 153 | for (const p of additionalPaths) { 154 | if (fs.existsSync(p)) { 155 | console.log(`Removing: ${p}`); 156 | fs.rmSync(p, { recursive: true, force: true }); 157 | } 158 | } 159 | 160 | // Clean logs and cache files 161 | const patterns = ['npm-debug.log*', 'yarn-debug.log*', 'yarn-error.log*']; 162 | 163 | for (const pattern of patterns) { 164 | const files = fs 165 | .readdirSync(ROOT_DIR) 166 | .filter((file) => { 167 | const regex = new RegExp(pattern.replace('*', '.*')); 168 | return regex.test(file); 169 | }) 170 | .map((file) => path.join(ROOT_DIR, file)); 171 | 172 | for (const file of files) { 173 | console.log(`Removing: ${file}`); 174 | fs.rmSync(file, { force: true }); 175 | } 176 | } 177 | 178 | // Clean npm cache 179 | try { 180 | console.log('Cleaning npm cache...'); 181 | execSync('npm cache clean --force', { stdio: 'inherit', cwd: ROOT_DIR }); 182 | } catch (error) { 183 | console.error(`Warning: Failed to clean npm cache: ${error.message}`); 184 | } 185 | 186 | console.log('Comprehensive cleanup completed successfully'); 187 | console.log(''); 188 | console.log( 189 | 'NOTE: Run "node scripts/index.js setup" to reinstall dependencies and rebuild the project' 190 | ); 191 | return true; 192 | } 193 | 194 | // Help output 195 | function printHelp() { 196 | console.log('AI Code Fusion - Build System'); 197 | console.log(''); 198 | console.log('Usage: node scripts/index.js [args...]'); 199 | console.log(''); 200 | console.log('Development Commands:'); 201 | console.log(' dev, start - Start development server'); 202 | console.log(''); 203 | console.log('Build Commands:'); 204 | console.log(' build - Build for current platform'); 205 | console.log(' build-win - Build for Windows'); 206 | console.log(' build-linux - Build for Linux'); 207 | console.log(' build-mac - Build for macOS (Intel)'); 208 | console.log(' build-mac-arm - Build for macOS (Apple Silicon)'); 209 | console.log(' build-mac-universal - Build for macOS (Universal Binary)'); 210 | console.log(''); 211 | console.log('Setup & Maintenance:'); 212 | console.log(' setup, init - Setup the project, install dependencies'); 213 | console.log(' clean - Clean build outputs'); 214 | console.log(' clean-all - Full project cleanup'); 215 | console.log(' hooks - Setup Git hooks'); 216 | console.log(''); 217 | console.log('Asset Commands:'); 218 | console.log(' css - Build CSS'); 219 | console.log(' css-watch - Watch CSS files for changes'); 220 | console.log(' icons - Generate application icons'); 221 | console.log(''); 222 | console.log('Testing & Quality:'); 223 | console.log(' test - Run tests'); 224 | console.log(' test:watch - Run tests in watch mode'); 225 | console.log(' lint - Run linter'); 226 | console.log(' format - Format code'); 227 | console.log(' validate - Run all code quality checks'); 228 | console.log(' sonar - Run SonarQube analysis'); 229 | console.log(''); 230 | console.log('Release:'); 231 | console.log(' release - Prepare a release (version, changelog, git tag)'); 232 | console.log(''); 233 | console.log('Other:'); 234 | console.log(' run 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/renderer/index.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | 3 | /** 4 | * @license React 5 | * react-dom.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * @license React 15 | * react-jsx-runtime.production.min.js 16 | * 17 | * Copyright (c) Facebook, Inc. and its affiliates. 18 | * 19 | * This source code is licensed under the MIT license found in the 20 | * LICENSE file in the root directory of this source tree. 21 | */ 22 | 23 | /** 24 | * @license React 25 | * react.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | /** 34 | * @license React 35 | * scheduler.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | -------------------------------------------------------------------------------- /src/renderer/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .file-tree { 6 | font-family: 7 | ui-sans-serif, 8 | system-ui, 9 | -apple-system, 10 | BlinkMacSystemFont, 11 | sans-serif; 12 | } 13 | 14 | .file-tree input[type='checkbox'] { 15 | cursor: pointer; 16 | } 17 | 18 | /* Animated transitions for folder open/close */ 19 | .transition-all { 20 | transition-property: all; 21 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 22 | } 23 | 24 | .tab-content { 25 | min-height: 500px; 26 | } 27 | 28 | /* Folder and file icons styling */ 29 | .folder-icon, 30 | .file-icon { 31 | display: inline-flex; 32 | align-items: center; 33 | justify-content: center; 34 | } 35 | 36 | /* Tree lines for better hierarchy visualization */ 37 | .tree-line { 38 | position: relative; 39 | } 40 | 41 | .tree-line::before { 42 | content: ''; 43 | position: absolute; 44 | left: -16px; 45 | top: 0; 46 | bottom: 0; 47 | width: 1px; 48 | background-color: #e5e7eb; 49 | } 50 | 51 | .dark .tree-line::before { 52 | background-color: #4b5563; 53 | } 54 | 55 | .tree-line::after { 56 | content: ''; 57 | position: absolute; 58 | left: -16px; 59 | top: 10px; 60 | width: 12px; 61 | height: 1px; 62 | background-color: #e5e7eb; 63 | } 64 | 65 | .dark .tree-line::after { 66 | background-color: #4b5563; 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/config-manager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const yaml = require('yaml'); 4 | 5 | // Default config path 6 | const DEFAULT_CONFIG_PATH = path.join(__dirname, 'config.default.yaml'); 7 | 8 | /** 9 | * Load the default configuration 10 | * @returns {string} The default configuration as a YAML string 11 | */ 12 | function loadDefaultConfig() { 13 | try { 14 | return fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf8'); 15 | } catch (error) { 16 | console.error('Error loading default config:', error); 17 | return '{}'; // Return empty config object as fallback 18 | } 19 | } 20 | 21 | /** 22 | * Get default config as object 23 | * @returns {object} The default configuration as a JavaScript object 24 | */ 25 | function getDefaultConfigObject() { 26 | try { 27 | const configYaml = loadDefaultConfig(); 28 | return yaml.parse(configYaml); 29 | } catch (error) { 30 | console.error('Error parsing default config:', error); 31 | return {}; 32 | } 33 | } 34 | 35 | module.exports = { 36 | loadDefaultConfig, 37 | getDefaultConfigObject, 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/config.default.yaml: -------------------------------------------------------------------------------- 1 | # Filtering options 2 | use_custom_excludes: true 3 | use_custom_includes: false 4 | use_gitignore: true 5 | include_tree_view: true 6 | show_token_count: true 7 | 8 | # File extensions to include (with dot) 9 | include_extensions: 10 | - .py 11 | - .ts 12 | - .js 13 | - .jsx 14 | - .tsx 15 | - .json 16 | - .md 17 | - .txt 18 | - .html 19 | - .css 20 | - .scss 21 | - .less 22 | - .ini 23 | - .yaml 24 | - .yml 25 | - .kt 26 | - .java 27 | - .go 28 | - .scm 29 | - .php 30 | - .rb 31 | - .c 32 | - .cpp 33 | - .h 34 | - .cs 35 | - .sql 36 | - .sh 37 | - .bat 38 | - .ps1 39 | - .xml 40 | - .config 41 | 42 | # Patterns to exclude (using fnmatch syntax) 43 | exclude_patterns: 44 | # Version Control 45 | - ".git" 46 | - "**/.git/**" 47 | - "**/.git{,/**}" 48 | - "**/.svn/**" 49 | - "**/.hg/**" 50 | - "**/vocab.txt" 51 | - "**.onnx" 52 | - "**/test*.py" 53 | 54 | # Dependencies 55 | - "**/node_modules/**" 56 | - "**/venv/**" 57 | - "**/env/**" 58 | - "**/.venv/**" 59 | - "**/.github/**" 60 | - "**/vendor/**" 61 | - "**/website/**" 62 | 63 | # Build outputs 64 | - "**/dist/**" 65 | - "**/build/**" 66 | - "**/__pycache__/**" 67 | - "**/*.pyc" 68 | - "**/bundle.js" 69 | - "**/bundle.js.map" 70 | - "**/bundle.js.LICENSE.txt" 71 | - "**/index.js.map" 72 | - "**/output.css" 73 | 74 | # Config files 75 | - "**/.DS_Store" 76 | - "**/.env" 77 | - "**/package-lock.json" 78 | - "**/yarn.lock" 79 | - "**/.prettierrc" 80 | - "**/.prettierignore" 81 | - "**/.gitignore" 82 | - "**/.gitattributes" 83 | - "**/.npmrc" 84 | 85 | # Documentation 86 | - "**/LICENSE*" 87 | - "**/LICENSE.*" 88 | - "**/COPYING" 89 | - "**/CODE_OF**" 90 | - "**/CONTRIBUTING**" 91 | -------------------------------------------------------------------------------- /src/utils/content-processor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { isBinaryFile } = require('./file-analyzer'); 4 | 5 | class ContentProcessor { 6 | constructor(tokenCounter) { 7 | this.tokenCounter = tokenCounter; 8 | } 9 | 10 | processFile(filePath, relativePath) { 11 | try { 12 | // For binary files, show a note instead of content 13 | if (isBinaryFile(filePath)) { 14 | console.log(`Binary file detected during processing: ${filePath}`); 15 | 16 | // Get file stats for size info 17 | const stats = fs.statSync(filePath); 18 | const fileSizeInKB = (stats.size / 1024).toFixed(2); 19 | 20 | const headerContent = `${relativePath} (binary file)`; 21 | 22 | const formattedContent = 23 | `######\n` + 24 | `${headerContent}\n` + 25 | `######\n\n` + 26 | `[BINARY FILE]\n` + 27 | `File Type: ${path.extname(filePath).replace('.', '').toUpperCase()}\n` + 28 | `Size: ${fileSizeInKB} KB\n\n` + 29 | `Note: Binary files are included in the file tree but not processed for content.\n\n`; 30 | 31 | return formattedContent; 32 | } 33 | 34 | // Always read the file fresh from disk to ensure we have the latest content 35 | // This is important for the refresh functionality 36 | console.log(`Reading fresh content from: ${filePath}`); 37 | const content = fs.readFileSync(filePath, { encoding: 'utf-8', flag: 'r' }); 38 | 39 | // Always use just the path without token count 40 | const headerContent = `${relativePath}`; 41 | 42 | const formattedContent = 43 | `######\n` + `${headerContent}\n` + `######\n\n` + `\`\`\`\n${content}\n\`\`\`\n\n`; 44 | 45 | return formattedContent; 46 | } catch (error) { 47 | console.error(`Error processing file ${filePath}:`, error); 48 | return null; 49 | } 50 | } 51 | 52 | readAnalysisFile(analysisPath) { 53 | const filesToProcess = []; 54 | 55 | try { 56 | const content = fs.readFileSync(analysisPath, { encoding: 'utf-8', flag: 'r' }); 57 | const lines = content.split('\n').map((line) => line.trim()); 58 | 59 | // Process pairs of lines (path and token count) 60 | for (let i = 0; i < lines.length - 1; i += 2) { 61 | if (i + 1 >= lines.length) { 62 | break; 63 | } 64 | 65 | const path = lines[i].trim(); 66 | if (path.startsWith('Total tokens:')) { 67 | break; 68 | } 69 | 70 | try { 71 | const tokens = parseInt(lines[i + 1].trim()); 72 | // Skip entries with invalid token counts (NaN) 73 | if (isNaN(tokens)) { 74 | console.warn(`Skipping entry with invalid token count: ${path}`); 75 | continue; 76 | } 77 | // Clean up the path 78 | const cleanPath = path.replace(/\\/g, '/').trim(); 79 | filesToProcess.push({ path: cleanPath, tokens }); 80 | } catch (error) { 81 | console.warn(`Failed to parse line ${i}:`, error); 82 | continue; 83 | } 84 | } 85 | } catch (error) { 86 | console.error(`Error reading analysis file ${analysisPath}:`, error); 87 | } 88 | 89 | return filesToProcess; 90 | } 91 | } 92 | 93 | module.exports = { ContentProcessor }; 94 | -------------------------------------------------------------------------------- /src/utils/file-analyzer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | // eslint-disable-next-line no-unused-vars 3 | const path = require('path'); 4 | const filterUtils = require('./filter-utils'); 5 | 6 | // Helper function to check if a file is a binary file by examining content 7 | const isBinaryFile = (filePath) => { 8 | try { 9 | // Read the first 4KB of the file to check for binary content 10 | const buffer = Buffer.alloc(4096); 11 | const fd = fs.openSync(filePath, 'r'); 12 | const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0); 13 | fs.closeSync(fd); 14 | 15 | if (bytesRead === 0) { 16 | // Empty file, consider it text 17 | return false; 18 | } 19 | 20 | // Check for NULL bytes and control characters (except common whitespace controls) 21 | // This is a reliable indicator of binary content 22 | let controlChars = 0; 23 | const totalBytes = bytesRead; 24 | 25 | for (let i = 0; i < bytesRead; i++) { 26 | // NULL byte check 27 | if (buffer[i] === 0) { 28 | return true; // Null bytes are a clear sign of binary content 29 | } 30 | 31 | // Control character check (except tab, newline, carriage return) 32 | if (buffer[i] < 32 && buffer[i] !== 9 && buffer[i] !== 10 && buffer[i] !== 13) { 33 | controlChars++; 34 | } 35 | } 36 | 37 | // If more than 10% of the file consists of control characters, consider it binary 38 | const ratio = controlChars / totalBytes; 39 | return ratio > 0.1; 40 | } catch (error) { 41 | console.error(`Error checking if file is binary: ${filePath}`, error); 42 | // If we can't read the file, safer to consider it binary 43 | return true; 44 | } 45 | }; 46 | 47 | class FileAnalyzer { 48 | constructor(config, tokenCounter, options = {}) { 49 | this.config = config; 50 | this.tokenCounter = tokenCounter; 51 | this.useGitignore = options.useGitignore || false; 52 | this.gitignorePatterns = options.gitignorePatterns || { 53 | excludePatterns: [], 54 | includePatterns: [], 55 | }; 56 | } 57 | 58 | shouldProcessFile(filePath) { 59 | // Convert path to forward slashes for consistent pattern matching 60 | const normalizedPath = filePath.replace(/\\/g, '/'); 61 | const ext = path.extname(filePath); 62 | 63 | // Explicit check for node_modules 64 | if (normalizedPath.split('/').includes('node_modules')) { 65 | return false; 66 | } 67 | 68 | // 1. Extension filtering - apply unless explicitly disabled 69 | if ( 70 | this.config.use_custom_includes !== false && 71 | this.config.include_extensions && 72 | Array.isArray(this.config.include_extensions) && 73 | ext 74 | ) { 75 | if (!this.config.include_extensions.includes(ext.toLowerCase())) { 76 | return false; // Exclude files with extensions not in the include list 77 | } 78 | } 79 | 80 | // 2. Build patterns array with proper structure and priority 81 | const patterns = []; 82 | 83 | // Add custom exclude patterns (highest priority) 84 | if ( 85 | this.config.use_custom_excludes !== false && 86 | this.config.exclude_patterns && 87 | Array.isArray(this.config.exclude_patterns) 88 | ) { 89 | patterns.push(...this.config.exclude_patterns); 90 | } 91 | 92 | // Add gitignore exclude patterns 93 | if (this.useGitignore && this.gitignorePatterns && this.gitignorePatterns.excludePatterns) { 94 | patterns.push(...this.gitignorePatterns.excludePatterns); 95 | } 96 | 97 | // Add include patterns property for gitignore negated patterns 98 | if (this.useGitignore && this.gitignorePatterns && this.gitignorePatterns.includePatterns) { 99 | patterns.includePatterns = this.gitignorePatterns.includePatterns; 100 | } 101 | 102 | // 3. Use the shouldExclude utility for consistent pattern matching 103 | if (filterUtils.shouldExclude(filePath, '', patterns, this.config)) { 104 | return false; // File should be excluded based on pattern matching 105 | } 106 | 107 | // If we reach this point, the file should be processed 108 | return true; 109 | } 110 | 111 | analyzeFile(filePath) { 112 | try { 113 | // Skip binary files completely 114 | if (isBinaryFile(filePath)) { 115 | console.log(`Skipping binary file: ${filePath}`); 116 | return null; 117 | } 118 | 119 | // Process text files only 120 | const content = fs.readFileSync(filePath, { encoding: 'utf-8', flag: 'r' }); 121 | return this.tokenCounter.countTokens(content); 122 | } catch (error) { 123 | console.error(`Error analyzing file ${filePath}:`, error); 124 | return null; 125 | } 126 | } 127 | 128 | createAnalysis() { 129 | const totalTokens = 0; 130 | const filesInfo = []; 131 | 132 | // Implement the rest of the analysis logic if needed 133 | // This is handled by the main process in this implementation 134 | 135 | return { 136 | filesInfo, 137 | totalTokens, 138 | }; 139 | } 140 | 141 | // Additional method to check if file should be processed before reading it 142 | shouldReadFile(filePath) { 143 | // Skip binary files completely 144 | return !isBinaryFile(filePath); 145 | } 146 | } 147 | 148 | module.exports = { FileAnalyzer, isBinaryFile }; 149 | -------------------------------------------------------------------------------- /src/utils/filter-utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fnmatch = require('./fnmatch'); 3 | 4 | /** 5 | * Utility functions for consistently handling file filtering across the application 6 | */ 7 | 8 | /** 9 | * Normalize path by converting backslashes to forward slashes 10 | * @param {string} inputPath - The path to normalize 11 | * @returns {string} - Normalized path 12 | */ 13 | const normalizePath = (inputPath) => { 14 | return inputPath.replace(/\\/g, '/'); 15 | }; 16 | 17 | /** 18 | * Get a path relative to a root directory with consistent normalization 19 | * @param {string} filePath - The absolute file path 20 | * @param {string} rootPath - The root directory path 21 | * @returns {string} - Normalized relative path 22 | */ 23 | const getRelativePath = (filePath, rootPath) => { 24 | const relativePath = path.relative(rootPath, filePath); 25 | return normalizePath(relativePath); 26 | }; 27 | 28 | /** 29 | * Check if a file should be excluded by extension 30 | * @param {string} itemPath - File path to check 31 | * @param {Object} config - Configuration object 32 | * @returns {boolean} - True if file should be excluded by extension 33 | */ 34 | const shouldExcludeByExtension = (itemPath, config) => { 35 | if ( 36 | config?.use_custom_includes && 37 | config?.include_extensions && 38 | Array.isArray(config.include_extensions) && 39 | path.extname(itemPath) 40 | ) { 41 | const ext = path.extname(itemPath).toLowerCase(); 42 | // If extension is not in the include list, exclude it 43 | return !config.include_extensions.includes(ext); 44 | } 45 | return false; 46 | }; 47 | 48 | /** 49 | * Check if a file matches include patterns 50 | * @param {string} normalizedPath - Normalized file path 51 | * @param {string} itemName - File name 52 | * @param {Array} includePatterns - Include patterns to check 53 | * @returns {boolean} - True if file matches any include pattern 54 | */ 55 | const matchesIncludePatterns = (normalizedPath, itemName, includePatterns) => { 56 | if (Array.isArray(includePatterns) && includePatterns.length > 0) { 57 | for (const pattern of includePatterns) { 58 | // Check both the full path and just the filename for simple patterns 59 | if ( 60 | fnmatch.fnmatch(normalizedPath, pattern) || 61 | (!pattern.includes('/') && fnmatch.fnmatch(itemName, pattern)) 62 | ) { 63 | // Matches an include pattern 64 | return true; 65 | } 66 | } 67 | } 68 | return false; 69 | }; 70 | 71 | /** 72 | * Check if a file matches exclude patterns 73 | * @param {string} normalizedPath - Normalized file path 74 | * @param {string} itemName - File name 75 | * @param {Array} excludePatterns - Exclude patterns to check 76 | * @returns {boolean} - True if file matches any exclude pattern 77 | */ 78 | const matchesExcludePatterns = (normalizedPath, itemName, excludePatterns) => 79 | Array.isArray(excludePatterns) && 80 | excludePatterns.length > 0 && 81 | excludePatterns.some( 82 | (pattern) => 83 | fnmatch.fnmatch(normalizedPath, pattern) || 84 | (!pattern.includes('/') && fnmatch.fnmatch(itemName, pattern)) 85 | ); 86 | 87 | /** 88 | * Check if a file should be excluded based on patterns and configuration 89 | * This is the shared implementation used by both main process and file analyzer 90 | * @param {string} itemPath - The file path to check 91 | * @param {string} rootPath - The root directory path 92 | * @param {Array} excludePatterns - Patterns to exclude 93 | * @param {Object} config - Configuration object 94 | * @returns {boolean} - True if the file should be excluded 95 | */ 96 | const shouldExclude = (itemPath, rootPath, excludePatterns, config) => { 97 | try { 98 | const itemName = path.basename(itemPath); 99 | const normalizedPath = getRelativePath(itemPath, rootPath); 100 | 101 | // 1. Check extension exclusion first 102 | if (shouldExcludeByExtension(itemPath, config)) { 103 | return true; // Excluded by extension 104 | } 105 | 106 | // 2. Process custom exclude patterns (highest priority) 107 | if (config?.use_custom_excludes === true && config?.exclude_patterns) { 108 | const customExcludes = Array.isArray(config.exclude_patterns) ? config.exclude_patterns : []; 109 | 110 | if ( 111 | customExcludes.length > 0 && 112 | matchesExcludePatterns(normalizedPath, itemName, customExcludes) 113 | ) { 114 | return true; // Excluded by custom pattern 115 | } 116 | } 117 | 118 | // 3. Process gitignore patterns if enabled 119 | if (config?.use_gitignore !== false) { 120 | // First check gitignore include patterns (negated patterns with ! prefix) 121 | const gitignoreIncludes = excludePatterns?.includePatterns || []; 122 | 123 | if ( 124 | gitignoreIncludes.length > 0 && 125 | matchesIncludePatterns(normalizedPath, itemName, gitignoreIncludes) 126 | ) { 127 | return false; // Explicitly included by gitignore negated pattern 128 | } 129 | 130 | // Then check regular gitignore exclude patterns 131 | const gitignoreExcludes = Array.isArray(excludePatterns) 132 | ? excludePatterns.filter( 133 | (pattern) => 134 | // Filter out patterns that are already in custom excludes 135 | !(config?.exclude_patterns || []).includes(pattern) 136 | ) 137 | : []; 138 | 139 | if ( 140 | gitignoreExcludes.length > 0 && 141 | matchesExcludePatterns(normalizedPath, itemName, gitignoreExcludes) 142 | ) { 143 | return true; // Excluded by gitignore pattern 144 | } 145 | } 146 | 147 | // 4. Default: not excluded 148 | return false; 149 | } catch (error) { 150 | console.error(`Error in shouldExclude for ${itemPath}:`, error); 151 | return false; // Default to including if there's an error 152 | } 153 | }; 154 | 155 | module.exports = { 156 | normalizePath, 157 | getRelativePath, 158 | shouldExclude, 159 | }; 160 | -------------------------------------------------------------------------------- /src/utils/fnmatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fnmatch compatibility layer using minimatch 3 | * This is the centralized pattern matching utility for the application 4 | */ 5 | 6 | // Import minimatch properly 7 | const Minimatch = require('minimatch').Minimatch; 8 | 9 | const fnmatch = { 10 | /** 11 | * Match a filepath against a pattern using minimatch 12 | * @param {string} filepath - The path to check 13 | * @param {string} pattern - The pattern to match against 14 | * @returns {boolean} - Whether the path matches the pattern 15 | */ 16 | fnmatch: (filepath, pattern) => { 17 | try { 18 | // Consistent options for all pattern matching throughout the app 19 | const mm = new Minimatch(pattern, { 20 | dot: true, // Match dotfiles 21 | matchBase: true, // Match basename if pattern has no slashes 22 | nocomment: true, // Disable comments in patterns 23 | nobrace: false, // Enable brace expansion 24 | noext: false, // Enable extglob features 25 | }); 26 | return mm.match(filepath); 27 | } catch (error) { 28 | console.error(`Error matching pattern ${pattern} against ${filepath}:`, error); 29 | // We never use FALLBACK, return the error 30 | return false; 31 | } 32 | }, 33 | }; 34 | 35 | module.exports = fnmatch; 36 | -------------------------------------------------------------------------------- /src/utils/formatters/list-formatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for converting between YAML and plain text formats for 3 | * include_extensions and exclude_patterns in the config 4 | */ 5 | 6 | /** 7 | * Converts a YAML array section to plain text format (one item per line) 8 | * 9 | * @param {Array} arrayItems - Array of items from the YAML config 10 | * @returns {string} Plain text representation with one item per line 11 | */ 12 | export function yamlArrayToPlainText(arrayItems) { 13 | if (!arrayItems || !Array.isArray(arrayItems)) { 14 | return ''; 15 | } 16 | 17 | // Process each item to remove quotes and trim 18 | const cleanedItems = arrayItems.map((item) => { 19 | // Remove surrounding quotes if present 20 | let cleanItem = item.toString(); 21 | if ( 22 | (cleanItem.startsWith('"') && cleanItem.endsWith('"')) || 23 | (cleanItem.startsWith("'") && cleanItem.endsWith("'")) 24 | ) { 25 | cleanItem = cleanItem.substring(1, cleanItem.length - 1); 26 | } 27 | return cleanItem.trim(); 28 | }); 29 | 30 | // Join processed items with newlines 31 | return cleanedItems.join('\n'); 32 | } 33 | 34 | /** 35 | * Converts plain text (one item per line) to an array for YAML 36 | * 37 | * @param {string} plainText - Text with one item per line 38 | * @returns {Array} Array of items for YAML config 39 | */ 40 | export function plainTextToYamlArray(plainText) { 41 | if (!plainText) { 42 | return []; 43 | } 44 | 45 | // Split by newlines, trim, and filter empty lines 46 | return plainText 47 | .split('\n') 48 | .map((line) => { 49 | // Clean each line by removing excess spaces and quotes 50 | let cleanLine = line.trim(); 51 | // Only remove quotes that are at both the beginning and end 52 | if ( 53 | (cleanLine.startsWith('"') && cleanLine.endsWith('"')) || 54 | (cleanLine.startsWith("'") && cleanLine.endsWith("'")) 55 | ) { 56 | cleanLine = cleanLine.substring(1, cleanLine.length - 1); 57 | } 58 | return cleanLine.trim(); 59 | }) 60 | .filter((line) => line.length > 0); 61 | } 62 | 63 | /** 64 | * Extracts array items from YAML format 65 | * 66 | * @param {string} yamlContent - YAML content containing a list 67 | * @param {string} arrayKey - The key for the array in the YAML object 68 | * @returns {Array} Array of items extracted from the YAML 69 | */ 70 | export function extractArrayFromYaml(yamlContent, arrayKey) { 71 | // Look for the array key and extract items 72 | const lines = yamlContent.split('\n'); 73 | let items = []; 74 | let inArray = false; 75 | 76 | for (const line of lines) { 77 | // Check if we're starting the array section 78 | if (line.trim().startsWith(arrayKey + ':')) { 79 | inArray = true; 80 | continue; 81 | } 82 | 83 | // If we're in the array section, extract items 84 | if (inArray && line.trim().startsWith('-')) { 85 | const item = line.replace('-', '').trim(); 86 | items.push(item); 87 | } 88 | 89 | // If we've moved to a new section (not indented), stop processing 90 | if (inArray && line.trim() !== '' && !line.trim().startsWith('-') && !line.startsWith(' ')) { 91 | break; 92 | } 93 | } 94 | 95 | return items; 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/gitignore-parser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * A utility class for parsing and applying gitignore rules. 6 | */ 7 | class GitignoreParser { 8 | constructor() { 9 | this.cache = new Map(); 10 | } 11 | 12 | /** 13 | * Clear the gitignore patterns cache 14 | */ 15 | clearCache() { 16 | this.cache.clear(); 17 | } 18 | 19 | /** 20 | * Parse a .gitignore file and return patterns organized by type. 21 | * @param {string} rootPath - The root path of the repository 22 | * @returns {Object} - Object with include and exclude patterns 23 | */ 24 | parseGitignore(rootPath) { 25 | // Check if we have a cached result for this root path 26 | if (this.cache.has(rootPath)) { 27 | return this.cache.get(rootPath); 28 | } 29 | 30 | const gitignorePath = path.join(rootPath, '.gitignore'); 31 | 32 | // Default result with empty pattern arrays 33 | const defaultResult = { 34 | excludePatterns: [], 35 | includePatterns: [], 36 | }; 37 | 38 | // Check if .gitignore exists 39 | if (!fs.existsSync(gitignorePath)) { 40 | // No gitignore file, cache default result 41 | this.cache.set(rootPath, defaultResult); 42 | return defaultResult; 43 | } 44 | 45 | try { 46 | // Read and parse .gitignore file 47 | const content = fs.readFileSync(gitignorePath, 'utf8'); 48 | const patterns = this._parseGitignoreContent(content); 49 | 50 | // Cache the parsed patterns 51 | this.cache.set(rootPath, patterns); 52 | return patterns; 53 | } catch (error) { 54 | console.error('Error parsing .gitignore:', error); 55 | // Cache default result on error 56 | this.cache.set(rootPath, defaultResult); 57 | return defaultResult; 58 | } 59 | } 60 | 61 | // Helper methods for _parseGitignoreContent 62 | _addPattern(result, pattern, isNegated) { 63 | if (!pattern) return; 64 | if (isNegated) { 65 | result.includePatterns.push(pattern); 66 | } else { 67 | result.excludePatterns.push(pattern); 68 | } 69 | } 70 | 71 | _processSimplePattern(result, pattern, isNegated) { 72 | // Simple pattern like *.log or node_modules 73 | const isDir = pattern.endsWith('/'); 74 | const rootPattern = pattern; 75 | const subdirPattern = `**/${pattern}`; 76 | 77 | this._addPattern(result, rootPattern, isNegated); 78 | this._addPattern(result, subdirPattern, isNegated); 79 | 80 | if (isDir) { 81 | this._addPattern(result, `${pattern}**`, isNegated); 82 | } 83 | } 84 | 85 | _processPathPattern(result, pattern, isNegated) { 86 | if (pattern.startsWith('/')) { 87 | // Remove leading slash 88 | pattern = pattern.substring(1); 89 | 90 | if (pattern.endsWith('/')) { 91 | pattern = `${pattern}**`; 92 | } 93 | 94 | this._addPattern(result, pattern, isNegated); 95 | } else if (!pattern.includes('*')) { 96 | // Pattern without leading slash and without wildcards 97 | const rootPattern = pattern; 98 | const subdirPattern = `**/${pattern}`; 99 | 100 | this._addPattern(result, rootPattern, isNegated); 101 | this._addPattern(result, subdirPattern, isNegated); 102 | } else { 103 | // Pattern with wildcards and path separators, but not starting with / 104 | this._addPattern(result, pattern, isNegated); 105 | 106 | // Also add the recursive version for patterns with path separators 107 | if (pattern.includes('/')) { 108 | this._addPattern(result, `**/${pattern}`, isNegated); 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Parse gitignore content and extract valid patterns 115 | * @param {string} content - The content of the .gitignore file 116 | * @returns {Object} - Object with include and exclude patterns 117 | */ 118 | _parseGitignoreContent(content) { 119 | const result = { 120 | excludePatterns: [], 121 | includePatterns: [], 122 | }; 123 | 124 | const lines = content.split('\n'); 125 | 126 | for (const line of lines) { 127 | const trimmedLine = line.trim(); 128 | 129 | // Skip empty lines and comments 130 | if (!trimmedLine || trimmedLine.startsWith('#')) continue; 131 | 132 | // Handle negated patterns 133 | const isNegated = trimmedLine.startsWith('!'); 134 | let pattern = isNegated ? trimmedLine.substring(1).trim() : trimmedLine; 135 | 136 | // Skip if pattern is empty after processing 137 | if (!pattern) continue; 138 | 139 | // Process pattern based on whether it includes a path separator 140 | if (pattern.includes('/')) { 141 | this._processPathPattern(result, pattern, isNegated); 142 | } else { 143 | this._processSimplePattern(result, pattern, isNegated); 144 | } 145 | } 146 | 147 | // Add common build artifacts 148 | const buildArtifacts = [ 149 | '**/bundle.js', 150 | '**/bundle.js.map', 151 | '**/bundle.js.LICENSE.txt', 152 | '**/index.js.map', 153 | '**/output.css', 154 | ]; 155 | 156 | for (const artifact of buildArtifacts) { 157 | result.excludePatterns.push(artifact); 158 | } 159 | 160 | return result; 161 | } 162 | } 163 | 164 | module.exports = { GitignoreParser }; 165 | -------------------------------------------------------------------------------- /src/utils/token-counter.js: -------------------------------------------------------------------------------- 1 | const tiktoken = require('tiktoken'); 2 | 3 | class TokenCounter { 4 | constructor(modelName = 'gpt-4') { 5 | try { 6 | this.encoder = tiktoken.encoding_for_model(modelName); 7 | } catch (error) { 8 | console.error(`Error initializing tiktoken for model ${modelName}:`, error); 9 | // Fallback to a simple approximation if tiktoken fails 10 | this.encoder = null; 11 | } 12 | } 13 | 14 | countTokens(text) { 15 | try { 16 | // Handle null or undefined text input 17 | if (text === null || text === undefined) { 18 | return 0; 19 | } 20 | 21 | // Convert to string just in case 22 | const textStr = String(text); 23 | 24 | if (this.encoder) { 25 | return this.encoder.encode(textStr).length; 26 | } else { 27 | // Very rough approximation: ~4 chars per token 28 | return Math.ceil(textStr.length / 4); 29 | } 30 | } catch (error) { 31 | console.error('Error counting tokens:', error); 32 | // No fallback here; just return 0 33 | return 0; 34 | } 35 | } 36 | } 37 | 38 | module.exports = { TokenCounter }; 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/renderer/**/*.{js,jsx}', './src/renderer/index.html'], 3 | darkMode: 'class', 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | node: true, 5 | }, 6 | extends: ['../.eslintrc.js'], 7 | // Add specific rules for test files 8 | rules: { 9 | // Allow console statements in tests for debugging 10 | 'no-console': 'off', 11 | // Allow expects in tests 12 | 'jest/valid-expect': 'off', 13 | // Add more test-specific rules as needed 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/fixtures/configs/default.yaml: -------------------------------------------------------------------------------- 1 | # Default test configuration 2 | use_custom_excludes: true 3 | use_custom_includes: false 4 | use_gitignore: true 5 | include_tree_view: true 6 | show_token_count: true 7 | 8 | # File extensions to include (with dot) 9 | include_extensions: 10 | - .js 11 | - .jsx 12 | - .json 13 | - .md 14 | - .txt 15 | 16 | # Patterns to exclude (using fnmatch syntax) 17 | exclude_patterns: 18 | # Version Control 19 | - ".git" 20 | - "**/.git/**" 21 | - "**/.git{,/**}" 22 | - "**/.svn/**" 23 | 24 | # Dependencies 25 | - "**/node_modules/**" 26 | - "**/venv/**" 27 | 28 | # Build outputs 29 | - "**/dist/**" 30 | - "**/build/**" 31 | - "**/__pycache__/**" 32 | -------------------------------------------------------------------------------- /tests/fixtures/configs/minimal.yaml: -------------------------------------------------------------------------------- 1 | # Minimal test configuration 2 | use_custom_excludes: false 3 | use_custom_includes: true 4 | use_gitignore: false 5 | 6 | # File extensions to include (with dot) 7 | include_extensions: 8 | - .js 9 | - .jsx 10 | -------------------------------------------------------------------------------- /tests/fixtures/files/.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore file for testing 2 | 3 | # Directories 4 | node_modules/ 5 | dist/ 6 | build/ 7 | coverage/ 8 | 9 | # Files 10 | *.log 11 | .env 12 | .DS_Store 13 | 14 | # Specific files 15 | config.local.js 16 | 17 | # Negated patterns (keep these files) 18 | !important.log 19 | !dist/keep-this-file.js 20 | -------------------------------------------------------------------------------- /tests/fixtures/files/sample.js: -------------------------------------------------------------------------------- 1 | // Sample JavaScript file for testing 2 | function hello() { 3 | console.log('Hello, World!'); 4 | } 5 | 6 | const add = (a, b) => a + b; 7 | 8 | module.exports = { 9 | hello, 10 | add, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/fixtures/files/sample.md: -------------------------------------------------------------------------------- 1 | # Sample Markdown 2 | 3 | This is a sample markdown file for testing. 4 | 5 | ## Features 6 | 7 | - Feature 1 8 | - Feature 2 9 | 10 | ## Code 11 | 12 | ```javascript 13 | const x = 10; 14 | console.log(x); 15 | ``` 16 | -------------------------------------------------------------------------------- /tests/fixtures/projects/mock-gitignore.txt: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build directories 5 | dist/ 6 | !dist/keep-this-file.js 7 | 8 | # Logs 9 | *.log 10 | !important.log 11 | 12 | # Other 13 | .DS_Store 14 | .env 15 | -------------------------------------------------------------------------------- /tests/fixtures/projects/mock-project-structure.txt: -------------------------------------------------------------------------------- 1 | /mock-project 2 | ├── src/ 3 | │ ├── index.js 4 | │ ├── utils/ 5 | │ │ └── helpers.js 6 | │ └── components/ 7 | │ └── App.jsx 8 | ├── node_modules/ 9 | │ └── react/ 10 | │ └── index.js 11 | ├── dist/ 12 | │ ├── bundle.js 13 | │ └── keep-this-file.js 14 | ├── public/ 15 | │ ├── index.html 16 | │ └── logo.png 17 | ├── package.json 18 | ├── .git/ 19 | │ └── index 20 | ├── .gitignore 21 | ├── README.md 22 | ├── debug.log 23 | └── important.log 24 | -------------------------------------------------------------------------------- /tests/mocks/yaml-mock.js: -------------------------------------------------------------------------------- 1 | // Mock implementation of yaml module for testing 2 | const yamlMock = { 3 | parse: jest.fn((yamlString) => { 4 | // Simple mock implementation 5 | // For testing, we'll handle some basic cases 6 | if (!yamlString || yamlString.trim() === '') { 7 | return {}; 8 | } 9 | 10 | // Return a mock object for testing 11 | if (yamlString.includes('include_extensions')) { 12 | return { 13 | include_extensions: ['.js', '.jsx'], 14 | use_custom_includes: true, 15 | use_gitignore: true, 16 | exclude_patterns: ['**/node_modules/**'], 17 | }; 18 | } 19 | 20 | return { 21 | default_value: 'mock_value', 22 | }; 23 | }), 24 | stringify: jest.fn((obj) => { 25 | // Simple stringification for testing 26 | return JSON.stringify(obj, null, 2).replace(/"/g, '').replace(/\{/g, '').replace(/\}/g, ''); 27 | }), 28 | }; 29 | 30 | module.exports = yamlMock; 31 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | // Add a dummy test to avoid Jest warning about no tests 4 | describe('Setup validation', () => { 5 | test('Jest is configured correctly', () => { 6 | expect(true).toBe(true); 7 | }); 8 | }); 9 | 10 | // Note: We need to be careful with mocking pattern matching utilities 11 | // as it can affect test reliability 12 | 13 | // Mock the tiktoken module 14 | jest.mock('tiktoken', () => ({ 15 | encoding_for_model: jest.fn().mockImplementation(() => ({ 16 | encode: jest.fn().mockImplementation(() => Array(10)), 17 | })), 18 | })); 19 | 20 | // Mock Electron's APIs 21 | window.electronAPI = { 22 | selectDirectory: jest.fn().mockResolvedValue('/mock/directory'), 23 | getDirectoryTree: jest.fn().mockResolvedValue([]), 24 | saveFile: jest.fn().mockResolvedValue('/mock/output.md'), 25 | resetGitignoreCache: jest.fn().mockResolvedValue(true), 26 | analyzeRepository: jest.fn().mockResolvedValue({ 27 | filesInfo: [], 28 | totalTokens: 0, 29 | }), 30 | processRepository: jest.fn().mockResolvedValue({ 31 | content: '', 32 | totalTokens: 0, 33 | processedFiles: 0, 34 | skippedFiles: 0, 35 | }), 36 | }; 37 | 38 | // Mock fs module functions that we use in various tests 39 | jest.mock('fs', () => ({ 40 | existsSync: jest.fn().mockReturnValue(true), 41 | readFileSync: jest.fn().mockReturnValue('mock content'), 42 | writeFileSync: jest.fn(), 43 | openSync: jest.fn().mockReturnValue(1), 44 | readSync: jest.fn().mockReturnValue(100), 45 | closeSync: jest.fn(), 46 | statSync: jest.fn().mockReturnValue({ 47 | size: 1024, 48 | mtime: new Date(), 49 | isDirectory: jest.fn().mockReturnValue(false), 50 | }), 51 | readdirSync: jest.fn().mockReturnValue([]), 52 | })); 53 | 54 | // Mock console methods to reduce test noise 55 | global.console = { 56 | ...console, 57 | error: jest.fn(), 58 | warn: jest.fn(), 59 | log: jest.fn(), 60 | }; 61 | -------------------------------------------------------------------------------- /tests/unit/binary-detection.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { isBinaryFile } = require('../../src/utils/file-analyzer'); 3 | 4 | // Mock fs module 5 | jest.mock('fs'); 6 | 7 | describe('Binary File Detection', () => { 8 | beforeEach(() => { 9 | jest.clearAllMocks(); 10 | }); 11 | 12 | // Helper function to create a Buffer with specific content 13 | const createMockBuffer = (content) => { 14 | if (typeof content === 'string') { 15 | return Buffer.from(content); 16 | } else { 17 | return Buffer.from(content); 18 | } 19 | }; 20 | 21 | // Function to create a binary buffer that will trigger detection 22 | const createBinaryBuffer = () => { 23 | const buffer = Buffer.alloc(4096); 24 | // Add NULL bytes and control characters to make it clearly binary 25 | for (let i = 0; i < buffer.length; i++) { 26 | buffer[i] = i % 32 === 0 ? 0 : i % 8; 27 | } 28 | return buffer; 29 | }; 30 | 31 | // Helper to mock the fs file reading operations 32 | const mockFileContent = (content) => { 33 | // Create a mock buffer 34 | const buffer = createMockBuffer(content); 35 | 36 | // Mock fs.openSync 37 | fs.openSync.mockReturnValue(1); // Return a mock file descriptor 38 | 39 | // Mock fs.readSync to copy our content into the buffer 40 | // Ignore position parameter by not naming it 41 | fs.readSync.mockImplementation((fd, buf, offset, length /* position */) => { 42 | const bytesToCopy = Math.min(buffer.length, length); 43 | buffer.copy(buf, offset, 0, bytesToCopy); 44 | return bytesToCopy; 45 | }); 46 | 47 | // Mock fs.closeSync 48 | fs.closeSync.mockReturnValue(undefined); 49 | }; 50 | 51 | test('should identify normal text files as non-binary', () => { 52 | // Mock a normal text file 53 | const textContent = 54 | 'This is a normal text file with some content.\nIt has multiple lines and normal characters.'; 55 | mockFileContent(textContent); 56 | 57 | expect(isBinaryFile('text-file.txt')).toBe(false); 58 | }); 59 | 60 | test('should identify files with NULL bytes as binary', () => { 61 | // Create a buffer with NULL bytes 62 | const binaryContent = Buffer.from([65, 66, 67, 0, 68, 69, 70]); // ABC\0DEF 63 | mockFileContent(binaryContent); 64 | 65 | expect(isBinaryFile('binary-with-null.bin')).toBe(true); 66 | }); 67 | 68 | test('should identify files with high concentration of control characters as binary', () => { 69 | // Create content with many control characters (not tab, newline, carriage return) 70 | const controlChars = []; 71 | for (let i = 0; i < 500; i++) { 72 | // Add some normal characters 73 | if (i % 4 === 0) { 74 | controlChars.push(65 + (i % 26)); // A-Z 75 | } else { 76 | // Add control characters (not 9, 10, 13 which are tab, newline, carriage return) 77 | controlChars.push((i % 8) + 1); // Control characters below 9 78 | } 79 | } 80 | 81 | mockFileContent(Buffer.from(controlChars)); 82 | 83 | expect(isBinaryFile('control-chars.bin')).toBe(true); 84 | }); 85 | 86 | test('should handle empty files as non-binary', () => { 87 | // Mock an empty file (0 bytes read) 88 | fs.openSync.mockReturnValue(1); 89 | fs.readSync.mockReturnValue(0); 90 | fs.closeSync.mockReturnValue(undefined); 91 | 92 | expect(isBinaryFile('empty-file.txt')).toBe(false); 93 | }); 94 | 95 | test('should identify image files as binary', () => { 96 | // Simplified PNG header 97 | const pngHeader = Buffer.from([ 98 | 0x89, 99 | 0x50, 100 | 0x4e, 101 | 0x47, 102 | 0x0d, 103 | 0x0a, 104 | 0x1a, 105 | 0x0a, // PNG signature 106 | 0x00, 107 | 0x00, 108 | 0x00, 109 | 0x0d, // Chunk length 110 | 0x49, 111 | 0x48, 112 | 0x44, 113 | 0x52, // "IHDR" chunk 114 | // Add some more bytes to make it long enough 115 | 0x00, 116 | 0x00, 117 | 0x01, 118 | 0x00, 119 | 0x00, 120 | 0x00, 121 | 0x01, 122 | 0x00, 123 | 0x08, 124 | 0x06, 125 | 0x00, 126 | 0x00, 127 | 0x00, 128 | ]); 129 | 130 | mockFileContent(pngHeader); 131 | 132 | expect(isBinaryFile('image.png')).toBe(true); 133 | }); 134 | 135 | test('should identify PDF files as binary', () => { 136 | // Create a buffer that will be detected as binary 137 | const binaryBuffer = createBinaryBuffer(); 138 | 139 | // Add PDF header at the beginning 140 | const pdfSignature = Buffer.from('%PDF-1.5\n%'); 141 | pdfSignature.copy(binaryBuffer, 0); 142 | 143 | mockFileContent(binaryBuffer); 144 | 145 | expect(isBinaryFile('document.pdf')).toBe(true); 146 | }); 147 | 148 | test('should identify executable files as binary', () => { 149 | // Mock a Windows PE executable header (MZ header) 150 | const exeHeader = Buffer.from([ 151 | 0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 152 | 0x00, 153 | // Add more binary data 154 | 0x0b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 155 | ]); 156 | 157 | mockFileContent(exeHeader); 158 | 159 | expect(isBinaryFile('program.exe')).toBe(true); 160 | }); 161 | 162 | test('should identify zip files as binary', () => { 163 | // Zip file header 164 | const zipHeader = Buffer.from([ 165 | 0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 166 | 0x00, 167 | ]); 168 | 169 | mockFileContent(zipHeader); 170 | 171 | expect(isBinaryFile('archive.zip')).toBe(true); 172 | }); 173 | 174 | test('should handle text files with low control character ratio as non-binary', () => { 175 | // Create a larger text file with some control characters, but below the threshold 176 | const textWithSomeControls = Buffer.alloc(4000); 177 | 178 | // Fill with normal text 179 | for (let i = 0; i < 3800; i++) { 180 | textWithSomeControls[i] = 65 + (i % 26); // A-Z 181 | } 182 | 183 | // Add some control characters (less than 10%) 184 | for (let i = 3800; i < 4000; i++) { 185 | // Control characters (not 9, 10, 13) 186 | textWithSomeControls[i] = (i % 8) + 1; 187 | } 188 | 189 | mockFileContent(textWithSomeControls); 190 | 191 | expect(isBinaryFile('text-with-some-controls.txt')).toBe(false); 192 | }); 193 | 194 | test('should handle file read errors by treating as binary', () => { 195 | // Mock a file read error 196 | fs.openSync.mockImplementation(() => { 197 | throw new Error('File read error'); 198 | }); 199 | 200 | expect(isBinaryFile('error-file.txt')).toBe(true); 201 | }); 202 | 203 | test('should handle binary files with text headers', () => { 204 | // Many binary files start with text signatures followed by binary content 205 | const mixedContent = Buffer.alloc(4096); 206 | 207 | // Text header part (first 100 bytes) 208 | const header = Buffer.from( 209 | 'SVG-XML-Header Version 1.0 - This looks like text but the file is actually binary content after this header' 210 | ); 211 | header.copy(mixedContent, 0); 212 | 213 | // Binary content - add lots of NULL and control characters 214 | for (let i = header.length; i < 4096; i++) { 215 | mixedContent[i] = i % 16 === 0 ? 0 : i % 8; // Add NULLs and control chars 216 | } 217 | 218 | mockFileContent(mixedContent); 219 | 220 | expect(isBinaryFile('mixed-binary.bin')).toBe(true); 221 | }); 222 | 223 | test('should identify JPEG files as binary', () => { 224 | // Create a JPEG file header 225 | const jpegHeader = Buffer.from([ 226 | 0xff, 227 | 0xd8, 228 | 0xff, 229 | 0xe0, // JPEG SOI marker and APP0 marker 230 | 0x00, 231 | 0x10, // APP0 length 232 | 0x4a, 233 | 0x46, 234 | 0x49, 235 | 0x46, 236 | 0x00, // 'JFIF\0' 237 | 0x01, 238 | 0x01, // Version 239 | 0x00, // Units 240 | 0x00, 241 | 0x01, 242 | 0x00, 243 | 0x01, // Density 244 | 0x00, 245 | 0x00, // Thumbnail 246 | ]); 247 | 248 | mockFileContent(jpegHeader); 249 | 250 | expect(isBinaryFile('image.jpg')).toBe(true); 251 | }); 252 | 253 | test('should identify GIF files as binary', () => { 254 | // Create a GIF file header 255 | const gifHeader = Buffer.from([ 256 | 0x47, 257 | 0x49, 258 | 0x46, 259 | 0x38, 260 | 0x39, 261 | 0x61, // 'GIF89a' 262 | 0x01, 263 | 0x00, 264 | 0x01, 265 | 0x00, // Width and height (1x1) 266 | 0x80, 267 | 0x00, 268 | 0x00, // Flags and background color 269 | ]); 270 | 271 | mockFileContent(gifHeader); 272 | 273 | expect(isBinaryFile('animation.gif')).toBe(true); 274 | }); 275 | 276 | test('should identify WebP files as binary', () => { 277 | // Create a WebP file header 278 | const webpHeader = Buffer.from([ 279 | 0x52, 280 | 0x49, 281 | 0x46, 282 | 0x46, // 'RIFF' 283 | 0x24, 284 | 0x00, 285 | 0x00, 286 | 0x00, // File size - 4 (36 bytes) 287 | 0x57, 288 | 0x45, 289 | 0x42, 290 | 0x50, // 'WEBP' 291 | 0x56, 292 | 0x50, 293 | 0x38, 294 | 0x20, // 'VP8 ' 295 | ]); 296 | 297 | mockFileContent(webpHeader); 298 | 299 | expect(isBinaryFile('image.webp')).toBe(true); 300 | }); 301 | 302 | test('should identify SQLite database files as binary', () => { 303 | // Create a SQLite database header 304 | const sqliteHeader = Buffer.from([ 305 | 0x53, 306 | 0x51, 307 | 0x4c, 308 | 0x69, 309 | 0x74, 310 | 0x65, 311 | 0x20, 312 | 0x66, 313 | 0x6f, 314 | 0x72, 315 | 0x6d, 316 | 0x61, 317 | 0x74, 318 | 0x20, 319 | 0x33, 320 | 0x00, // 'SQLite format 3\0' 321 | ]); 322 | 323 | mockFileContent(sqliteHeader); 324 | 325 | expect(isBinaryFile('database.sqlite')).toBe(true); 326 | }); 327 | 328 | test('should identify font files as binary', () => { 329 | // Create a TTF font header 330 | const ttfHeader = Buffer.from([ 331 | 0x00, 332 | 0x01, 333 | 0x00, 334 | 0x00, // TTF version 1.0 335 | 0x00, 336 | 0x04, // Four tables 337 | 0x00, 338 | 0x00, 339 | 0x00, 340 | 0x00, // Header 341 | 0x00, 342 | 0x00, 343 | 0x00, 344 | 0x00, // More header data 345 | 0x00, 346 | 0x00, 347 | 0x00, 348 | 0x00, // More header data 349 | ]); 350 | 351 | mockFileContent(ttfHeader); 352 | 353 | expect(isBinaryFile('font.ttf')).toBe(true); 354 | }); 355 | 356 | test('should identify Office document files as binary', () => { 357 | // Create an Office document header (simplified DOCX/ZIP header) 358 | const docxHeader = Buffer.from([ 359 | 0x50, 360 | 0x4b, 361 | 0x03, 362 | 0x04, // ZIP signature 363 | 0x14, 364 | 0x00, 365 | 0x06, 366 | 0x00, // Version and flags 367 | 0x08, 368 | 0x00, 369 | 0x00, 370 | 0x00, // Compression method and file time 371 | 0x00, 372 | 0x00, 373 | 0x00, 374 | 0x00, // CRC32 375 | 0x00, 376 | 0x00, 377 | 0x00, 378 | 0x00, // Compressed size 379 | 0x00, 380 | 0x00, 381 | 0x00, 382 | 0x00, // Uncompressed size 383 | ]); 384 | 385 | mockFileContent(docxHeader); 386 | 387 | expect(isBinaryFile('document.docx')).toBe(true); 388 | }); 389 | }); 390 | -------------------------------------------------------------------------------- /tests/unit/components/config-tab.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; 3 | import ConfigTab from '../../../src/renderer/components/ConfigTab'; 4 | 5 | // Mock the list formatter 6 | jest.mock('../../../src/utils/formatters/list-formatter', () => ({ 7 | yamlArrayToPlainText: jest.fn((arr) => (arr || []).join('\n')), 8 | plainTextToYamlArray: jest.fn((text) => (text ? text.split('\n').filter(Boolean) : [])), 9 | })); 10 | 11 | // Mock yaml package 12 | jest.mock('yaml', () => ({ 13 | parse: jest.fn().mockImplementation((str) => { 14 | if (str && str.includes('include_extensions')) { 15 | return { 16 | include_extensions: ['.js', '.jsx'], 17 | use_custom_excludes: true, 18 | use_gitignore: true, 19 | use_custom_includes: true, 20 | }; 21 | } 22 | return {}; 23 | }), 24 | stringify: jest.fn().mockReturnValue('mocked yaml string'), 25 | })); 26 | 27 | // Mock localStorage 28 | const localStorageMock = { 29 | getItem: jest.fn(), 30 | setItem: jest.fn(), 31 | clear: jest.fn(), 32 | }; 33 | Object.defineProperty(window, 'localStorage', { value: localStorageMock }); 34 | 35 | // Mock electronAPI 36 | window.electronAPI = { 37 | selectDirectory: jest.fn().mockResolvedValue('/mock/directory'), 38 | }; 39 | 40 | // Mock alert and custom events 41 | window.alert = jest.fn(); 42 | window.dispatchEvent = jest.fn(); 43 | window.switchToTab = jest.fn(); 44 | 45 | describe('ConfigTab', () => { 46 | const mockConfigContent = '# Test configuration\ninclude_extensions:\n - .js\n - .jsx'; 47 | const mockOnConfigChange = jest.fn(); 48 | 49 | beforeEach(() => { 50 | jest.clearAllMocks(); 51 | localStorageMock.getItem.mockReturnValue('/mock/saved/path'); 52 | jest.useFakeTimers(); 53 | }); 54 | 55 | afterEach(() => { 56 | jest.runOnlyPendingTimers(); 57 | jest.useRealTimers(); 58 | }); 59 | 60 | test('renders inputs and checkboxes correctly', () => { 61 | render(); 62 | 63 | // Check folder input 64 | const folderInput = screen.getByPlaceholderText('Select a root folder'); 65 | expect(folderInput).toBeInTheDocument(); 66 | expect(folderInput).toHaveValue('/mock/saved/path'); 67 | 68 | // Check checkboxes are rendered 69 | expect(screen.getByLabelText('Filter by file extensions')).toBeChecked(); 70 | expect(screen.getByLabelText('Use exclude patterns')).toBeChecked(); 71 | expect(screen.getByLabelText('Apply .gitignore rules')).toBeChecked(); 72 | 73 | // Check textareas 74 | const extensionsTextarea = screen.getByPlaceholderText(/\.py/); 75 | expect(extensionsTextarea).toBeInTheDocument(); 76 | expect(extensionsTextarea).toHaveValue('.js\n.jsx'); 77 | }); 78 | 79 | test('calls onConfigChange when checkbox changes', async () => { 80 | render(); 81 | 82 | const excludePatternCheckbox = screen.getByLabelText('Use exclude patterns'); 83 | 84 | act(() => { 85 | fireEvent.click(excludePatternCheckbox); 86 | jest.advanceTimersByTime(100); // Advance past the debounce 87 | }); 88 | 89 | // onConfigChange should be called after the debounce 90 | await waitFor(() => { 91 | expect(mockOnConfigChange).toHaveBeenCalled(); 92 | }); 93 | }); 94 | 95 | test('calls selectDirectory when folder button is clicked', async () => { 96 | render(); 97 | 98 | const selectFolderButton = screen.getByText('Select Folder'); 99 | 100 | act(() => { 101 | fireEvent.click(selectFolderButton); 102 | }); 103 | 104 | await waitFor(() => { 105 | expect(window.electronAPI.selectDirectory).toHaveBeenCalled(); 106 | expect(localStorageMock.setItem).toHaveBeenCalledWith('rootPath', '/mock/directory'); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/unit/components/file-tree.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import FileTree from '../../../src/renderer/components/FileTree'; 5 | 6 | // Mock data for testing 7 | const mockItems = [ 8 | { 9 | name: 'src', 10 | path: '/project/src', 11 | type: 'directory', 12 | children: [ 13 | { 14 | name: 'index.js', 15 | path: '/project/src/index.js', 16 | type: 'file', 17 | }, 18 | { 19 | name: 'utils', 20 | path: '/project/src/utils', 21 | type: 'directory', 22 | children: [ 23 | { 24 | name: 'helpers.js', 25 | path: '/project/src/utils/helpers.js', 26 | type: 'file', 27 | }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | { 33 | name: 'package.json', 34 | path: '/project/package.json', 35 | type: 'file', 36 | }, 37 | ]; 38 | 39 | describe('FileTree Component', () => { 40 | const mockFileSelect = jest.fn(); 41 | const mockFolderSelect = jest.fn(); 42 | 43 | beforeEach(() => { 44 | mockFileSelect.mockClear(); 45 | mockFolderSelect.mockClear(); 46 | }); 47 | 48 | test('renders the file tree correctly', () => { 49 | render( 50 | 56 | ); 57 | 58 | // Check if folders are rendered - use more specific role-based queries 59 | expect(screen.getByRole('button', { name: /expand folder src/i })).toBeInTheDocument(); 60 | // For files, look for the label or containing element 61 | expect(screen.getByRole('button', { name: /package\.json/i })).toBeInTheDocument(); 62 | }); 63 | 64 | test('displays correct count of selected files', () => { 65 | const selectedFiles = ['/project/src/index.js', '/project/package.json']; 66 | 67 | render( 68 | 74 | ); 75 | 76 | // Check if the count is displayed correctly - use more specific query 77 | const countDisplay = screen.getByText(/files selected/i); 78 | expect(countDisplay).toHaveTextContent('2 of 3 files selected'); 79 | }); 80 | 81 | test('selects individual files when clicked', () => { 82 | render( 83 | 89 | ); 90 | 91 | // Find and click on package.json using a more specific query 92 | const packageJsonButton = screen.getByRole('button', { name: /package\.json/i }); 93 | fireEvent.click(packageJsonButton); 94 | 95 | // Verify that onFileSelect was called with the correct path and selected state 96 | expect(mockFileSelect).toHaveBeenCalledWith('/project/package.json', true); 97 | }); 98 | 99 | test('toggles folder expansion when folder is clicked', () => { 100 | render( 101 | 107 | ); 108 | 109 | // First check that the helpers.js element is not visible initially 110 | // In React components with CSS transitions, the element might exist but be hidden via CSS 111 | const helpersElement = screen.queryByRole('button', { name: /helpers\.js/i }); 112 | if (helpersElement) { 113 | expect(helpersElement).not.toBeVisible(); 114 | } else { 115 | // If it's not in the DOM at all, that's also acceptable 116 | expect(helpersElement).toBeNull(); 117 | } 118 | 119 | // Find and click on the src folder expand button 120 | const srcExpandButton = screen.getByRole('button', { name: /expand folder src/i }); 121 | expect(srcExpandButton).toBeInTheDocument(); 122 | fireEvent.click(srcExpandButton); 123 | 124 | // Now utils folder should be visible 125 | const utilsExpandButton = screen.getByRole('button', { name: /expand folder utils/i }); 126 | expect(utilsExpandButton).toBeInTheDocument(); 127 | expect(utilsExpandButton).toBeVisible(); 128 | 129 | // Click to expand utils folder 130 | fireEvent.click(utilsExpandButton); 131 | 132 | // After expanding utils, verify helpers.js is accessible and visible 133 | const helpersButton = screen.getByRole('button', { name: /helpers\.js/i }); 134 | expect(helpersButton).toBeInTheDocument(); 135 | expect(helpersButton).toBeVisible(); 136 | }); 137 | 138 | test('selects all files when "Select All" is clicked', () => { 139 | render( 140 | 146 | ); 147 | 148 | // Find and click the "Select All" checkbox 149 | const selectAllCheckbox = screen.getByLabelText('Select All'); 150 | fireEvent.click(selectAllCheckbox); 151 | 152 | // Verify that onFileSelect was called for all files 153 | expect(mockFileSelect).toHaveBeenCalledWith('/project/src/index.js', true); 154 | expect(mockFileSelect).toHaveBeenCalledWith('/project/src/utils/helpers.js', true); 155 | expect(mockFileSelect).toHaveBeenCalledWith('/project/package.json', true); 156 | 157 | // Verify that onFolderSelect was called for all folders 158 | expect(mockFolderSelect).toHaveBeenCalledWith('/project/src', true); 159 | expect(mockFolderSelect).toHaveBeenCalledWith('/project/src/utils', true); 160 | }); 161 | 162 | test('deselects all files when "Select All" is toggled off', () => { 163 | render( 164 | 175 | ); 176 | 177 | // Find and click the "Select All" checkbox (which should be checked) 178 | const selectAllCheckbox = screen.getByLabelText('Select All'); 179 | expect(selectAllCheckbox).toBeChecked(); 180 | 181 | fireEvent.click(selectAllCheckbox); 182 | 183 | // Verify that onFileSelect was called to deselect all files 184 | expect(mockFileSelect).toHaveBeenCalledWith('/project/src/index.js', false); 185 | expect(mockFileSelect).toHaveBeenCalledWith('/project/src/utils/helpers.js', false); 186 | expect(mockFileSelect).toHaveBeenCalledWith('/project/package.json', false); 187 | 188 | // Verify that onFolderSelect was called to deselect all folders 189 | expect(mockFolderSelect).toHaveBeenCalledWith('/project/src', false); 190 | expect(mockFolderSelect).toHaveBeenCalledWith('/project/src/utils', false); 191 | }); 192 | 193 | test('shows empty state when no items are provided', () => { 194 | render( 195 | 201 | ); 202 | 203 | // Check if empty state message is shown 204 | expect(screen.getByText('No files to display')).toBeInTheDocument(); 205 | expect(screen.getByText('Select a directory to view files')).toBeInTheDocument(); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /tests/unit/utils/config-manager.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const yaml = require('yaml'); 3 | 4 | // Mock path BEFORE requiring the module under test 5 | const mockConfigPath = '/mock/path/config.default.yaml'; 6 | jest.mock('path', () => { 7 | const originalPath = jest.requireActual('path'); 8 | return { 9 | ...originalPath, 10 | join: jest.fn((...args) => { 11 | // Specifically intercept calls for config.default.yaml 12 | if (args.length >= 2 && args[args.length - 1] === 'config.default.yaml') { 13 | return mockConfigPath; 14 | } 15 | return args.join('/'); 16 | }), 17 | }; 18 | }); 19 | 20 | // Mock other dependencies 21 | jest.mock('fs'); 22 | jest.mock('yaml'); 23 | 24 | // NOW require the module under test 25 | const { loadDefaultConfig, getDefaultConfigObject } = require('../../../src/utils/config-manager'); 26 | 27 | describe('config-manager', () => { 28 | const mockConfigContent = ` 29 | # Default configuration 30 | use_custom_excludes: true 31 | use_gitignore: true 32 | 33 | # Extensions 34 | include_extensions: 35 | - .js 36 | - .jsx 37 | 38 | # Patterns 39 | exclude_patterns: 40 | - "**/node_modules/**" 41 | - "**/.git/**" 42 | `; 43 | 44 | const mockConfigObject = { 45 | use_custom_excludes: true, 46 | use_gitignore: true, 47 | include_extensions: ['.js', '.jsx'], 48 | exclude_patterns: ['**/node_modules/**', '**/.git/**'], 49 | }; 50 | 51 | beforeEach(() => { 52 | jest.clearAllMocks(); 53 | }); 54 | 55 | describe('loadDefaultConfig', () => { 56 | test('should load default config file correctly', () => { 57 | // Setup 58 | fs.readFileSync.mockReturnValue(mockConfigContent); 59 | 60 | // Execute 61 | const result = loadDefaultConfig(); 62 | 63 | // Verify 64 | expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8'); 65 | expect(result).toBe(mockConfigContent); 66 | }); 67 | 68 | test('should handle errors and return empty config', () => { 69 | // Setup 70 | fs.readFileSync.mockImplementation(() => { 71 | throw new Error('File not found'); 72 | }); 73 | 74 | // Execute 75 | const result = loadDefaultConfig(); 76 | 77 | // Verify 78 | expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8'); 79 | expect(result).toBe('{}'); 80 | }); 81 | }); 82 | 83 | describe('getDefaultConfigObject', () => { 84 | test('should parse default config to object', () => { 85 | // Setup 86 | fs.readFileSync.mockReturnValue(mockConfigContent); 87 | yaml.parse.mockReturnValue(mockConfigObject); 88 | 89 | // Execute 90 | const result = getDefaultConfigObject(); 91 | 92 | // Verify 93 | expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8'); 94 | expect(yaml.parse).toHaveBeenCalledWith(mockConfigContent); 95 | expect(result).toEqual(mockConfigObject); 96 | }); 97 | 98 | test('should handle parsing errors and return empty object', () => { 99 | // Setup 100 | fs.readFileSync.mockReturnValue(mockConfigContent); 101 | yaml.parse.mockImplementation(() => { 102 | throw new Error('Invalid YAML'); 103 | }); 104 | 105 | // Execute 106 | const result = getDefaultConfigObject(); 107 | 108 | // Verify 109 | expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8'); 110 | expect(yaml.parse).toHaveBeenCalledWith(mockConfigContent); 111 | expect(result).toEqual({}); 112 | }); 113 | 114 | test('should handle file read errors and return empty object', () => { 115 | // Setup 116 | fs.readFileSync.mockImplementation(() => { 117 | throw new Error('File not found'); 118 | }); 119 | 120 | // Execute 121 | const result = getDefaultConfigObject(); 122 | 123 | // Verify 124 | expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8'); 125 | expect(result).toEqual({}); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /tests/unit/utils/content-processor.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { ContentProcessor } = require('../../../src/utils/content-processor'); 4 | const { isBinaryFile } = require('../../../src/utils/file-analyzer'); 5 | 6 | // Mock dependencies 7 | jest.mock('fs'); 8 | jest.mock('path'); 9 | jest.mock('../../../src/utils/file-analyzer', () => ({ 10 | isBinaryFile: jest.fn(), 11 | })); 12 | 13 | describe('ContentProcessor', () => { 14 | let contentProcessor; 15 | let mockTokenCounter; 16 | 17 | beforeEach(() => { 18 | // Reset all mocks 19 | jest.clearAllMocks(); 20 | 21 | // Create mock token counter 22 | mockTokenCounter = { 23 | countTokens: jest.fn().mockReturnValue(100), 24 | }; 25 | 26 | // Create instance with mock 27 | contentProcessor = new ContentProcessor(mockTokenCounter); 28 | 29 | // Setup path mock 30 | path.extname.mockImplementation((filePath) => { 31 | const parts = filePath.split('.'); 32 | return parts.length > 1 ? '.' + parts[parts.length - 1] : ''; 33 | }); 34 | }); 35 | 36 | describe('processFile', () => { 37 | test('should process text files correctly', () => { 38 | // Setup 39 | const filePath = '/project/src/file.js'; 40 | const relativePath = 'src/file.js'; 41 | const fileContent = 'const x = 10;'; 42 | 43 | // Mock dependencies 44 | isBinaryFile.mockReturnValue(false); 45 | fs.readFileSync.mockReturnValue(fileContent); 46 | 47 | // Execute 48 | const result = contentProcessor.processFile(filePath, relativePath); 49 | 50 | // Verify 51 | expect(isBinaryFile).toHaveBeenCalledWith(filePath); 52 | expect(fs.readFileSync).toHaveBeenCalledWith(filePath, { encoding: 'utf-8', flag: 'r' }); 53 | 54 | // Check formatting 55 | expect(result).toContain('######'); 56 | expect(result).toContain(relativePath); 57 | expect(result).toContain('```'); 58 | expect(result).toContain(fileContent); 59 | }); 60 | 61 | test('should handle binary files correctly', () => { 62 | // Setup 63 | const filePath = '/project/images/logo.png'; 64 | const relativePath = 'images/logo.png'; 65 | 66 | // Mock dependencies 67 | isBinaryFile.mockReturnValue(true); 68 | fs.statSync.mockReturnValue({ size: 1024 }); 69 | path.extname.mockReturnValue('.png'); 70 | 71 | // Execute 72 | const result = contentProcessor.processFile(filePath, relativePath); 73 | 74 | // Verify 75 | expect(isBinaryFile).toHaveBeenCalledWith(filePath); 76 | expect(fs.statSync).toHaveBeenCalledWith(filePath); 77 | 78 | // Check formatting 79 | expect(result).toContain('######'); 80 | expect(result).toContain(`${relativePath} (binary file)`); 81 | expect(result).toContain('[BINARY FILE]'); 82 | expect(result).toContain('PNG'); 83 | expect(result).toContain('1.00 KB'); 84 | }); 85 | 86 | test('should handle errors when reading files', () => { 87 | // Setup 88 | const filePath = '/project/src/missing.js'; 89 | const relativePath = 'src/missing.js'; 90 | 91 | // Mock dependencies 92 | isBinaryFile.mockReturnValue(false); 93 | fs.readFileSync.mockImplementation(() => { 94 | throw new Error('File not found'); 95 | }); 96 | 97 | // Execute 98 | const result = contentProcessor.processFile(filePath, relativePath); 99 | 100 | // Verify 101 | expect(isBinaryFile).toHaveBeenCalledWith(filePath); 102 | expect(fs.readFileSync).toHaveBeenCalledWith(filePath, { encoding: 'utf-8', flag: 'r' }); 103 | 104 | // Should return null on error 105 | expect(result).toBeNull(); 106 | }); 107 | 108 | test('should use custom processing options if provided', () => { 109 | // Setup 110 | const filePath = '/project/src/file.js'; 111 | const relativePath = 'src/file.js'; 112 | const fileContent = 'const x = 10;'; 113 | const options = { 114 | showTokenCount: true, 115 | }; 116 | 117 | // Mock dependencies 118 | isBinaryFile.mockReturnValue(false); 119 | fs.readFileSync.mockReturnValue(fileContent); 120 | mockTokenCounter.countTokens.mockReturnValue(42); 121 | 122 | // Execute 123 | const result = contentProcessor.processFile(filePath, relativePath, options); 124 | 125 | // Verify core behavior 126 | expect(isBinaryFile).toHaveBeenCalledWith(filePath); 127 | expect(fs.readFileSync).toHaveBeenCalledWith(filePath, { encoding: 'utf-8', flag: 'r' }); 128 | 129 | // With mock implementation, just check it returned properly formatted content 130 | expect(result).toContain('######'); 131 | expect(result).toContain(relativePath); 132 | expect(result).toContain('```'); 133 | expect(result).toContain(fileContent); 134 | }); 135 | }); 136 | 137 | describe('readAnalysisFile', () => { 138 | test('should parse analysis file correctly', () => { 139 | // Setup 140 | const analysisPath = '/project/analysis.txt'; 141 | const analysisContent = `src/file.js 142 | 100 143 | src/utils/helper.js 144 | 50 145 | Total tokens: 150`; 146 | 147 | // Mock dependencies 148 | fs.readFileSync.mockReturnValue(analysisContent); 149 | 150 | // Execute 151 | const result = contentProcessor.readAnalysisFile(analysisPath); 152 | 153 | // Verify 154 | expect(fs.readFileSync).toHaveBeenCalledWith(analysisPath, { encoding: 'utf-8', flag: 'r' }); 155 | 156 | // Check parsing 157 | expect(result).toHaveLength(2); 158 | expect(result[0]).toEqual({ path: 'src/file.js', tokens: 100 }); 159 | expect(result[1]).toEqual({ path: 'src/utils/helper.js', tokens: 50 }); 160 | }); 161 | 162 | test('should handle malformed analysis files', () => { 163 | // Setup 164 | const analysisPath = '/project/malformed.txt'; 165 | const analysisContent = `src/file.js 166 | not-a-number 167 | src/utils/helper.js 168 | 50`; 169 | 170 | // Mock dependencies 171 | fs.readFileSync.mockReturnValue(analysisContent); 172 | 173 | // Execute 174 | const result = contentProcessor.readAnalysisFile(analysisPath); 175 | 176 | // Verify 177 | expect(fs.readFileSync).toHaveBeenCalledWith(analysisPath, { encoding: 'utf-8', flag: 'r' }); 178 | 179 | // Should only include the valid entry 180 | expect(result).toHaveLength(1); 181 | expect(result[0]).toEqual({ path: 'src/utils/helper.js', tokens: 50 }); 182 | }); 183 | 184 | test('should handle errors when reading analysis file', () => { 185 | // Setup 186 | const analysisPath = '/project/missing.txt'; 187 | 188 | // Mock dependencies 189 | fs.readFileSync.mockImplementation(() => { 190 | throw new Error('File not found'); 191 | }); 192 | 193 | // Execute 194 | const result = contentProcessor.readAnalysisFile(analysisPath); 195 | 196 | // Verify 197 | expect(fs.readFileSync).toHaveBeenCalledWith(analysisPath, { encoding: 'utf-8', flag: 'r' }); 198 | 199 | // Should return empty array on error 200 | expect(result).toEqual([]); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /tests/unit/utils/filter-utils.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { 3 | normalizePath, 4 | getRelativePath, 5 | shouldExclude, 6 | } = require('../../../src/utils/filter-utils'); 7 | 8 | // Mock path module 9 | jest.mock('path', () => ({ 10 | ...jest.requireActual('path'), 11 | relative: jest.fn().mockImplementation((from, to) => { 12 | // Simple implementation for testing 13 | if (to.startsWith(from)) { 14 | return to.substring(from.length).replace(/^\//, ''); 15 | } 16 | return to; 17 | }), 18 | extname: jest.fn().mockImplementation((filePath) => { 19 | // Extract extension from filename 20 | const parts = filePath.split('.'); 21 | return parts.length > 1 ? `.${parts[parts.length - 1]}` : ''; 22 | }), 23 | })); 24 | 25 | describe('filter-utils', () => { 26 | describe('normalizePath', () => { 27 | test('should convert backslashes to forward slashes', () => { 28 | expect(normalizePath('C:\\path\\to\\file.js')).toBe('C:/path/to/file.js'); 29 | }); 30 | 31 | test('should keep forward slashes unchanged', () => { 32 | expect(normalizePath('/path/to/file.js')).toBe('/path/to/file.js'); 33 | }); 34 | 35 | test('should handle mixed slashes', () => { 36 | expect(normalizePath('/path\\to/file\\name.js')).toBe('/path/to/file/name.js'); 37 | }); 38 | 39 | test('should handle empty paths', () => { 40 | expect(normalizePath('')).toBe(''); 41 | }); 42 | }); 43 | 44 | describe('getRelativePath', () => { 45 | test('should get path relative to root', () => { 46 | path.relative.mockImplementationOnce(() => 'src/file.js'); 47 | const result = getRelativePath('/root/src/file.js', '/root'); 48 | expect(result).toBe('src/file.js'); 49 | expect(path.relative).toHaveBeenCalledWith('/root', '/root/src/file.js'); 50 | }); 51 | 52 | test('should normalize the result', () => { 53 | path.relative.mockImplementationOnce(() => 'src\\file.js'); 54 | const result = getRelativePath('/root/src/file.js', '/root'); 55 | expect(result).toBe('src/file.js'); 56 | }); 57 | }); 58 | 59 | describe('shouldExclude', () => { 60 | // Test cases for different combinations of config settings 61 | 62 | test('should exclude files that match exclude patterns when use_custom_excludes is true', () => { 63 | const itemPath = '/project/node_modules/package.json'; 64 | const rootPath = '/project'; 65 | const excludePatterns = ['**/node_modules/**']; 66 | const config = { use_custom_excludes: true }; 67 | 68 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); 69 | }); 70 | 71 | test('should not exclude files that match exclude patterns when use_custom_excludes is false', () => { 72 | const itemPath = '/project/node_modules/package.json'; 73 | const rootPath = '/project'; 74 | 75 | // When testing custom excludes, gitignore patterns should be empty to isolate the test 76 | const gitignorePatterns = []; 77 | 78 | const config = { 79 | use_custom_excludes: false, // This is what we're testing - should NOT apply exclude_patterns 80 | use_gitignore: false, // Explicitly disable gitignore to avoid interference 81 | exclude_patterns: ['**/node_modules/**'], // This pattern should be ignored due to use_custom_excludes: false 82 | }; 83 | 84 | // The function should return false (don't exclude) because use_custom_excludes is false 85 | expect(shouldExclude(itemPath, rootPath, gitignorePatterns, config)).toBe(false); 86 | }); 87 | 88 | test('should exclude files without matching extension when use_custom_includes is true', () => { 89 | const itemPath = '/project/src/file.css'; 90 | const rootPath = '/project'; 91 | const excludePatterns = []; 92 | const config = { 93 | use_custom_includes: true, 94 | include_extensions: ['.js', '.jsx'], 95 | }; 96 | 97 | // Mock implementation to ensure correct behavior for this test 98 | jest.spyOn(path, 'extname').mockImplementationOnce(() => '.css'); 99 | 100 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); 101 | 102 | // Reset the mock 103 | path.extname.mockRestore(); 104 | }); 105 | 106 | test('should exclude files with non-matching extensions when use_custom_includes is true', () => { 107 | const itemPath = '/project/src/file.css'; 108 | const rootPath = '/project'; 109 | const excludePatterns = []; 110 | const config = { 111 | use_custom_includes: true, 112 | include_extensions: ['.js', '.jsx', '.json'], 113 | }; 114 | 115 | // Use a direct mock replacement rather than mockReturnValueOnce 116 | const originalExtname = path.extname; 117 | path.extname = jest.fn().mockReturnValue('.css'); 118 | 119 | // Debug: Log values to understand the issue 120 | console.log('Testing file extension exclusion:'); 121 | console.log(`Path extname returns: ${path.extname(itemPath)}`); 122 | console.log(`Config includes: ${config.include_extensions}`); 123 | console.log( 124 | `Should exclude?: ${!config.include_extensions.includes(path.extname(itemPath))}` 125 | ); 126 | 127 | const result = shouldExclude(itemPath, rootPath, excludePatterns, config); 128 | 129 | // Restore original function 130 | path.extname = originalExtname; 131 | 132 | expect(result).toBe(true); 133 | }); 134 | 135 | test('should include files with matching extensions when use_custom_includes is true', () => { 136 | const itemPath = '/project/src/file.js'; 137 | const rootPath = '/project'; 138 | const excludePatterns = []; 139 | const config = { 140 | use_custom_includes: true, 141 | include_extensions: ['.js', '.jsx', '.json'], 142 | }; 143 | 144 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); 145 | }); 146 | 147 | test('should exclude files that match gitignore patterns when use_gitignore is true', () => { 148 | const itemPath = '/project/logs/error.log'; 149 | const rootPath = '/project'; 150 | const excludePatterns = ['*.log']; 151 | const config = { 152 | use_custom_excludes: false, 153 | use_gitignore: true, 154 | }; 155 | 156 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); 157 | }); 158 | 159 | test('should not exclude files that match gitignore patterns when use_gitignore is false', () => { 160 | const itemPath = '/project/logs/error.log'; 161 | const rootPath = '/project'; 162 | const excludePatterns = ['*.log']; 163 | const config = { 164 | use_custom_excludes: false, 165 | use_gitignore: false, 166 | }; 167 | 168 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); 169 | }); 170 | 171 | test('should handle precedence of custom excludes over gitignore includes', () => { 172 | const itemPath = '/project/logs/important.log'; 173 | const rootPath = '/project'; 174 | // This represents the negated pattern !important.log in gitignore 175 | const excludePatterns = { 176 | excludePatterns: ['*.log'], 177 | includePatterns: ['important.log'], 178 | }; 179 | const config = { 180 | use_custom_excludes: true, 181 | use_gitignore: true, 182 | exclude_patterns: ['important.log'], // explicitly exclude in custom patterns 183 | }; 184 | 185 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(true); 186 | }); 187 | 188 | test('should honor gitignore negated patterns when not explicitly excluded', () => { 189 | const itemPath = '/project/logs/important.log'; 190 | const rootPath = '/project'; 191 | // This represents the negated pattern !important.log in gitignore 192 | const excludePatterns = { 193 | excludePatterns: ['*.log'], 194 | includePatterns: ['important.log'], 195 | }; 196 | const config = { 197 | use_custom_excludes: true, 198 | use_gitignore: true, 199 | exclude_patterns: ['*.html'], // No explicit exclude for important.log 200 | }; 201 | 202 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); 203 | }); 204 | 205 | test('should handle empty patterns', () => { 206 | const itemPath = '/project/src/file.js'; 207 | const rootPath = '/project'; 208 | const excludePatterns = []; 209 | const config = {}; 210 | 211 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); 212 | }); 213 | 214 | test('should handle error cases gracefully', () => { 215 | const itemPath = null; 216 | const rootPath = '/project'; 217 | const excludePatterns = ['*.log']; 218 | const config = { 219 | use_custom_excludes: true, 220 | }; 221 | 222 | // Should not throw an error 223 | expect(() => shouldExclude(itemPath, rootPath, excludePatterns, config)).not.toThrow(); 224 | // Default to not excluding in error case 225 | expect(shouldExclude(itemPath, rootPath, excludePatterns, config)).toBe(false); 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /tests/unit/utils/fnmatch.test.js: -------------------------------------------------------------------------------- 1 | const { fnmatch } = require('../../../src/utils/fnmatch'); 2 | 3 | describe('fnmatch', () => { 4 | // Basic pattern tests 5 | describe('basic patterns', () => { 6 | test('exact matches', () => { 7 | expect(fnmatch('file.txt', 'file.txt')).toBe(true); 8 | expect(fnmatch('file.txt', 'other.txt')).toBe(false); 9 | expect(fnmatch('path/to/file.txt', 'path/to/file.txt')).toBe(true); 10 | }); 11 | 12 | test('wildcard patterns', () => { 13 | expect(fnmatch('file.txt', '*.txt')).toBe(true); 14 | expect(fnmatch('file.js', '*.txt')).toBe(false); 15 | expect(fnmatch('path/file.txt', '*/file.txt')).toBe(true); 16 | expect(fnmatch('path/to/file.txt', '*/file.txt')).toBe(false); 17 | }); 18 | 19 | test('multiple wildcards', () => { 20 | expect(fnmatch('path/to/file.txt', '*/*/*.txt')).toBe(true); 21 | expect(fnmatch('path/file.txt', '*/*/*.txt')).toBe(false); 22 | expect(fnmatch('path/to/file.jpg', '*/*/*.txt')).toBe(false); 23 | }); 24 | }); 25 | 26 | // Path-specific features 27 | describe('path patterns', () => { 28 | test('matchBase mode for basename matching', () => { 29 | // matchBase should match basename if pattern has no slashes 30 | expect(fnmatch('path/to/file.txt', 'file.txt')).toBe(true); 31 | expect(fnmatch('deeply/nested/path/file.txt', 'file.txt')).toBe(true); 32 | 33 | // Should still require exact extension match 34 | expect(fnmatch('path/to/file.js', 'file.txt')).toBe(false); 35 | }); 36 | 37 | test('root-anchored patterns', () => { 38 | expect(fnmatch('file.txt', '/file.txt')).toBe(false); // Pattern is absolute, path is relative 39 | expect(fnmatch('/file.txt', '/file.txt')).toBe(true); 40 | expect(fnmatch('/path/file.txt', '/path/*')).toBe(true); 41 | }); 42 | 43 | test('directory matches with trailing slash', () => { 44 | expect(fnmatch('node_modules/', 'node_modules/')).toBe(true); 45 | expect(fnmatch('node_modules', 'node_modules/')).toBe(false); // Path needs trailing slash 46 | expect(fnmatch('src/node_modules/', '*/node_modules/')).toBe(true); 47 | }); 48 | }); 49 | 50 | // Glob features 51 | describe('glob patterns', () => { 52 | test('double-star patterns for recursive matching', () => { 53 | expect(fnmatch('path/to/file.txt', '**/file.txt')).toBe(true); 54 | expect(fnmatch('file.txt', '**/file.txt')).toBe(true); 55 | expect(fnmatch('a/very/deep/path/file.txt', '**/file.txt')).toBe(true); 56 | 57 | expect(fnmatch('path/to/other.txt', '**/file.txt')).toBe(false); 58 | }); 59 | 60 | test('character classes', () => { 61 | expect(fnmatch('file.txt', 'file.[tj]xt')).toBe(true); 62 | expect(fnmatch('file.jxt', 'file.[tj]xt')).toBe(true); 63 | expect(fnmatch('file.sxt', 'file.[tj]xt')).toBe(false); 64 | 65 | expect(fnmatch('file1.txt', 'file[0-9].txt')).toBe(true); 66 | expect(fnmatch('fileA.txt', 'file[0-9].txt')).toBe(false); 67 | }); 68 | 69 | test('negated character classes', () => { 70 | expect(fnmatch('file1.txt', 'file[!a-z].txt')).toBe(true); 71 | expect(fnmatch('filea.txt', 'file[!a-z].txt')).toBe(false); 72 | }); 73 | 74 | test('brace expansion', () => { 75 | expect(fnmatch('file.js', '*.{js,jsx}')).toBe(true); 76 | expect(fnmatch('file.jsx', '*.{js,jsx}')).toBe(true); 77 | expect(fnmatch('file.ts', '*.{js,jsx}')).toBe(false); 78 | 79 | expect(fnmatch('src/components', '{src,app}/components')).toBe(true); 80 | expect(fnmatch('app/components', '{src,app}/components')).toBe(true); 81 | expect(fnmatch('lib/components', '{src,app}/components')).toBe(false); 82 | }); 83 | 84 | test('complex patterns', () => { 85 | expect(fnmatch('src/components/Button.jsx', 'src/**/*.{js,jsx}')).toBe(true); 86 | expect(fnmatch('src/utils/helpers.js', 'src/**/*.{js,jsx}')).toBe(true); 87 | expect(fnmatch('src/components/Button.css', 'src/**/*.{js,jsx}')).toBe(false); 88 | 89 | expect(fnmatch('test/unit/Button.test.js', 'test/**/*.test.{js,jsx}')).toBe(true); 90 | expect(fnmatch('test/integration/api.spec.js', 'test/**/*.test.{js,jsx}')).toBe(false); 91 | }); 92 | 93 | test('dot files', () => { 94 | // With dot:true option, should match dot files 95 | expect(fnmatch('.gitignore', '*')).toBe(true); 96 | expect(fnmatch('.env', '*')).toBe(true); 97 | expect(fnmatch('path/.config', '*/.config')).toBe(true); 98 | }); 99 | }); 100 | 101 | // Error handling 102 | describe('error handling', () => { 103 | test('invalid patterns', () => { 104 | // Minimatch should throw an error on invalid pattern, but our fnmatch wrapper should handle it 105 | expect(fnmatch('file.txt', '[invalid-pattern')).toBe(false); 106 | }); 107 | 108 | test('non-string inputs', () => { 109 | expect(fnmatch(null, '*.txt')).toBe(false); 110 | expect(fnmatch('file.txt', null)).toBe(false); 111 | expect(fnmatch({}, '*.txt')).toBe(false); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/unit/utils/token-counter.test.js: -------------------------------------------------------------------------------- 1 | import { TokenCounter } from '../../../src/utils/token-counter'; 2 | 3 | // Mock the tiktoken import 4 | jest.mock('tiktoken', () => ({ 5 | encoding_for_model: jest.fn().mockImplementation(() => ({ 6 | encode: jest.fn().mockImplementation((text) => { 7 | // Simple mock that returns an array with length roughly proportional to text length 8 | // Handle null/undefined case 9 | if (!text) return []; 10 | return Array(Math.ceil(text.length / 4)).fill(0); 11 | }), 12 | })), 13 | })); 14 | 15 | describe('TokenCounter', () => { 16 | let tokenCounter; 17 | 18 | beforeEach(() => { 19 | tokenCounter = new TokenCounter(); 20 | }); 21 | 22 | test('countTokens returns expected token count for a string', () => { 23 | const text = 'This is a test string for token counting'; 24 | const count = tokenCounter.countTokens(text); 25 | 26 | // Our mock implementation will return text length / 4 rounded up 27 | expect(count).toBe(Math.ceil(text.length / 4)); 28 | }); 29 | 30 | test('countTokens returns 0 for empty string', () => { 31 | const count = tokenCounter.countTokens(''); 32 | expect(count).toBe(0); 33 | }); 34 | 35 | test('countTokens handles null or undefined', () => { 36 | expect(tokenCounter.countTokens(null)).toBe(0); 37 | expect(tokenCounter.countTokens(undefined)).toBe(0); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | // Source and destination paths 5 | const srcPath = path.resolve(__dirname, 'src/renderer'); 6 | const entryFile = path.resolve(srcPath, 'index.js'); 7 | 8 | // Check if the entry file exists and create it if it doesn't 9 | if (!fs.existsSync(entryFile)) { 10 | const entryContent = `import React from 'react'; 11 | import { createRoot } from 'react-dom/client'; 12 | import App from './components/App'; 13 | 14 | const container = document.getElementById('app'); 15 | const root = createRoot(container); 16 | root.render(); 17 | `; 18 | fs.writeFileSync(entryFile, entryContent); 19 | console.log('Created entry file:', entryFile); 20 | } 21 | 22 | module.exports = { 23 | entry: entryFile, 24 | output: { 25 | filename: 'bundle.js', // Output to bundle.js to avoid webpack processing its own output 26 | path: srcPath, // Same directory for simplicity 27 | }, 28 | // Increase the node options to allow more stack space 29 | node: { 30 | global: true, 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.(js|jsx)$/, 36 | exclude: /node_modules/, 37 | use: { 38 | loader: 'babel-loader', 39 | }, 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: ['style-loader', 'css-loader', 'postcss-loader'], 44 | }, 45 | ], 46 | }, 47 | resolve: { 48 | extensions: ['.js', '.jsx'], 49 | fallback: { 50 | path: require.resolve('path-browserify'), 51 | process: require.resolve('process/browser'), 52 | }, 53 | }, 54 | devtool: 'source-map', 55 | }; 56 | --------------------------------------------------------------------------------