From 1fa7a9c7698328dbc5c43a54c6fdf5ddc2de1616 Mon Sep 17 00:00:00 2001 From: Ian <251394470+ianstantiate@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:31:50 -0500 Subject: [PATCH] fix: Resolve pre-commit hook failures with version sync, pytest path, ruff version, and broken quality-dco workflow (#334) * fix: Fixes issues to get clean pre-commit run 1. version-sync hook was failing due to formatting, fixed with block scalar. 2. Python Tests step was failing because it could not locate python or pytest. Fixed by referencing pytest in .venv, [as was shown here in CONTRIBUTING.md](https://github.com/AndyMik90/Auto-Claude/blob/develop/CONTRIBUTING.md?plain=1#L299) At this point pre-commit could run, then there were a few issues it found that had to be fixed: 3. "check yaml" hook failed for the file ".github/workflows/quality-dco.yml". Fixed indenting issue. 4. Various files had whitespace issues that were auto-fixed by the pre-commit commands. After this, "pre-commit run --all-files" passes for all checks. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * docs: Update CONTRIBUTING.md with cmake dependency cmake is not present by default on macs, can be installed via homebrew Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Addressed PR comments on file consistency and install instructions. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Ran pre-commit autoupdate, disabled broken quality-dco workflow The version of ruff in pre-commit was on a much older version than what was running as part of the lint github workflow. This caused it to make changes that were rejected by the newer version. As far as disabling quality-dco workflow; according to https://github.com/AndyMik90/Auto-Claude/actions/workflows/quality-dco.yml, it has never actually successfully parsed since it was introduced in https://github.com/AndyMik90/Auto-Claude/pull/266, and so it has not been running on any PRs to date. Given that, plus the fact that I see no mention/discussion of Developer Certificate of Origin in any github issues or the discord, I will run with the assumption this needs more explicit discussion before we turn it on and force all contributors to add these signoffs for every commit. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Fixed bad sed command in pre-commit version-sync hook It resulted in bad version names being produced for beta versions, such as "Auto-Claude-2.7.2-beta.9-beta.9-arm64.dmg". Also addressed PR comment for needed spacing in markdown code blocks. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Fixed other sed command for version sync to avoid incorrect names Addresses PR comment to keep this in line with existing sed command fix in the same PR. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Keep version of ruff in sync between pre-commit and github workflow This will avoid situations where the checks done locally and in CI start to diverge and even conflict Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Enabling DCO workflow Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Fixed windows compatibility issue for running pytest Also committing some more file whitespace changes made by the working pre-commit hook. Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Removed out of date disabled banner on quality dco workflow Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> * Fixed version-sync issue with incorrect version badge image url, fixed dco workflow Updated readme with correct value as well Fixed DCO workflow as it was pointing at a nonexistent step. Improved DCO workflow failure message to warn about accidentally signing others commits. --------- Signed-off-by: Ian <251394470+ianstantiate@users.noreply.github.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com> --- .github/workflows/lint.yml | 4 +- .github/workflows/quality-dco.yml | 101 ++++++++++++------ .github/workflows/stale.yml | 2 +- .husky/pre-commit | 12 +-- .pre-commit-config.yaml | 72 ++++++++++--- CONTRIBUTING.md | 21 ++++ README.md | 2 +- apps/frontend/CONTRIBUTING.md | 2 +- apps/frontend/package-lock.json | 54 +++++----- .../src/main/__tests__/ndjson-parser.test.ts | 34 +++--- .../main/__tests__/version-manager.test.ts | 2 +- apps/frontend/src/main/agent/agent-process.ts | 20 ++-- apps/frontend/src/main/app-updater.ts | 4 +- .../src/main/ipc-handlers/memory-handlers.ts | 6 +- apps/frontend/src/main/python-detector.ts | 64 +++++------ .../src/main/updater/version-manager.ts | 30 +++--- .../onboarding/OllamaModelSelector.tsx | 8 +- .../renderer/lib/mocks/infrastructure-mock.ts | 2 +- .../src/renderer/stores/task-store.ts | 2 +- 19 files changed, 269 insertions(+), 173 deletions(-) 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