fix(installer): warn on stale gsd-sdk path
This commit is contained in:
5
.changeset/fix-3359-stale-sdk-path-version.md
Normal file
5
.changeset/fix-3359-stale-sdk-path-version.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3363
|
||||
---
|
||||
**Installer SDK readiness now detects stale `gsd-sdk` executables earlier on PATH** — when the resolved `gsd-sdk --version` differs from the package/runtime version being installed, the installer withholds the ready message and prints the resolved path, detected version, expected version, and global update remediation. (#3359)
|
||||
@@ -9933,7 +9933,8 @@ function installSdkIfNeeded(opts) {
|
||||
// shell. A gsd-sdk found there must NOT count as "on PATH".
|
||||
const shimSrc = path.resolve(__dirname, 'gsd-sdk.js');
|
||||
const persistentPath = filterNpxFromPath(process.env.PATH || '');
|
||||
let onPath = isGsdSdkOnPath(persistentPath);
|
||||
let resolvedSdkPath = findGsdSdkOnPath(persistentPath);
|
||||
let onPath = !!resolvedSdkPath;
|
||||
|
||||
// Track WHERE we wrote the shim so the diagnostic can be specific even
|
||||
// when isGsdSdkOnPath() returns false because the write target isn't on
|
||||
@@ -9950,7 +9951,8 @@ function installSdkIfNeeded(opts) {
|
||||
const linked = trySelfLinkGsdSdk(shimSrc);
|
||||
if (linked) {
|
||||
shimDir = path.dirname(linked);
|
||||
onPath = isGsdSdkOnPath(persistentPath);
|
||||
resolvedSdkPath = findGsdSdkOnPath(persistentPath);
|
||||
onPath = !!resolvedSdkPath;
|
||||
if (onPath) {
|
||||
console.log(` ${dim}↪ linked gsd-sdk → ${linked}${reset}`);
|
||||
}
|
||||
@@ -9983,16 +9985,24 @@ function installSdkIfNeeded(opts) {
|
||||
const persistentUserShellPath = process.platform === 'win32'
|
||||
? userShellPath // already filtered by getUserShellWindowsPersistentPath
|
||||
: filterNpxFromPath(userShellPath);
|
||||
const userSees = isGsdSdkOnPath(persistentUserShellPath);
|
||||
if (!userSees) {
|
||||
const userSdkPath = findGsdSdkOnPath(persistentUserShellPath);
|
||||
if (!userSdkPath) {
|
||||
onPath = false;
|
||||
resolvedSdkPath = null;
|
||||
} else {
|
||||
resolvedSdkPath = userSdkPath;
|
||||
}
|
||||
}
|
||||
// If userShellPath is null (probe failed or unavailable), onPath reflects
|
||||
// the persistent-PATH check — that is the best available invariant.
|
||||
|
||||
if (onPath) {
|
||||
console.log(` ${green}✓${reset} GSD SDK ready (sdk/dist/cli.js)`);
|
||||
const versionReport = buildGsdSdkVersionMismatchReport(resolvedSdkPath, pkg.version);
|
||||
if (versionReport) {
|
||||
renderGsdSdkVersionMismatchReport(versionReport);
|
||||
} else {
|
||||
console.log(` ${green}✓${reset} GSD SDK ready (sdk/dist/cli.js)`);
|
||||
}
|
||||
} else {
|
||||
// #3011: actionable diagnostic. The previous shape printed a generic
|
||||
// "not on your PATH" message that didn't tell the user where to look.
|
||||
@@ -10099,7 +10109,7 @@ function filterNpxFromPath(pathString) {
|
||||
}
|
||||
|
||||
/**
|
||||
* #2775 helper: check whether a callable `gsd-sdk` exists on a PATH.
|
||||
* #2775 helper: find a callable `gsd-sdk` on a PATH.
|
||||
*
|
||||
* Pure PATH walk (no spawn) — we look for a regular file or symlink named
|
||||
* `gsd-sdk` (or `gsd-sdk.cmd`/`.exe` on Windows) in any directory on PATH and
|
||||
@@ -10117,7 +10127,7 @@ function filterNpxFromPath(pathString) {
|
||||
* isLegacyGsdSdkShim — a symlink pointing at the deprecated gsd-tools.cjs
|
||||
* binary must NOT be treated as "on PATH" even if it is executable.
|
||||
*/
|
||||
function isGsdSdkOnPath(pathString) {
|
||||
function findGsdSdkOnPath(pathString) {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
// Type-guard the explicit input (#3028 CR): callers may pass null
|
||||
@@ -10133,13 +10143,13 @@ function isGsdSdkOnPath(pathString) {
|
||||
const st = fs.statSync(candidate);
|
||||
if (st.isFile()) {
|
||||
if (process.platform === 'win32') {
|
||||
if (!isLegacyGsdSdkShim(candidate)) return true;
|
||||
if (!isLegacyGsdSdkShim(candidate)) return candidate;
|
||||
} else if ((st.mode & 0o111) !== 0) {
|
||||
// #3231: resolve symlink before sniffing, so we detect legacy
|
||||
// through any level of indirection.
|
||||
let target = candidate;
|
||||
try { target = fs.realpathSync(candidate); } catch {}
|
||||
if (!isLegacyGsdSdkShim(target)) return true;
|
||||
if (!isLegacyGsdSdkShim(target)) return candidate;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -10147,7 +10157,60 @@ function isGsdSdkOnPath(pathString) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function isGsdSdkOnPath(pathString) {
|
||||
return !!findGsdSdkOnPath(pathString);
|
||||
}
|
||||
|
||||
function parseGsdSdkVersion(text) {
|
||||
const match = String(text || '').match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function readGsdSdkVersion(sdkPath) {
|
||||
if (!sdkPath) return null;
|
||||
const cp = require('child_process');
|
||||
try {
|
||||
const isWindowsCommandShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(sdkPath));
|
||||
const result = cp.spawnSync(isWindowsCommandShim ? 'cmd.exe' : sdkPath, isWindowsCommandShim ? ['/c', sdkPath, '--version'] : ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 2000,
|
||||
env: process.env,
|
||||
});
|
||||
if (result.error || result.status !== 0) return null;
|
||||
return parseGsdSdkVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildGsdSdkVersionMismatchReport(sdkPath, expectedVersion) {
|
||||
const actualVersion = readGsdSdkVersion(sdkPath);
|
||||
if (!actualVersion || !expectedVersion) return null;
|
||||
if (actualVersion === expectedVersion) return null;
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'gsd_sdk_version_mismatch',
|
||||
sdk_path: sdkPath,
|
||||
actual_version: actualVersion,
|
||||
expected_version: expectedVersion,
|
||||
fix_command: 'npm install -g get-shit-done-cc@latest',
|
||||
};
|
||||
}
|
||||
|
||||
function renderGsdSdkVersionMismatchReport(ir) {
|
||||
console.log('');
|
||||
console.log(` ${yellow}⚠${reset} ${bold}gsd-sdk version mismatch${reset} — PATH resolves a stale SDK.`);
|
||||
console.log(` Resolved gsd-sdk: ${ir.sdk_path}`);
|
||||
console.log(` Resolved version: ${ir.actual_version}`);
|
||||
console.log(` Installer version: ${ir.expected_version}`);
|
||||
console.log(` Workflows that call ${cyan}gsd-sdk query …${reset} will use the stale executable first.`);
|
||||
console.log(` Fix: ${cyan}${ir.fix_command}${reset}`);
|
||||
console.log(` Or remove the stale global install / adjust PATH so the current shim is first.`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10637,6 +10700,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
buildSdkFailFastReport,
|
||||
renderSdkFailFastReport,
|
||||
classifySdkInstall,
|
||||
readGsdSdkVersion,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
|
||||
169
tests/bug-3359-stale-gsd-sdk-path-version.test.cjs
Normal file
169
tests/bug-3359-stale-gsd-sdk-path-version.test.cjs
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Regression test for bug #3359.
|
||||
*
|
||||
* `npx get-shit-done-cc@latest` can refresh runtime files while an older
|
||||
* global `gsd-sdk` earlier on PATH remains the executable workflows call.
|
||||
* The installer must not report SDK readiness when the resolved `gsd-sdk`
|
||||
* version differs from the package/runtime version being installed.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { installSdkIfNeeded, readGsdSdkVersion } = require('../bin/install.js');
|
||||
const cp = require('node:child_process');
|
||||
const pkg = require('../package.json');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function captureConsole(fn) {
|
||||
const stdout = [];
|
||||
const stderr = [];
|
||||
const origLog = console.log;
|
||||
const origWarn = console.warn;
|
||||
const origError = console.error;
|
||||
console.log = (...a) => stdout.push(a.join(' '));
|
||||
console.warn = (...a) => stderr.push(a.join(' '));
|
||||
console.error = (...a) => stderr.push(a.join(' '));
|
||||
let threw = null;
|
||||
try {
|
||||
fn();
|
||||
} catch (e) {
|
||||
threw = e;
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
console.warn = origWarn;
|
||||
console.error = origError;
|
||||
}
|
||||
if (threw) throw threw;
|
||||
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
return {
|
||||
stdout: stdout.map(strip).join('\n'),
|
||||
stderr: stderr.map(strip).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('bug #3359: installer detects stale gsd-sdk earlier on PATH', () => {
|
||||
let tmpRoot;
|
||||
let sdkDir;
|
||||
let pathDir;
|
||||
let homeDir;
|
||||
let savedEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpRoot = createTempDir('gsd-3359-');
|
||||
sdkDir = path.join(tmpRoot, 'sdk');
|
||||
pathDir = path.join(tmpRoot, 'global-bin');
|
||||
homeDir = path.join(tmpRoot, 'home');
|
||||
fs.mkdirSync(path.join(sdkDir, 'dist'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(sdkDir, 'dist', 'cli.js'),
|
||||
'#!/usr/bin/env node\nconsole.log("sdk cli");\n',
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
fs.mkdirSync(pathDir, { recursive: true });
|
||||
fs.mkdirSync(homeDir, { recursive: true });
|
||||
savedEnv = {
|
||||
PATH: process.env.PATH,
|
||||
HOME: process.env.HOME,
|
||||
SHELL: process.env.SHELL,
|
||||
};
|
||||
process.env.PATH = pathDir;
|
||||
process.env.HOME = homeDir;
|
||||
delete process.env.SHELL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv.PATH == null) delete process.env.PATH;
|
||||
else process.env.PATH = savedEnv.PATH;
|
||||
if (savedEnv.HOME == null) delete process.env.HOME;
|
||||
else process.env.HOME = savedEnv.HOME;
|
||||
if (savedEnv.SHELL == null) delete process.env.SHELL;
|
||||
else process.env.SHELL = savedEnv.SHELL;
|
||||
cleanup(tmpRoot);
|
||||
});
|
||||
|
||||
test('does not print ready when resolved gsd-sdk version differs from installer package version', () => {
|
||||
const staleSdk = path.join(pathDir, 'gsd-sdk');
|
||||
fs.writeFileSync(
|
||||
staleSdk,
|
||||
'#!/bin/sh\nprintf "%s\\n" "gsd-sdk v0.0.1"\n',
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
const { stdout, stderr } = captureConsole(() => {
|
||||
installSdkIfNeeded({ sdkDir });
|
||||
});
|
||||
const combined = `${stdout}\n${stderr}`;
|
||||
|
||||
assert.ok(
|
||||
/version mismatch|different version|stale/i.test(combined),
|
||||
`installer must warn that resolved gsd-sdk is stale. Output:\n${combined}`,
|
||||
);
|
||||
assert.ok(
|
||||
combined.includes(staleSdk),
|
||||
`warning must include resolved gsd-sdk path. Output:\n${combined}`,
|
||||
);
|
||||
assert.ok(
|
||||
combined.includes('0.0.1') && combined.includes(pkg.version),
|
||||
`warning must include detected and installer versions. Output:\n${combined}`,
|
||||
);
|
||||
assert.ok(
|
||||
/npm install -g get-shit-done-cc@latest/.test(combined),
|
||||
`warning must include global update remediation. Output:\n${combined}`,
|
||||
);
|
||||
assert.ok(
|
||||
!/GSD SDK ready/.test(combined),
|
||||
`installer must not report ready while PATH resolves a stale gsd-sdk. Output:\n${combined}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('prints ready when no stale gsd-sdk is on PATH', () => {
|
||||
const currentSdk = path.join(pathDir, 'gsd-sdk');
|
||||
fs.writeFileSync(
|
||||
currentSdk,
|
||||
`#!/bin/sh\nprintf "%s\\n" "gsd-sdk v${pkg.version}"\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
const { stdout, stderr } = captureConsole(() => {
|
||||
installSdkIfNeeded({ sdkDir });
|
||||
});
|
||||
const combined = `${stdout}\n${stderr}`;
|
||||
|
||||
assert.ok(
|
||||
/GSD SDK ready/.test(combined),
|
||||
`installer must report ready when no stale gsd-sdk exists. Output:\n${combined}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('reads Windows cmd shim versions through cmd.exe', () => {
|
||||
const originalSpawnSync = cp.spawnSync;
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
const calls = [];
|
||||
cp.spawnSync = (command, args, options) => {
|
||||
calls.push({ command, args, options });
|
||||
return { status: 0, stdout: `gsd-sdk v${pkg.version}\n`, stderr: '' };
|
||||
};
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
try {
|
||||
assert.equal(readGsdSdkVersion('C:\\tools\\gsd-sdk.cmd'), pkg.version);
|
||||
} finally {
|
||||
cp.spawnSync = originalSpawnSync;
|
||||
Object.defineProperty(process, 'platform', platformDescriptor);
|
||||
}
|
||||
|
||||
assert.deepEqual(calls.map(({ command, args }) => ({ command, args })), [{
|
||||
command: 'cmd.exe',
|
||||
args: ['/c', 'C:\\tools\\gsd-sdk.cmd', '--version'],
|
||||
}]);
|
||||
assert.equal(calls[0].options.encoding, 'utf8');
|
||||
assert.equal(calls[0].options.timeout, 2000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user