diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 541fb7c1..86035f1c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,12 +23,12 @@ jobs: with: python-version: '3.12' + # Pin ruff version to match .pre-commit-config.yaml (astral-sh/ruff-pre-commit rev) - name: Install ruff - run: pip install ruff + run: pip install ruff==0.14.10 - name: Run ruff check run: ruff check apps/backend/ --output-format=github - name: Run ruff format check run: ruff format apps/backend/ --check --diff - diff --git a/.github/workflows/quality-dco.yml b/.github/workflows/quality-dco.yml index cfb15ec7..e96c6520 100644 --- a/.github/workflows/quality-dco.yml +++ b/.github/workflows/quality-dco.yml @@ -8,16 +8,34 @@ permissions: contents: read pull-requests: read +env: + # Comma-separated list of emails that should be ignored during DCO checks + DCO_EXCLUDE_EMAILS: '' + +# Job is taken from https://github.com/cncf/dcochecker/blob/main/action.yml +# Using steps instead of reusable workflow to preserve custom failure message jobs: check: name: DCO Check runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Check DCO Sign-off - uses: dcoapp/dco-check@v1 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.x + uses: actions/setup-python@v5 with: - require-signoff: true + python-version: '3.x' + + - name: Check DCO + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DCO_CHECK_VERBOSE: '1' + DCO_CHECK_EXCLUDE_EMAILS: ${{ env.DCO_EXCLUDE_EMAILS }} + run: | + pip3 install -U dco-check + dco-check - name: DCO Help on Failure if: failure() @@ -26,45 +44,64 @@ jobs: script: | const helpMsg = `## ❌ DCO Sign-off Required -This project requires all commits to be signed off with the Developer Certificate of Origin (DCO). + This project requires all commits to be signed off with the Developer Certificate of Origin (DCO). -### How to Fix + ### How to Fix -**Option 1: Sign off your last commit** -\`\`\`bash -git commit --amend --signoff -git push --force-with-lease -\`\`\` + **⚠️ Important:** If you merged \`develop\` or another branch, be careful not to accidentally sign other people's commits. Only sign YOUR commits. -**Option 2: Sign off all commits in this PR** -\`\`\`bash -git rebase HEAD~N --signoff # Replace N with number of commits -git push --force-with-lease -\`\`\` + **Step 1: Identify which commits are yours** + \`\`\`bash + git log --oneline + \`\`\` -**Option 3: Configure git to always sign off** -\`\`\`bash -git config --global format.signoff true -\`\`\` + **Step 2: Choose the appropriate method** -### What is DCO? + **If only your last commit needs signing:** + \`\`\`bash + git commit --amend --signoff + git push --force-with-lease + \`\`\` -The [Developer Certificate of Origin](https://developercertificate.org/) is a lightweight way for contributors to certify that they wrote or have the right to submit the code they are contributing. + **If you have multiple commits and HAVE NOT merged other branches:** + \`\`\`bash + git rebase HEAD~N --signoff # Replace N with number of YOUR commits + git push --force-with-lease + \`\`\` -By signing off, you agree to the DCO terms: -- The contribution was created by you -- You have the right to submit it under the project's license -- You understand the contribution is public and recorded + **If you merged another branch (use interactive rebase):** + \`\`\`bash + git rebase -i HEAD~N # Replace N with total number of commits + # Mark only YOUR commits with 'edit', leave others as 'pick' + # For each commit you marked: + git commit --amend --signoff --no-edit + git rebase --continue + git push --force-with-lease + \`\`\` -### Sign-off Format + **Tip: Configure git to always sign off future commits** + \`\`\`bash + git config --global format.signoff true + \`\`\` -Your commit message should end with: -\`\`\` -Signed-off-by: Your Name -\`\`\` + ### What is DCO? -This line is automatically added when you use \`git commit -s\` or \`git commit --signoff\`. -`; + The [Developer Certificate of Origin](https://developercertificate.org/) is a lightweight way for contributors to certify that they wrote or have the right to submit the code they are contributing. + + By signing off, you agree to the DCO terms: + - The contribution was created by you + - You have the right to submit it under the project's license + - You understand the contribution is public and recorded + + ### Sign-off Format + + Your commit message should end with: + \`\`\` + Signed-off-by: Your Name + \`\`\` + + This line is automatically added when you use \`git commit -s\` or \`git commit --signoff\`. + `; core.summary.addRaw(helpMsg); await core.summary.write(); diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 06646da2..f3564ad5 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: with: stale-issue-message: | This issue has been inactive for 60 days. It will be closed in 14 days if there's no activity. - + - If this is still relevant, please comment or update the issue - If you're working on this, add the `in-progress` label close-issue-message: 'Closed due to inactivity. Feel free to reopen if still relevant.' diff --git a/.husky/pre-commit b/.husky/pre-commit index 9e7c5b0d..333288f2 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -111,7 +111,7 @@ if git diff --cached --name-only | grep -q "^apps/backend/.*\.py$"; then exit 1 fi cd ../.. - + echo "Backend checks passed!" fi @@ -123,10 +123,10 @@ fi if git diff --cached --name-only | grep -q "^apps/frontend/"; then echo "Frontend changes detected, running frontend checks..." cd apps/frontend - + # Run lint-staged (handles staged .ts/.tsx files) npm exec lint-staged - + # Run TypeScript type check echo "Running type check..." npm run typecheck @@ -134,7 +134,7 @@ if git diff --cached --name-only | grep -q "^apps/frontend/"; then echo "Type check failed. Please fix TypeScript errors before committing." exit 1 fi - + # Run linting echo "Running lint..." npm run lint @@ -142,7 +142,7 @@ if git diff --cached --name-only | grep -q "^apps/frontend/"; then echo "Lint failed. Run 'npm run lint:fix' to auto-fix issues." exit 1 fi - + # Check for vulnerabilities (only high severity) echo "Checking for vulnerabilities..." npm audit --audit-level=high @@ -150,7 +150,7 @@ if git diff --cached --name-only | grep -q "^apps/frontend/"; then echo "High severity vulnerabilities found. Run 'npm audit fix' to resolve." exit 1 fi - + cd ../.. echo "Frontend checks passed!" fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97c54a1a..ccdf54ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,26 +4,44 @@ repos: hooks: - id: version-sync name: Version Sync - entry: bash -c ' - VERSION=$(node -p "require(\"./package.json\").version"); - if [ -n "$VERSION" ]; then - # Sync to apps/frontend/package.json - node -e "const fs=require(\"fs\");const p=require(\"./apps/frontend/package.json\");if(p.version!==\"$VERSION\"){p.version=\"$VERSION\";fs.writeFileSync(\"./apps/frontend/package.json\",JSON.stringify(p,null,2)+\"\n\");}"; - # Sync to apps/backend/__init__.py - sed -i.bak "s/__version__ = \"[^\"]*\"/__version__ = \"$VERSION\"/" apps/backend/__init__.py && rm -f apps/backend/__init__.py.bak; - # Sync to README.md - sed -i.bak "s/version-[0-9]*\.[0-9]*\.[0-9]*-blue/version-$VERSION-blue/g" README.md; - sed -i.bak "s/Auto-Claude-[0-9]*\.[0-9]*\.[0-9]*/Auto-Claude-$VERSION/g" README.md && rm -f README.md.bak; - git add apps/frontend/package.json apps/backend/__init__.py README.md 2>/dev/null || true; - fi - ' + entry: bash + args: + - -c + - | + VERSION=$(node -p "require('./package.json').version") + if [ -n "$VERSION" ]; then + + # Sync to apps/frontend/package.json + node -e " + const fs = require('fs'); + const p = require('./apps/frontend/package.json'); + const v = process.argv[1]; + if (p.version !== v) { + p.version = v; + fs.writeFileSync('./apps/frontend/package.json', JSON.stringify(p, null, 2) + '\n'); + } + " "$VERSION" + + # Sync to apps/backend/__init__.py + sed -i.bak "s/__version__ = \"[^\"]*\"/__version__ = \"$VERSION\"/" apps/backend/__init__.py && rm -f apps/backend/__init__.py.bak + + # Sync to README.md - shields.io version badge (link requires escaped hyphens for version) + ESCAPED_VERSION=$(echo "$VERSION" | sed 's/-/--/g') + sed -i.bak "s/version-[0-9]*\.[0-9]*\.[0-9]*\(-\{1,2\}[a-z]*\.[0-9]*\)*-blue/version-$ESCAPED_VERSION-blue/g" README.md + + # Sync to README.md - download filenames (no escaping needed) + sed -i.bak "s/Auto-Claude-[0-9]*\.[0-9]*\.[0-9]*\(-[a-z]*\.[0-9]*\)*/Auto-Claude-$VERSION/g" README.md && rm -f README.md.bak + + # Stage changes + git add apps/frontend/package.json apps/backend/__init__.py README.md 2>/dev/null || true + fi language: system files: ^package\.json$ pass_filenames: false # Python linting (apps/backend/) - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.14.10 hooks: - id: ruff args: [--fix] @@ -37,7 +55,29 @@ repos: hooks: - id: pytest name: Python Tests - entry: bash -c 'cd apps/backend && PYTHONPATH=. python -m pytest ../../tests/ -v --tb=short -x -m "not slow and not integration" --ignore=../../tests/test_graphiti.py --ignore=../../tests/test_merge_file_tracker.py --ignore=../../tests/test_service_orchestrator.py --ignore=../../tests/test_worktree.py --ignore=../../tests/test_workspace.py' + entry: bash + args: + - -c + - | + cd apps/backend + if [ -f ".venv/bin/pytest" ]; then + PYTEST_CMD=".venv/bin/pytest" + elif [ -f ".venv/Scripts/pytest.exe" ]; then + PYTEST_CMD=".venv/Scripts/pytest.exe" + else + PYTEST_CMD="python -m pytest" + fi + PYTHONPATH=. $PYTEST_CMD \ + ../../tests/ \ + -v \ + --tb=short \ + -x \ + -m "not slow and not integration" \ + --ignore=../../tests/test_graphiti.py \ + --ignore=../../tests/test_merge_file_tracker.py \ + --ignore=../../tests/test_service_orchestrator.py \ + --ignore=../../tests/test_worktree.py \ + --ignore=../../tests/test_workspace.py language: system files: ^(apps/backend/.*\.py$|tests/.*\.py$) pass_filenames: false @@ -61,7 +101,7 @@ repos: # General checks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ced327ad..80435c56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,7 @@ Before contributing, ensure you have the following installed: - **Node.js 24+** - For the Electron frontend - **npm 10+** - Package manager for the frontend (comes with Node.js) - **uv** (recommended) or **pip** - Python package manager +- **CMake** - Required for building native dependencies (e.g., LadybugDB) - **Git** - Version control ### Installing Python 3.12 @@ -54,6 +55,26 @@ brew install python@3.12 sudo apt install python3.12 python3.12-venv ``` +### Installing CMake + +**Windows:** + +```bash +winget install Kitware.CMake +``` + +**macOS:** + +```bash +brew install cmake +``` + +**Linux (Ubuntu/Debian):** + +```bash +sudo apt install cmake +``` + ## Quick Start The fastest way to get started: diff --git a/README.md b/README.md index b506a7e1..22c69171 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Auto Claude Kanban Board](.github/assets/Auto-Claude-Kanban.png) -[![Version](https://img.shields.io/badge/version-2.7.2-beta.10-blue?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/latest) +[![Version](https://img.shields.io/badge/version-2.7.2--beta.10-blue?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/latest) [![License](https://img.shields.io/badge/license-AGPL--3.0-green?style=flat-square)](./agpl-3.0.txt) [![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/KCXaPBr4Dj) [![CI](https://img.shields.io/github/actions/workflow/status/AndyMik90/Auto-Claude/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/AndyMik90/Auto-Claude/actions) diff --git a/apps/frontend/CONTRIBUTING.md b/apps/frontend/CONTRIBUTING.md index 30bf164a..2814803a 100644 --- a/apps/frontend/CONTRIBUTING.md +++ b/apps/frontend/CONTRIBUTING.md @@ -118,7 +118,7 @@ describe('TaskCard', () => { it('renders task title', () => { const task = { id: '1', title: 'Test Task' }; render(); - + expect(screen.getByText('Test Task')).toBeInTheDocument(); }); }); diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index ce812372..988799cf 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -196,7 +196,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -581,7 +580,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -625,7 +623,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -665,7 +662,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1072,6 +1068,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1093,6 +1090,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -4215,7 +4213,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4402,7 +4401,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4413,7 +4411,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4505,7 +4502,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4918,7 +4914,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4979,7 +4974,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5152,6 +5146,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5546,7 +5541,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6217,7 +6211,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -6567,7 +6562,6 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -6625,7 +6619,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dotenv": { "version": "16.6.1", @@ -6701,7 +6696,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -6830,6 +6824,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -6850,6 +6845,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6865,6 +6861,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6875,6 +6872,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7244,7 +7242,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8490,7 +8487,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -9281,7 +9277,6 @@ "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10213,6 +10208,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12403,7 +12399,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12501,7 +12496,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12538,6 +12532,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -12555,6 +12550,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -12575,6 +12571,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12590,6 +12587,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -12602,7 +12600,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/proc-log": { "version": "2.0.1", @@ -12706,7 +12705,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12716,7 +12714,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14036,8 +14033,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -14094,6 +14090,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14120,6 +14117,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14141,6 +14139,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14154,6 +14153,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -14168,6 +14168,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14484,7 +14485,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14834,7 +14834,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15876,7 +15875,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/apps/frontend/src/main/__tests__/ndjson-parser.test.ts b/apps/frontend/src/main/__tests__/ndjson-parser.test.ts index 880db8cb..8e554d9a 100644 --- a/apps/frontend/src/main/__tests__/ndjson-parser.test.ts +++ b/apps/frontend/src/main/__tests__/ndjson-parser.test.ts @@ -33,11 +33,11 @@ interface ProgressData { */ function parseNDJSON(chunk: string, bufferRef: { current: string }): ProgressData[] { const results: ProgressData[] = []; - + let stderrBuffer = bufferRef.current + chunk; const lines = stderrBuffer.split('\n'); stderrBuffer = lines.pop() || ''; - + lines.forEach((line) => { if (line.trim()) { try { @@ -48,7 +48,7 @@ function parseNDJSON(chunk: string, bufferRef: { current: string }): ProgressDat } } }); - + bufferRef.current = stderrBuffer; return results; } @@ -64,7 +64,7 @@ describe('NDJSON Parser', () => { it('should parse single JSON object', () => { const chunk = '{"status":"downloading","completed":100,"total":1000}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(results[0].status).toBe('downloading'); expect(results[0].completed).toBe(100); @@ -74,7 +74,7 @@ describe('NDJSON Parser', () => { it('should parse multiple JSON objects', () => { const chunk = '{"completed":100}\n{"completed":200}\n{"completed":300}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(3); expect(results[0].completed).toBe(100); expect(results[1].completed).toBe(200); @@ -86,7 +86,7 @@ describe('NDJSON Parser', () => { it('should preserve incomplete line in buffer', () => { const chunk = '{"completed":100}\n{"incomplete":true'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(bufferRef.current).toBe('{"incomplete":true'); }); @@ -96,7 +96,7 @@ describe('NDJSON Parser', () => { let results = parseNDJSON(chunk, bufferRef); expect(results).toHaveLength(1); expect(bufferRef.current).toBe('{"status":"down'); - + chunk = 'loading"}\n'; results = parseNDJSON(chunk, bufferRef); expect(results).toHaveLength(1); @@ -109,7 +109,7 @@ describe('NDJSON Parser', () => { it('should skip invalid JSON and continue', () => { const chunk = '{"completed":100}\nINVALID\n{"completed":200}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(2); expect(results[0].completed).toBe(100); expect(results[1].completed).toBe(200); @@ -118,7 +118,7 @@ describe('NDJSON Parser', () => { it('should skip empty lines', () => { const chunk = '{"completed":100}\n\n{"completed":200}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(2); }); }); @@ -133,7 +133,7 @@ describe('NDJSON Parser', () => { }); const chunk = ollamaProgress + '\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(results[0].status).toBe('downloading'); expect(results[0].completed).toBe(500000000); @@ -148,7 +148,7 @@ describe('NDJSON Parser', () => { ]; const chunk = updates.map(u => JSON.stringify(u)).join('\n') + '\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(3); expect(results[2].completed).toBe(300000000); }); @@ -156,7 +156,7 @@ describe('NDJSON Parser', () => { it('should handle success status', () => { const chunk = '{"status":"success","digest":"sha256:123"}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(results[0].status).toBe('success'); }); @@ -165,7 +165,7 @@ describe('NDJSON Parser', () => { describe('Streaming Scenarios', () => { it('should accumulate data across multiple chunks', () => { let allResults: ProgressData[] = []; - + // Simulate streaming 3 progress updates for (let i = 1; i <= 3; i++) { const chunk = JSON.stringify({ @@ -175,7 +175,7 @@ describe('NDJSON Parser', () => { const results = parseNDJSON(chunk, bufferRef); allResults = allResults.concat(results); } - + expect(allResults).toHaveLength(3); expect(allResults[2].completed).toBe(300000000); }); @@ -189,7 +189,7 @@ describe('NDJSON Parser', () => { }; const chunk = JSON.stringify(obj) + '\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(results[0].completed).toBe(123456789); }); @@ -197,7 +197,7 @@ describe('NDJSON Parser', () => { it('should handle very large numbers', () => { const chunk = '{"completed":999999999999,"total":1000000000000}\n'; const results = parseNDJSON(chunk, bufferRef); - + expect(results).toHaveLength(1); expect(results[0].completed).toBe(999999999999); expect(results[0].total).toBe(1000000000000); @@ -211,7 +211,7 @@ describe('NDJSON Parser', () => { let results = parseNDJSON(chunk, bufferRef); expect(results).toHaveLength(1); expect(bufferRef.current).toBe('{"other'); - + // Second call completes the incomplete data chunk = '":200}\n'; results = parseNDJSON(chunk, bufferRef); diff --git a/apps/frontend/src/main/__tests__/version-manager.test.ts b/apps/frontend/src/main/__tests__/version-manager.test.ts index c187f811..fb36bd37 100644 --- a/apps/frontend/src/main/__tests__/version-manager.test.ts +++ b/apps/frontend/src/main/__tests__/version-manager.test.ts @@ -1,6 +1,6 @@ /** * Tests for version-manager.ts - * + * * Tests the compareVersions function with various version formats * including pre-release versions (alpha, beta, rc). */ diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index fbcb27fa..4d1defb3 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -318,15 +318,15 @@ export class AgentProcessManager { }); const isDebug = ['true', '1', 'yes', 'on'].includes(process.env.DEBUG?.toLowerCase() ?? ''); - + const processLog = (line: string) => { allOutput = (allOutput + line).slice(-10000); - + const hasMarker = line.includes('__EXEC_PHASE__'); if (isDebug && hasMarker) { console.log(`[PhaseDebug:${taskId}] Found marker in line: "${line.substring(0, 200)}"`); } - + const phaseUpdate = this.events.parseExecutionPhase(line, currentPhase, isSpecRunner); if (isDebug && hasMarker) { @@ -335,11 +335,11 @@ export class AgentProcessManager { if (phaseUpdate) { const phaseChanged = phaseUpdate.phase !== currentPhase; - + if (isDebug) { console.log(`[PhaseDebug:${taskId}] Phase update: ${currentPhase} -> ${phaseUpdate.phase} (changed: ${phaseChanged})`); } - + currentPhase = phaseUpdate.phase; if (phaseUpdate.currentSubtask) { @@ -377,15 +377,15 @@ export class AgentProcessManager { console.log(`[PhaseDebug:${taskId}] Raw chunk with marker (${newData.length} bytes): "${newData.substring(0, 300)}"`); console.log(`[PhaseDebug:${taskId}] Current buffer before append (${buffer.length} bytes): "${buffer.substring(0, 100)}"`); } - + buffer += newData; const lines = buffer.split('\n'); const remaining = lines.pop() || ''; - + if (isDebug && newData.includes('__EXEC_PHASE__')) { console.log(`[PhaseDebug:${taskId}] Split into ${lines.length} complete lines, remaining buffer: "${remaining.substring(0, 100)}"`); } - + for (const line of lines) { if (line.trim()) { this.emitter.emit('log', taskId, line + '\n'); @@ -395,7 +395,7 @@ export class AgentProcessManager { } } } - + return remaining; }; @@ -416,7 +416,7 @@ export class AgentProcessManager { this.emitter.emit('log', taskId, stderrBuffer + '\n'); processLog(stderrBuffer); } - + this.state.deleteProcess(taskId); if (this.state.wasSpawnKilled(spawnId)) { diff --git a/apps/frontend/src/main/app-updater.ts b/apps/frontend/src/main/app-updater.ts index 3a6ee980..a76444dd 100644 --- a/apps/frontend/src/main/app-updater.ts +++ b/apps/frontend/src/main/app-updater.ts @@ -200,11 +200,11 @@ export async function checkForUpdates(): Promise { const currentVersion = autoUpdater.currentVersion.version; const latestVersion = result.updateInfo.version; - + // Use proper semver comparison to detect if update is actually newer // This prevents offering downgrades (e.g., v2.7.1 when on v2.7.2-beta.6) const isNewer = compareVersions(latestVersion, currentVersion) > 0; - + console.warn(`[app-updater] Version comparison: ${latestVersion} vs ${currentVersion} -> ${isNewer ? 'UPDATE' : 'NO UPDATE'}`); if (!isNewer) { diff --git a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts index b106ea37..7e098ecb 100644 --- a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts @@ -601,11 +601,11 @@ export function registerMemoryHandlers(): void { if (line.trim()) { try { const progressData = JSON.parse(line); - + // Extract progress information if (progressData.completed !== undefined && progressData.total !== undefined) { - const percentage = progressData.total > 0 - ? Math.round((progressData.completed / progressData.total) * 100) + const percentage = progressData.total > 0 + ? Math.round((progressData.completed / progressData.total) * 100) : 0; // Emit progress event to renderer diff --git a/apps/frontend/src/main/python-detector.ts b/apps/frontend/src/main/python-detector.ts index 899739ba..f8c80d20 100644 --- a/apps/frontend/src/main/python-detector.ts +++ b/apps/frontend/src/main/python-detector.ts @@ -359,7 +359,7 @@ function verifyIsPython(pythonCmd: string): boolean { windowsHide: true, shell: false }).toString().trim(); - + // Must output "Python X.Y.Z" return /^Python \d+\.\d+/.test(output); } catch { @@ -369,13 +369,13 @@ function verifyIsPython(pythonCmd: string): boolean { /** * Validate a Python path for security before use in spawn(). - * + * * Security checks: * 1. No shell metacharacters that could enable command injection * 2. Path must match allowlist of known Python locations OR be a safe command * 3. If a file path, must exist and be executable * 4. Must actually be Python (verified via --version) - * + * * @param pythonPath - The Python path or command to validate * @returns Validation result with success status and reason */ @@ -385,7 +385,7 @@ export function validatePythonPath(pythonPath: string): PythonPathValidation { } const trimmedPath = pythonPath.trim(); - + // Strip surrounding quotes for validation let cleanPath = trimmedPath; if ((cleanPath.startsWith('"') && cleanPath.endsWith('"')) || @@ -395,9 +395,9 @@ export function validatePythonPath(pythonPath: string): PythonPathValidation { // Security check 1: No shell metacharacters if (DANGEROUS_SHELL_CHARS.test(cleanPath)) { - return { - valid: false, - reason: 'Path contains dangerous shell metacharacters' + return { + valid: false, + reason: 'Path contains dangerous shell metacharacters' }; } @@ -407,56 +407,56 @@ export function validatePythonPath(pythonPath: string): PythonPathValidation { if (verifyIsPython(cleanPath)) { return { valid: true, sanitizedPath: cleanPath }; } - return { - valid: false, - reason: `Command '${cleanPath}' does not appear to be Python` + return { + valid: false, + reason: `Command '${cleanPath}' does not appear to be Python` }; } // It's a file path - apply stricter validation const isFilePath = cleanPath.includes('/') || cleanPath.includes('\\'); - + if (isFilePath) { // Normalize the path to prevent directory traversal tricks const normalizedPath = path.normalize(cleanPath); - + // Check for path traversal attempts if (normalizedPath.includes('..')) { - return { - valid: false, - reason: 'Path contains directory traversal sequences' + return { + valid: false, + reason: 'Path contains directory traversal sequences' }; } // Security check 2: Must match allowlist if (!matchesAllowedPattern(normalizedPath)) { - return { - valid: false, - reason: 'Path does not match allowed Python locations. Expected: system Python, Homebrew, pyenv, or virtual environment paths' + return { + valid: false, + reason: 'Path does not match allowed Python locations. Expected: system Python, Homebrew, pyenv, or virtual environment paths' }; } // Security check 3: File must exist if (!existsSync(normalizedPath)) { - return { - valid: false, - reason: 'Python executable does not exist at specified path' + return { + valid: false, + reason: 'Python executable does not exist at specified path' }; } // Security check 4: Must be executable (Unix) or .exe (Windows) if (process.platform !== 'win32' && !isExecutable(normalizedPath)) { - return { - valid: false, - reason: 'File exists but is not executable' + return { + valid: false, + reason: 'File exists but is not executable' }; } // Security check 5: Verify it's actually Python if (!verifyIsPython(normalizedPath)) { - return { - valid: false, - reason: 'File exists but does not appear to be a Python interpreter' + return { + valid: false, + reason: 'File exists but does not appear to be a Python interpreter' }; } @@ -464,9 +464,9 @@ export function validatePythonPath(pythonPath: string): PythonPathValidation { } // Unknown format - reject - return { - valid: false, - reason: 'Unrecognized Python path format' + return { + valid: false, + reason: 'Unrecognized Python path format' }; } @@ -474,12 +474,12 @@ export function getValidatedPythonPath(providedPath: string | undefined, service if (!providedPath) { return findPythonCommand() || 'python'; } - + const validation = validatePythonPath(providedPath); if (validation.valid) { return validation.sanitizedPath || providedPath; } - + console.error(`[${serviceName}] Invalid Python path rejected: ${validation.reason}`); return findPythonCommand() || 'python'; } diff --git a/apps/frontend/src/main/updater/version-manager.ts b/apps/frontend/src/main/updater/version-manager.ts index bc850e22..92edcb8b 100644 --- a/apps/frontend/src/main/updater/version-manager.ts +++ b/apps/frontend/src/main/updater/version-manager.ts @@ -94,20 +94,20 @@ export function parseVersionFromTag(tag: string): string { /** * Parse a version string into its components * Handles versions like "2.7.2", "2.7.2-beta.6", "2.7.2-alpha.1" - * + * * @returns { base: number[], prerelease: { type: string, num: number } | null } */ -function parseVersion(version: string): { - base: number[]; - prerelease: { type: string; num: number } | null +function parseVersion(version: string): { + base: number[]; + prerelease: { type: string; num: number } | null } { // Split into base version and prerelease suffix // e.g., "2.7.2-beta.6" -> ["2.7.2", "beta.6"] const [baseStr, prereleaseStr] = version.split('-'); - + // Parse base version numbers const base = baseStr.split('.').map(n => parseInt(n, 10) || 0); - + // Parse prerelease if present let prerelease: { type: string; num: number } | null = null; if (prereleaseStr) { @@ -120,14 +120,14 @@ function parseVersion(version: string): { }; } } - + return { base, prerelease }; } /** * Compare semantic versions with proper pre-release support * Returns: 1 if a > b, -1 if a < b, 0 if equal - * + * * Pre-release ordering: * - alpha < beta < rc < stable (no prerelease) * - 2.7.2-beta.1 < 2.7.2-beta.2 < 2.7.2 (stable) @@ -136,34 +136,34 @@ function parseVersion(version: string): { export function compareVersions(a: string, b: string): number { const parsedA = parseVersion(a); const parsedB = parseVersion(b); - + // Compare base versions first const maxLen = Math.max(parsedA.base.length, parsedB.base.length); for (let i = 0; i < maxLen; i++) { const numA = parsedA.base[i] || 0; const numB = parsedB.base[i] || 0; - + if (numA > numB) return 1; if (numA < numB) return -1; } - + // Base versions are equal, compare prereleases // No prerelease = stable = higher than any prerelease of same base if (!parsedA.prerelease && !parsedB.prerelease) return 0; if (!parsedA.prerelease && parsedB.prerelease) return 1; // a is stable, b is prerelease if (parsedA.prerelease && !parsedB.prerelease) return -1; // a is prerelease, b is stable - + // Both have prereleases - compare type then number const prereleaseOrder: Record = { alpha: 0, beta: 1, rc: 2 }; const typeA = prereleaseOrder[parsedA.prerelease!.type] ?? 1; const typeB = prereleaseOrder[parsedB.prerelease!.type] ?? 1; - + if (typeA > typeB) return 1; if (typeA < typeB) return -1; - + // Same prerelease type, compare numbers if (parsedA.prerelease!.num > parsedB.prerelease!.num) return 1; if (parsedA.prerelease!.num < parsedB.prerelease!.num) return -1; - + return 0; } diff --git a/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx b/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx index 03129043..d0bcad92 100644 --- a/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx @@ -102,7 +102,7 @@ export function OllamaModelSelector({ const [error, setError] = useState(null); const [ollamaAvailable, setOllamaAvailable] = useState(true); const [downloadProgress, setDownloadProgress] = useState({}); - + // Track previous progress for speed calculation const downloadProgressRef = useRef<{ [modelName: string]: { @@ -193,7 +193,7 @@ export function OllamaModelSelector({ percentage: number; }) => { const now = Date.now(); - + // Initialize tracking for this model if needed if (!downloadProgressRef.current[data.modelName]) { downloadProgressRef.current[data.modelName] = { @@ -209,12 +209,12 @@ export function OllamaModelSelector({ // Calculate speed only if we have meaningful time delta (> 100ms) let speedStr = ''; let timeStr = ''; - + if (timeDelta > 100 && bytesDelta > 0) { const speed = (bytesDelta / timeDelta) * 1000; // bytes per second const remaining = data.total - data.completed; const timeRemaining = speed > 0 ? Math.ceil(remaining / speed) : 0; - + // Format speed (MB/s or KB/s) if (speed > 1024 * 1024) { speedStr = `${(speed / (1024 * 1024)).toFixed(1)} MB/s`; diff --git a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts index 753083ea..943ff0e7 100644 --- a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts @@ -110,7 +110,7 @@ export const infrastructureMock = { }) => void) => { // Store callback for test verification (window as any).__downloadProgressCallback = callback; - + // Return cleanup function return () => { delete (window as any).__downloadProgressCallback; diff --git a/apps/frontend/src/renderer/stores/task-store.ts b/apps/frontend/src/renderer/stores/task-store.ts index f88ec08e..43ba85a5 100644 --- a/apps/frontend/src/renderer/stores/task-store.ts +++ b/apps/frontend/src/renderer/stores/task-store.ts @@ -52,7 +52,7 @@ export const useTaskStore = create((set, get) => ({ // Determine execution progress based on status transition let executionProgress = t.executionProgress; - + if (status === 'backlog') { // When status goes to backlog, reset execution progress to idle // This ensures the planning/coding animation stops when task is stopped