From f89a56eb55c5757e245a6ee923a41a0904f40402 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 26 Apr 2026 10:20:41 -0400 Subject: [PATCH 1/3] fix(graphify): update CLI invocation from legacy flag form to subcommand graphify . --update was removed in favor of graphify update . in v0.4.x. Also improves version detection to try `graphify --version` before falling back to python3 importlib query. Closes #2732 --- commands/gsd/graphify.md | 2 +- get-shit-done/bin/lib/graphify.cjs | 45 ++++++++++++++++++++++++------ tests/graphify.test.cjs | 32 ++++++++++++++++----- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/commands/gsd/graphify.md b/commands/gsd/graphify.md index 52ecacdf7..dcdb7cc8e 100644 --- a/commands/gsd/graphify.md +++ b/commands/gsd/graphify.md @@ -153,7 +153,7 @@ gsd-tools path: $HOME/.claude/get-shit-done/bin/gsd-tools.cjs 1. **Invoke graphify:** Run from the project root: ``` - graphify . --update + graphify update . ``` This builds the knowledge graph with SHA256 incremental caching. Timeout: up to 5 minutes (or as configured via graphify.build_timeout). diff --git a/get-shit-done/bin/lib/graphify.cjs b/get-shit-done/bin/lib/graphify.cjs index 5a7fa2a87..a31e0fa2f 100644 --- a/get-shit-done/bin/lib/graphify.cjs +++ b/get-shit-done/bin/lib/graphify.cjs @@ -102,26 +102,55 @@ function checkGraphifyInstalled() { } /** - * Detect graphify version via python3 importlib.metadata and check compatibility. + * Detect graphify version and check compatibility. * Tested range: >=0.4.0,<1.0 * + * Detection strategy: + * 1. Try `graphify --version` (works for most CLI installations, incl. venv installs) + * 2. Fall back to python3 importlib.metadata (legacy / system Python path) + * 3. Return null version gracefully if both fail + * * @returns {{ version: string|null, compatible: boolean|null, warning: string|null }} */ function checkGraphifyVersion() { - const result = childProcess.spawnSync('python3', [ - '-c', - 'from importlib.metadata import version; print(version("graphifyy"))', - ], { + // Strategy 1: try `graphify --version` directly (2s timeout -- fast path) + const versionResult = childProcess.spawnSync('graphify', ['--version'], { stdio: 'pipe', encoding: 'utf-8', - timeout: 5000, + timeout: 2000, }); - if (result.status !== 0 || !result.stdout || !result.stdout.trim()) { + let versionStr = null; + + if (!versionResult.error && versionResult.status === 0) { + const raw = (versionResult.stdout || '').trim(); + // graphify --version may emit "graphify 0.4.23" or just "0.4.23" + const match = raw.match(/(\d+\.\d+(?:\.\d+)*)/); + if (match) { + versionStr = match[1]; + } + } + + // Strategy 2: fall back to python3 importlib.metadata + if (!versionStr) { + const pyResult = childProcess.spawnSync('python3', [ + '-c', + 'from importlib.metadata import version; print(version("graphifyy"))', + ], { + stdio: 'pipe', + encoding: 'utf-8', + timeout: 5000, + }); + + if (!pyResult.error && pyResult.status === 0 && pyResult.stdout && pyResult.stdout.trim()) { + versionStr = pyResult.stdout.trim(); + } + } + + if (!versionStr) { return { version: null, compatible: null, warning: 'Could not determine graphify version' }; } - const versionStr = result.stdout.trim(); const parts = versionStr.split('.').map(Number); if (parts.length < 2 || parts.some(isNaN)) { diff --git a/tests/graphify.test.cjs b/tests/graphify.test.cjs index 988c7c0c8..afef9edb0 100644 --- a/tests/graphify.test.cjs +++ b/tests/graphify.test.cjs @@ -384,18 +384,36 @@ describe('checkGraphifyVersion', () => { assert.ok(result.warning.includes('Could not parse')); }); - test('calls python3 with importlib.metadata', () => { - let capturedCmd; - let capturedArgs; + test('tries graphify --version first before python3', () => { + const calls = []; mock.method(childProcess, 'spawnSync', (cmd, args) => { - capturedCmd = cmd; - capturedArgs = args; + calls.push({ cmd, args }); return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null }; }); checkGraphifyVersion(); - assert.strictEqual(capturedCmd, 'python3'); - assert.ok(capturedArgs.some(arg => arg.includes('importlib.metadata'))); + assert.ok(calls.length >= 1, 'at least one spawnSync call'); + assert.strictEqual(calls[0].cmd, 'graphify'); + assert.deepStrictEqual(calls[0].args, ['--version']); + }); + + test('falls back to python3 importlib.metadata when graphify --version fails', () => { + const calls = []; + mock.method(childProcess, 'spawnSync', (cmd, args) => { + calls.push({ cmd, args }); + if (cmd === 'graphify') { + return { status: 1, stdout: '', stderr: 'unknown option', error: undefined, signal: null }; + } + // python3 fallback + return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null }; + }); + + const result = checkGraphifyVersion(); + assert.strictEqual(result.version, '0.4.3'); + assert.strictEqual(result.compatible, true); + const pythonCall = calls.find(c => c.cmd === 'python3'); + assert.ok(pythonCall, 'python3 should be called as fallback'); + assert.ok(pythonCall.args.some(arg => arg.includes('importlib.metadata'))); }); }); From 7c08a155eaebc2d89520d9c4035c7d86a1c85508 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 26 Apr 2026 11:21:54 -0400 Subject: [PATCH 2/3] test(graphify): tighten call-sequence assertions per coderabbit review Adds explicit call-count and ordering assertions to version-check tests: - Success path: exactly 1 spawnSync (graphify --version only, no python fallback) - Failure path: graphify --version attempted first, python3 fallback second Addresses coderabbit nitpick on PR #2739. --- tests/graphify.test.cjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/graphify.test.cjs b/tests/graphify.test.cjs index afef9edb0..2cf697400 100644 --- a/tests/graphify.test.cjs +++ b/tests/graphify.test.cjs @@ -392,9 +392,9 @@ describe('checkGraphifyVersion', () => { }); checkGraphifyVersion(); - assert.ok(calls.length >= 1, 'at least one spawnSync call'); + assert.strictEqual(calls.length, 1, 'exactly one spawnSync call — no python3 fallback'); assert.strictEqual(calls[0].cmd, 'graphify'); - assert.deepStrictEqual(calls[0].args, ['--version']); + assert.ok(calls[0].args.includes('--version'), 'graphify called with --version'); }); test('falls back to python3 importlib.metadata when graphify --version fails', () => { @@ -411,9 +411,11 @@ describe('checkGraphifyVersion', () => { const result = checkGraphifyVersion(); assert.strictEqual(result.version, '0.4.3'); assert.strictEqual(result.compatible, true); - const pythonCall = calls.find(c => c.cmd === 'python3'); - assert.ok(pythonCall, 'python3 should be called as fallback'); - assert.ok(pythonCall.args.some(arg => arg.includes('importlib.metadata'))); + assert.ok(calls.length >= 2, 'at least two spawnSync calls (graphify attempt + python3 fallback)'); + assert.ok(calls[0].args.includes('--version'), 'graphify --version attempted first'); + const lastCall = calls[calls.length - 1]; + assert.strictEqual(lastCall.cmd, 'python3', 'python3 fallback fires last'); + assert.ok(lastCall.args.some(arg => arg.includes('importlib.metadata'))); }); }); From 022b577922487c513d88d42d92552e011ad74305 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 26 Apr 2026 11:36:14 -0400 Subject: [PATCH 3/3] test(graphify): tighten version-check assertions per CodeRabbit nitpick - Success path: add explicit python3Calls.length === 0 assertion so "no fallback" is stated directly rather than implied by calls.length === 1 - Fallback path: add explicit calls[0].cmd === 'graphify' assertion so "graphify precedes python3" is verified by name, not just argument --- tests/graphify.test.cjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/graphify.test.cjs b/tests/graphify.test.cjs index 2cf697400..e1ec51d3d 100644 --- a/tests/graphify.test.cjs +++ b/tests/graphify.test.cjs @@ -395,6 +395,8 @@ describe('checkGraphifyVersion', () => { assert.strictEqual(calls.length, 1, 'exactly one spawnSync call — no python3 fallback'); assert.strictEqual(calls[0].cmd, 'graphify'); assert.ok(calls[0].args.includes('--version'), 'graphify called with --version'); + const python3Calls = calls.filter(c => c.cmd === 'python3'); + assert.strictEqual(python3Calls.length, 0, 'no python3 fallback when graphify --version succeeds'); }); test('falls back to python3 importlib.metadata when graphify --version fails', () => { @@ -412,6 +414,7 @@ describe('checkGraphifyVersion', () => { assert.strictEqual(result.version, '0.4.3'); assert.strictEqual(result.compatible, true); assert.ok(calls.length >= 2, 'at least two spawnSync calls (graphify attempt + python3 fallback)'); + assert.strictEqual(calls[0].cmd, 'graphify', 'graphify call precedes python3 fallback'); assert.ok(calls[0].args.includes('--version'), 'graphify --version attempted first'); const lastCall = calls[calls.length - 1]; assert.strictEqual(lastCall.cmd, 'python3', 'python3 fallback fires last');