From 9e58a1de8bdb12f0fc464d2a90c076a36d78f54d Mon Sep 17 00:00:00 2001 From: zzxwill Date: Sun, 25 Oct 2026 18:34:03 +0800 Subject: [PATCH] feat: offer global CLI upgrades from both plugins --- CHANGELOG.md | 4 + docs/codex-plugin.md | 8 +- plugin/README.md | 3 +- plugin/scripts/jury.mjs | 33 ++++++++ plugin/skills/codejury/SKILL.md | 7 ++ plugin/skills/review/SKILL.md | 16 +++- plugins/codejury/plugin.json | 6 +- plugins/codejury/scripts/jury.mjs | 33 ++++++++ .../codejury/skills/codejury-results/SKILL.md | 7 ++ .../codejury/skills/codejury-review/SKILL.md | 7 ++ .../codejury/skills/codejury-setup/SKILL.md | 11 ++- test/codex-plugin.test.js | 76 ++++++++++++++++++- 12 files changed, 196 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fbdba..85cdad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Have both plugins check the global CLI against npm's latest stable version at + startup and offer an upgrade before continuing. Existing installations change + only after the user agrees; offline update checks keep the current CLI usable. + ## 0.10.0 — 2026-10-25 - Make Codex and Claude plugins reuse the global CodeJury CLI, install globally diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 45be651..ff2d1f0 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -16,7 +16,8 @@ In a new Codex chat, select **CodeJury** and ask: Setup checks prerequisites without starting a review. The plugin uses your global CodeJury CLI, or installs it globally with `npm install --global @agentsdance/codejury@latest` -if missing. Agent installation and login are separate. +if missing. If a newer stable CLI is available, it asks whether to upgrade or +keep your current version. Agent installation and login are separate. ## Review a PR @@ -45,6 +46,11 @@ checks npm's global package directory and reuses CodeJury there. If missing, it installs globally; it never creates a separate plugin CLI installation. The plugin and CLI have separate versions. +The startup `version` check compares the installed CLI with npm's `latest` stable +release. The host asks before upgrading an existing CLI; ordinary CLI commands +do not repeat the lookup. The registry check times out after three seconds, and +offline or invalid responses leave the installed CLI usable. + Build the submission archive from the repository root: ```sh diff --git a/plugin/README.md b/plugin/README.md index 210482c..547f72f 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -4,7 +4,8 @@ Review and fix pull requests with a Claude reviewer and a separate Claude judge. You need Node.js 20.10+, npm, Git, and a signed-in Claude Code CLI. GitHub PRs also need a signed-in `gh` CLI. The plugin checks your global `jury` installation -and installs it globally if missing. +and installs it globally if missing. If a newer stable CLI is available, it asks +whether to upgrade or keep your current version. ## Install diff --git a/plugin/scripts/jury.mjs b/plugin/scripts/jury.mjs index f4bdaff..336bbb6 100644 --- a/plugin/scripts/jury.mjs +++ b/plugin/scripts/jury.mjs @@ -18,6 +18,38 @@ function npm(args, capture = false) { return capture ? result.stdout.trim() : undefined; } +function versionParts(version) { + return typeof version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version); +} + +function checkForUpdate(currentVersion) { + // The host runs `version` once before the operation and presents the choice + // in chat. Never wait for terminal input or upgrade an existing CLI here. + try { + const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', [ + 'view', '@agentsdance/codejury@latest', 'version', '--json', '--prefer-online', + '--fetch-retries=0', '--fetch-timeout=2000', + ], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 3000, + maxBuffer: 16 * 1024, shell: process.platform === 'win32', + }); + if (result.error || result.status !== 0) throw new Error('Registry unavailable'); + const latestVersion = JSON.parse(result.stdout); + const current = versionParts(currentVersion); + const latest = versionParts(latestVersion); + if (!current || !latest || latest[4]) throw new Error('Invalid stable version'); + const difference = [1, 2, 3].find(index => BigInt(latest[index]) !== BigInt(current[index])); + const newer = difference ? BigInt(latest[difference]) > BigInt(current[difference]) : Boolean(current[4]); + if (newer) { + console.error(`CodeJury update available: ${currentVersion} -> ${latestVersion}`); + console.error('Upgrade the global CLI? Ask the user before upgrading; they can keep the current version.'); + console.error(`To upgrade: npm install --global @agentsdance/codejury@${latestVersion}`); + } + } catch { + console.error(`Update check unavailable; continuing with global CodeJury ${currentVersion}.`); + } +} + const globalRoot = npm(['root', '--global'], true); if (!path.isAbsolute(globalRoot)) throw new Error('npm did not return an absolute global package directory.'); const packageRoot = path.join(globalRoot, '@agentsdance', 'codejury'); @@ -37,6 +69,7 @@ if (!metadata) { } if (!metadata) throw new Error(`Global CodeJury installation is incomplete at ${packageRoot}. Run npm install --global @agentsdance/codejury@latest and retry.`); console.error(`Using global CodeJury ${metadata.version} at ${entry}`); +if (['version', '--version', '-v'].includes(process.argv[2])) checkForUpdate(metadata.version); // Import in this process so cancellation reaches the CLI and its agent cleanup. process.argv = [process.execPath, entry, ...process.argv.slice(2)]; diff --git a/plugin/skills/codejury/SKILL.md b/plugin/skills/codejury/SKILL.md index 02a0fab..7bf63d6 100644 --- a/plugin/skills/codejury/SKILL.md +++ b/plugin/skills/codejury/SKILL.md @@ -23,6 +23,13 @@ checks npm's global CodeJury installation and installs globally if missing; it s on errors instead of using npx or a private copy. Use this runner in place of `jury` in the examples below. Keep the same `--dir` and `--run` for saved findings. +The `version` check also looks for a newer stable CLI on npm. If an update is +available, show both versions and ask whether to **upgrade globally** or **keep +the current version**. Run the printed npm install command only after the user +agrees, then rerun `version` to verify. If they decline or explicitly chose a +version, keep it. An unavailable update check does not block the operation. +Do not repeat this prompt during the same operation. + ## The loop ``` diff --git a/plugin/skills/review/SKILL.md b/plugin/skills/review/SKILL.md index e63be27..7051949 100644 --- a/plugin/skills/review/SKILL.md +++ b/plugin/skills/review/SKILL.md @@ -32,6 +32,13 @@ CLI path, and stops on installation errors. Node.js 20.10+ and npm are required. Use this runner in place of `jury` in every command below, including `agents`, `runs`, and reopening the console. Never use npx or install a private CLI copy. +The `version` check also looks for a newer stable CLI on npm. If an update is +available, show both versions and ask whether to **upgrade globally** or **keep +the current version** before starting the review. Run the printed npm install +command only after the user agrees, then rerun `version` to verify. If they +decline or explicitly chose a version, keep it. An unavailable update check does +not block the review. Do not repeat this prompt during the same operation. + ## 3. Build the command Start from `node "/scripts/jury.mjs" review`. Normalize the role flags as @@ -69,10 +76,11 @@ missing or unauthenticated agent CLI, a missing `gh` login for a GitHub URL), sh the fix it names; `jury agents` lists which agent CLIs are installed. Older CLIs, including 0.8.1, reject a reviewer that is also the judge. If that specific error -appears, explain that the global CLI is too old. Upgrade it with -`npm install --global @agentsdance/codejury@latest`, then retry once through the -same runner with the same normalized arguments. Honor an explicitly requested -CLI version instead of upgrading it silently. If the upgrade or retry fails, +appears, explain that the global CLI is too old. Offer +`npm install --global @agentsdance/codejury@latest`; only after the user agrees, +upgrade and retry once through the same runner with the same normalized arguments. +If they already declined the startup offer or explicitly chose the old version, +explain the incompatibility and stop without asking again. If the upgrade or retry fails, report the error and stop. Do not use npx, a private install, or another agent as a fallback. Disabled Claude or missing authentication still needs to be fixed. diff --git a/plugins/codejury/plugin.json b/plugins/codejury/plugin.json index 478ab64..0bd8e9d 100644 --- a/plugins/codejury/plugin.json +++ b/plugins/codejury/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codejury", - "version": "0.1.4", + "version": "0.1.5", "description": "Review pull requests supplied by URL with CodeJury's independent AI reviewers and judge, resume PR reviews, and inspect saved findings.", "author": { "name": "ZHOU ZHENGXI", @@ -22,7 +22,7 @@ "interface": { "displayName": "CodeJury", "shortDescription": "AI pull request review", - "longDescription": "Review pull requests supplied by URL with independent coding agents and a judge that investigates findings, applies fixes, and asks reviewers to check again. Set up the CLI, run or resume a PR review, and inspect saved findings and conversations. A new review requires a PR link; arbitrary local files and folders are not supported plugin review targets. PR reviews use a local checkout. Desktop only: requires Node.js 20.10+, npm, Git, and separately installed and authenticated coding-agent CLIs. GitHub PRs also require GitHub CLI. This plugin reuses your npm-global CodeJury CLI, or installs the latest stable CLI globally if missing. It does not create a private CLI installation. Reviews can edit files, create commits, and push fixes to the PR by default; ask to keep fixes local or use --push=false to disable pushing. Code and prompts are processed by your selected agents and their providers, subject to their terms and usage charges. CodeJury keeps review records locally and operates no hosted review service. The included console screenshot shows a real prior review; results vary by repository and agent.", + "longDescription": "Review pull requests supplied by URL with independent coding agents and a judge that investigates findings, applies fixes, and asks reviewers to check again. Set up the CLI, run or resume a PR review, and inspect saved findings and conversations. A new review requires a PR link; arbitrary local files and folders are not supported plugin review targets. PR reviews use a local checkout. Desktop only: requires Node.js 20.10+, npm, Git, and separately installed and authenticated coding-agent CLIs. GitHub PRs also require GitHub CLI. This plugin reuses your npm-global CodeJury CLI, or installs the latest stable CLI globally if missing. It checks for newer stable CLI releases and asks before upgrading an existing installation. It does not create a private CLI installation. Reviews can edit files, create commits, and push fixes to the PR by default; ask to keep fixes local or use --push=false to disable pushing. Code and prompts are processed by your selected agents and their providers, subject to their terms and usage charges. CodeJury keeps review records locally and operates no hosted review service. The included console screenshot shows a real prior review; results vary by repository and agent.", "developerName": "ZHOU ZHENGXI", "category": "Developer Tools", "capabilities": [ @@ -47,7 +47,7 @@ ] }, "publication": { - "release_notes": "Reuse the global CodeJury CLI and install globally when missing. Remove private CLI downloads and stop on installation errors." + "release_notes": "Check the global CLI for newer stable releases during setup and ask before upgrading. Keep the installed CLI usable when the update check is unavailable." } } } diff --git a/plugins/codejury/scripts/jury.mjs b/plugins/codejury/scripts/jury.mjs index f4bdaff..336bbb6 100644 --- a/plugins/codejury/scripts/jury.mjs +++ b/plugins/codejury/scripts/jury.mjs @@ -18,6 +18,38 @@ function npm(args, capture = false) { return capture ? result.stdout.trim() : undefined; } +function versionParts(version) { + return typeof version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version); +} + +function checkForUpdate(currentVersion) { + // The host runs `version` once before the operation and presents the choice + // in chat. Never wait for terminal input or upgrade an existing CLI here. + try { + const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', [ + 'view', '@agentsdance/codejury@latest', 'version', '--json', '--prefer-online', + '--fetch-retries=0', '--fetch-timeout=2000', + ], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 3000, + maxBuffer: 16 * 1024, shell: process.platform === 'win32', + }); + if (result.error || result.status !== 0) throw new Error('Registry unavailable'); + const latestVersion = JSON.parse(result.stdout); + const current = versionParts(currentVersion); + const latest = versionParts(latestVersion); + if (!current || !latest || latest[4]) throw new Error('Invalid stable version'); + const difference = [1, 2, 3].find(index => BigInt(latest[index]) !== BigInt(current[index])); + const newer = difference ? BigInt(latest[difference]) > BigInt(current[difference]) : Boolean(current[4]); + if (newer) { + console.error(`CodeJury update available: ${currentVersion} -> ${latestVersion}`); + console.error('Upgrade the global CLI? Ask the user before upgrading; they can keep the current version.'); + console.error(`To upgrade: npm install --global @agentsdance/codejury@${latestVersion}`); + } + } catch { + console.error(`Update check unavailable; continuing with global CodeJury ${currentVersion}.`); + } +} + const globalRoot = npm(['root', '--global'], true); if (!path.isAbsolute(globalRoot)) throw new Error('npm did not return an absolute global package directory.'); const packageRoot = path.join(globalRoot, '@agentsdance', 'codejury'); @@ -37,6 +69,7 @@ if (!metadata) { } if (!metadata) throw new Error(`Global CodeJury installation is incomplete at ${packageRoot}. Run npm install --global @agentsdance/codejury@latest and retry.`); console.error(`Using global CodeJury ${metadata.version} at ${entry}`); +if (['version', '--version', '-v'].includes(process.argv[2])) checkForUpdate(metadata.version); // Import in this process so cancellation reaches the CLI and its agent cleanup. process.argv = [process.execPath, entry, ...process.argv.slice(2)]; diff --git a/plugins/codejury/skills/codejury-results/SKILL.md b/plugins/codejury/skills/codejury-results/SKILL.md index 20c096d..1bf0ef2 100644 --- a/plugins/codejury/skills/codejury-results/SKILL.md +++ b/plugins/codejury/skills/codejury-results/SKILL.md @@ -12,6 +12,13 @@ Installation errors stop the operation; do not use npx or a private CLI copy. Respect an explicitly requested CLI version, install it globally if needed, and check its help for supported flags. +Run `version` once before inspecting results. It checks npm for a newer stable +CLI. If an update is available, show both versions and ask whether to **upgrade +globally** or **keep the current version**. Run the printed npm install command +only after the user agrees, then rerun `version` to verify. If they decline or +explicitly chose a version, keep it. An unavailable update check does not block +reading saved results. Do not repeat this prompt during the same operation. + 1. Identify the original state root. URL reviews default to `~/.jury`; use `--dir ""` to select it explicitly. A local-checkout review stores `runs/` under that checkout. Do not assume the current repository owns a URL run. diff --git a/plugins/codejury/skills/codejury-review/SKILL.md b/plugins/codejury/skills/codejury-review/SKILL.md index 7ff0932..9002a42 100644 --- a/plugins/codejury/skills/codejury-review/SKILL.md +++ b/plugins/codejury/skills/codejury-review/SKILL.md @@ -29,6 +29,13 @@ version and absolute path. Installation errors stop the run; do not use npx or a private CLI copy. If the user requests a different version, install that version globally and inspect its help. +Run `version` once before starting or resuming a review. It also checks npm for a +newer stable CLI. If an update is available, show both versions and ask whether +to **upgrade globally** or **keep the current version**. Run the printed npm +install command only after the user agrees, then rerun `version` to verify. +If they decline or explicitly chose a version, keep it. An unavailable update +check does not block the review. Do not repeat this prompt during the same operation. + Check Node.js 20.10+, npm, Git, and the requested agent CLIs. For GitHub PRs, check GitHub CLI authentication. Run `jury agents` to discover executables; this does not verify provider login or quota. Fix missing prerequisites before diff --git a/plugins/codejury/skills/codejury-setup/SKILL.md b/plugins/codejury/skills/codejury-setup/SKILL.md index 44f127d..80a4475 100644 --- a/plugins/codejury/skills/codejury-setup/SKILL.md +++ b/plugins/codejury/skills/codejury-setup/SKILL.md @@ -21,16 +21,19 @@ selected, without starting a review. ```sh node "/scripts/jury.mjs" version - node "/scripts/jury.mjs" agents ``` The launcher checks `npm root --global` for CodeJury and reuses that global installation. If missing, it runs `npm install --global @agentsdance/codejury@latest`. It reports the version and absolute CLI path. Installation errors stop setup; never use npx, a custom prefix, or a plugin-private CLI copy as a fallback. - Existing versions are not upgraded automatically. For a requested version, - install that version globally and check its help before using its options. -3. `jury agents` checks executable availability, not authentication or quota. + `version` also checks npm for a newer stable CLI. If it reports an update, + show both versions and ask whether to **upgrade globally** or **keep the current + version** before continuing. Run the printed npm install command only after + the user agrees, then rerun `version` to verify. If they decline or explicitly + chose a version, keep it. An unavailable update check does not block setup. +3. Run `node "/scripts/jury.mjs" agents`. This checks executable + availability, not authentication or quota. Report these separately. Consult the CLI's built-in install hints for missing agents. Let the user complete provider sign-in; do not collect credentials into the plugin or copy credentials between providers. diff --git a/test/codex-plugin.test.js b/test/codex-plugin.test.js index d15d1b3..bbe26e4 100644 --- a/test/codex-plugin.test.js +++ b/test/codex-plugin.test.js @@ -8,7 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const launchers = ['plugins/codejury/scripts/jury.mjs', 'plugin/scripts/jury.mjs']; -function fixture(t, { installed = false, windows = process.platform === 'win32', mode = 'ok' } = {}) { +function fixture(t, { installed = false, windows = process.platform === 'win32', mode = 'ok', version = '0.9.0', latest = '0.9.0' } = {}) { const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'codejury-global-'))); t.after(() => rmSync(root, { recursive: true, force: true })); const bin = path.join(root, 'bin'); @@ -23,7 +23,7 @@ function fixture(t, { installed = false, windows = process.platform === 'win32', // A project-local CLI must never shadow the global installation. writeFileSync(path.join(bin, windows ? 'jury.cmd' : 'jury'), windows ? '@exit /b 99\r\n' : '#!/bin/sh\nexit 99\n', { mode: 0o755 }); const cli = `console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2), entry: process.argv[1] })); process.exitCode = 7;`; - const metadata = JSON.stringify({ name: '@agentsdance/codejury', version: '0.9.0', type: 'module' }); + const metadata = JSON.stringify({ name: '@agentsdance/codejury', version, type: 'module' }); if (installed) { mkdirSync(path.dirname(entry), { recursive: true }); writeFileSync(path.join(pkg, 'package.json'), metadata); @@ -37,6 +37,11 @@ function fixture(t, { installed = false, windows = process.platform === 'win32', if (args[0] === 'root' && args[1] === '--global') { if (${JSON.stringify(mode)} === 'root-fails') process.exit(13); console.log(${JSON.stringify(globalRoot)}); + } else if (args[0] === 'view') { + if (${JSON.stringify(mode)} === 'registry-fails') process.exit(13); + if (${JSON.stringify(mode)} === 'registry-stalls') setInterval(() => {}, 1000); + else if (${JSON.stringify(mode)} === 'invalid-json') console.log('not json'); + else console.log(JSON.stringify(${JSON.stringify(latest)})); } else if (args[0] === 'install') { if (!args.includes('--global') || args.includes('--prefix')) throw new Error('installation must use the configured global prefix'); if (!args.includes('@agentsdance/codejury@latest')) throw new Error('expected the latest stable global CLI'); @@ -96,8 +101,75 @@ for (const launcher of launchers) { assert.equal(existsSync(f.cache), false); assert.equal(existsSync(path.join(f.cwd, 'node_modules')), false); }); + + test(`${label} version preflight offers a newer global CLI without upgrading`, t => { + const f = fixture(t, { installed: true, windows, version: '0.9.9', latest: '0.10.0' }); + const result = f.run(launcher); + assert.equal(result.status, 7, result.stderr); + assert.match(result.stderr, /CodeJury update available: 0\.9\.9 -> 0\.10\.0/); + assert.match(result.stderr, /Upgrade the global CLI\?/); + assert.match(result.stderr, /npm install --global @agentsdance\/codejury@0\.10\.0/); + assert.deepEqual(JSON.parse(result.stdout), { cwd: f.cwd, argv: ['version'], entry: f.entry }); + assert.equal(f.calls().filter(args => args[0] === 'view').length, 1); + assert.equal(f.calls().filter(args => args[0] === 'install').length, 0); + assert.equal(existsSync(f.cache), false); + }); + } + + test(`${launcher} compares stable versions numerically and respects prereleases and build metadata`, t => { + for (const [version, latest, update] of [ + ['0.10.0', '0.10.0', false], + ['0.11.0', '0.10.0', false], + ['0.11.0-rc.1', '0.10.0', false], + ['0.10.0+local', '0.10.0', false], + ['0.10.0', '0.10.0+build.2', false], + ['0.10.0-rc.1', '0.10.0', true], + ['0.10.0', '0.10.1', true], + ['0.10.0', '1.0.0', true], + ]) { + const f = fixture(t, { installed: true, version, latest }); + const result = f.run(launcher); + assert.equal(result.status, 7, result.stderr); + assert.equal(result.stderr.includes('CodeJury update available:'), update, `${version} -> ${latest}`); + assert.doesNotMatch(result.stderr, /Update check unavailable/); + assert.equal(f.calls().filter(args => args[0] === 'install').length, 0); + } + }); + + test(`${launcher} checks version aliases without checking ordinary CLI commands`, t => { + const f = fixture(t, { installed: true, latest: '0.10.0' }); + for (const command of ['--version', '-v', 'agents', 'runs', 'help']) { + const result = f.run(launcher, [command]); + assert.equal(result.status, 7, result.stderr); + assert.equal(result.stderr.includes('CodeJury update available:'), ['--version', '-v'].includes(command)); + } + assert.equal(f.calls().filter(args => args[0] === 'view').length, 2); + }); + + for (const mode of ['registry-fails', 'registry-stalls', 'invalid-json']) { + test(`${launcher} continues with the installed CLI when ${mode}`, t => { + const f = fixture(t, { installed: true, mode }); + const started = Date.now(); + const result = f.run(launcher); + assert.equal(result.status, 7, result.stderr); + assert.match(result.stderr, /Update check unavailable/); + assert.doesNotMatch(result.stderr, /CodeJury update available/); + assert.equal(f.calls().filter(args => args[0] === 'install').length, 0); + assert.ok(Date.now() - started < 8000, 'registry check must be bounded'); + }); } + test(`${launcher} ignores malformed, missing, or prerelease latest metadata`, t => { + for (const latest of ['0.11.0-rc.1', '0.10', '01.0.0', '0.10.1; touch unwanted', { error: 'not found' }, null]) { + const f = fixture(t, { installed: true, latest }); + const result = f.run(launcher); + assert.equal(result.status, 7, result.stderr); + assert.match(result.stderr, /Update check unavailable/); + assert.doesNotMatch(result.stderr, /CodeJury update available|touch unwanted/); + assert.equal(f.calls().filter(args => args[0] === 'install').length, 0); + } + }); + for (const mode of ['root-fails', 'install-fails', 'empty-install']) { test(`${launcher} stops on ${mode} without falling back to a private cache`, t => { const f = fixture(t, { mode });