From b2fcacda1b19a987cd27ff8f0e1153239fa82dd8 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 17 Apr 2026 15:56:35 -0500 Subject: [PATCH 1/5] fix(install): auto-install @gsd-build/sdk so gsd-sdk is on PATH (#2385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every /gsd-* command shells out to `gsd-sdk query …`, but the SDK was never installed by bin/install.js — the `--sdk` flag documented in README was never implemented. Users upgrading to 1.36+ hit "command not found: gsd-sdk" on every command. - Implement SDK install in finishInstall's finalize path - Default on; --no-sdk to skip; --sdk to force when already present - Idempotent probe via `which gsd-sdk` before reinstalling - Failures are warnings, not fatal — install hint printed Closes #2385 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 +++ README.md | 2 +- bin/install.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60db661ea..63f7ff7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- **Installer now installs `@gsd-build/sdk` automatically** so `gsd-sdk` lands on PATH. Resolves `command not found: gsd-sdk` errors that affected every `/gsd-*` command after a fresh install or `/gsd-update` to 1.36+. Adds `--no-sdk` to opt out and `--sdk` to force reinstall. Implements the `--sdk` flag that was previously documented in README but never wired up (#2385) + ## [1.37.1] - 2026-04-17 ### Fixed diff --git a/README.md b/README.md index 56e2dccc6..4d17aa282 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ npx get-shit-done-cc --all --global # Install to all directories Use `--global` (`-g`) or `--local` (`-l`) to skip the location prompt. Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, `--augment`, `--trae`, `--qwen`, `--codebuddy`, `--cline`, or `--all` to skip the runtime prompt. -Use `--sdk` to also install the GSD SDK CLI (`gsd-sdk`) for headless autonomous execution. +The GSD SDK CLI (`gsd-sdk`) is installed automatically (required by `/gsd-*` commands). Pass `--no-sdk` to skip the SDK install, or `--sdk` to force a reinstall. diff --git a/bin/install.js b/bin/install.js index 37d79e1dc..b36891223 100755 --- a/bin/install.js +++ b/bin/install.js @@ -77,6 +77,8 @@ const hasBoth = args.includes('--both'); // Legacy flag, keeps working const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1'; +const hasSdk = args.includes('--sdk'); +const hasNoSdk = args.includes('--no-sdk'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; @@ -6626,6 +6628,41 @@ function promptLocation(runtimes) { }); } +/** + * Ensure `@gsd-build/sdk` (the `gsd-sdk` binary) is installed globally so + * workflow commands that shell out to `gsd-sdk query …` succeed. + * + * Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit). + * Failures are warnings, not fatal. + */ +function installSdkIfNeeded() { + if (hasNoSdk) { + console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`); + return; + } + + const { spawnSync } = require('child_process'); + + if (!hasSdk) { + const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['gsd-sdk'], { stdio: 'ignore' }); + if (probe.status === 0) { + console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH)`); + return; + } + } + + console.log(`\n ${cyan}Installing GSD SDK (@gsd-build/sdk)…${reset}`); + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const result = spawnSync(npmCmd, ['install', '-g', '@gsd-build/sdk'], { stdio: 'inherit' }); + if (result.status === 0) { + console.log(` ${green}✓${reset} Installed @gsd-build/sdk (gsd-sdk now on PATH)`); + } else { + console.warn(` ${yellow}⚠${reset} Failed to install @gsd-build/sdk automatically.`); + console.warn(` Run manually: ${cyan}npm install -g @gsd-build/sdk${reset}`); + console.warn(` Without it, /gsd-* commands will fail with "command not found: gsd-sdk".`); + } +} + /** * Install GSD for all selected runtimes */ @@ -6641,7 +6678,12 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime)); const finalize = (shouldInstallStatusline) => { - // Handle SDK installation before printing final summaries + // Install @gsd-build/sdk so `gsd-sdk` lands on PATH. + // Every /gsd-* command shells out to `gsd-sdk query …`; without this, + // commands fail with "command not found: gsd-sdk". + // Runs by default; skip with --no-sdk. Idempotent when already present. + installSdkIfNeeded(); + const printSummaries = () => { for (const result of results) { const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; From 53b49dfe20cc247617734ef383ae47f5c883e911 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 17 Apr 2026 15:59:05 -0500 Subject: [PATCH 2/5] test: update #1657 regression guard for #2385 SDK install restoration The guard was added when @gsd-build/sdk did not yet exist on npm. The package is now published at v0.1.0 and every /gsd-* command depends on the `gsd-sdk` binary. Invert the assertions: --sdk/--no-sdk must be wired up and the installer must reference @gsd-build/sdk. Keep the promptSdk() ban to prevent reintroducing the old broken prompt. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/bugs-1656-1657.test.cjs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/bugs-1656-1657.test.cjs b/tests/bugs-1656-1657.test.cjs index 7fd2306c7..c940f8ac0 100644 --- a/tests/bugs-1656-1657.test.cjs +++ b/tests/bugs-1656-1657.test.cjs @@ -48,22 +48,43 @@ describe('#1656: community .sh hooks must be present in hooks/dist', () => { }); // ─── #1657 ─────────────────────────────────────────────────────────────────── +// +// Historical context: #1657 originally guarded against a broken `promptSdk()` +// flow that shipped when `@gsd-build/sdk` did not yet exist on npm. The +// package was published at v0.1.0 and is now a hard runtime requirement for +// every /gsd-* command (they all shell out to `gsd-sdk query …`). +// +// #2385 restored the `--sdk` flag and made SDK install the default path in +// bin/install.js. These guards are inverted: we now assert that SDK install +// IS wired up, and that the old broken `promptSdk()` prompt is still gone. -describe('#1657: SDK prompt must not appear in installer source', () => { +describe('#1657 / #2385: SDK install must be wired into installer source', () => { let src; - test('install.js does not contain promptSdk call', () => { + test('install.js does not contain the legacy promptSdk() prompt (#1657)', () => { src = fs.readFileSync(INSTALL_SRC, 'utf-8'); assert.ok( !src.includes('promptSdk('), - 'promptSdk() must not be called — SDK prompt causes install failures when package does not exist on npm' + 'promptSdk() must not be reintroduced — the old interactive prompt flow was broken' ); }); - test('install.js does not contain --sdk flag handling', () => { + test('install.js wires up --sdk / --no-sdk flag handling (#2385)', () => { src = src || fs.readFileSync(INSTALL_SRC, 'utf-8'); assert.ok( - !src.includes("args.includes('--sdk')"), - '--sdk flag must be removed to prevent users triggering a broken SDK install' + src.includes("args.includes('--sdk')"), + '--sdk flag must be parsed so users can force SDK (re)install' + ); + assert.ok( + src.includes("args.includes('--no-sdk')"), + '--no-sdk flag must be parsed so users can opt out of SDK install' + ); + }); + + test('install.js installs @gsd-build/sdk by default (#2385)', () => { + src = src || fs.readFileSync(INSTALL_SRC, 'utf-8'); + assert.ok( + src.includes('@gsd-build/sdk'), + 'installer must reference @gsd-build/sdk so gsd-sdk lands on PATH' ); }); }); From 08b1d8377d29eb1bbc5198f7b0e3492897b28667 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 17 Apr 2026 16:03:25 -0500 Subject: [PATCH 3/5] fix(install): error on mutually exclusive --sdk and --no-sdk flags Previously passing both silently had --no-sdk win. Exit non-zero with a clear error to match how other exclusive flag pairs (--global/--local, --config-dir/--local) are handled. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/install.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/install.js b/bin/install.js index b36891223..3e953904e 100755 --- a/bin/install.js +++ b/bin/install.js @@ -80,6 +80,11 @@ const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PO const hasSdk = args.includes('--sdk'); const hasNoSdk = args.includes('--no-sdk'); +if (hasSdk && hasNoSdk) { + console.error(` ${yellow}Cannot specify both --sdk and --no-sdk${reset}`); + process.exit(1); +} + // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { From 819af761a040c38edc6860e19a422c13eaa560f7 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 17 Apr 2026 16:09:19 -0500 Subject: [PATCH 4/5] fix(install): verify gsd-sdk resolves on PATH after npm install `npm install -g` can succeed while the binary lands in a prefix that isn't on the current shell's PATH (common with Homebrew, nvm, or an unconfigured npm prefix). Re-probe via `which gsd-sdk` (or `where` on Windows) after install; if it doesn't resolve, downgrade the success message to a warning with a shell-restart hint. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/install.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/bin/install.js b/bin/install.js index 3e953904e..bd901cccf 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6659,12 +6659,30 @@ function installSdkIfNeeded() { console.log(`\n ${cyan}Installing GSD SDK (@gsd-build/sdk)…${reset}`); const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const result = spawnSync(npmCmd, ['install', '-g', '@gsd-build/sdk'], { stdio: 'inherit' }); - if (result.status === 0) { - console.log(` ${green}✓${reset} Installed @gsd-build/sdk (gsd-sdk now on PATH)`); - } else { - console.warn(` ${yellow}⚠${reset} Failed to install @gsd-build/sdk automatically.`); + + const warnManual = (reason) => { + console.warn(` ${yellow}⚠${reset} ${reason}`); console.warn(` Run manually: ${cyan}npm install -g @gsd-build/sdk${reset}`); + console.warn(` Then restart your shell so the updated PATH is picked up.`); console.warn(` Without it, /gsd-* commands will fail with "command not found: gsd-sdk".`); + }; + + if (result.status !== 0) { + warnManual('Failed to install @gsd-build/sdk automatically.'); + return; + } + + // Verify gsd-sdk is actually resolvable on PATH. npm's global bin dir is + // not always on the current shell's PATH (Homebrew prefixes, nvm setups, + // unconfigured npm prefix), so a zero exit status from `npm install -g` + // alone is not proof of a working binary. + const resolverCmd = process.platform === 'win32' ? 'where' : 'which'; + const verify = spawnSync(resolverCmd, ['gsd-sdk'], { encoding: 'utf-8' }); + if (verify.status === 0 && verify.stdout && verify.stdout.trim()) { + console.log(` ${green}✓${reset} Installed @gsd-build/sdk (gsd-sdk resolved at ${verify.stdout.trim().split('\n')[0]})`); + } else { + warnManual('Installed @gsd-build/sdk but gsd-sdk is not on PATH — npm global bin may not be in your PATH.'); + if (verify.stderr) console.warn(` resolver stderr: ${verify.stderr.trim()}`); } } From fc1fa9172bae51b25b0ba327c68dd2f7e18435af Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 17 Apr 2026 19:53:16 -0500 Subject: [PATCH 5/5] fix(install): build gsd-sdk from in-repo sdk/ source, not stale npm package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2386 v1 installed the published @gsd-build/sdk from npm, which ships an older version that lacks query handlers needed by current workflows. Every GSD release would drift further from what the installer put on PATH. This commit rewires installSdkIfNeeded() to build from the in-repo sdk/ source tree instead: 1. cd sdk && npm install (build-time deps incl. tsc) 2. npm run build (tsc → sdk/dist/) 3. npm install -g . (global install; gsd-sdk on PATH) Each step is a hard gate — failures warn loudly and point users at the manual equivalent command. No more silent drift between installed SDK and the rest of the GSD system. Root package.json `files` now ships sdk/src, sdk/prompts, sdk/package.json, sdk/package-lock.json, and sdk/tsconfig.json so npm-registry installs also carry the source tree needed to build gsd-sdk locally. Also fixes a blocking tsc error in sdk/src/event-stream.ts:313 — the cast to `Array<{ type: string; [key: string]: unknown }>` needed a double-cast via `unknown` because BetaContentBlock's variants don't carry an index signature. Runtime-neutral type-widening; sdk vitest suite unchanged (1256 passing; the lone failure is a pre-existing integration test that requires external API access). Updates the #1657/#2385 regression test to assert the new build-from-source path (path.resolve(__dirname, '..', 'sdk') + `npm run build` + `npm install -g .`) plus a new assertion that root package.json files array ships sdk source. Refs #2385 Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/install.js | 65 ++++++++++++++++++++++++++++------- package.json | 7 +++- sdk/package-lock.json | 1 + sdk/src/event-stream.ts | 6 ++-- tests/bugs-1656-1657.test.cjs | 30 ++++++++++++++-- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/bin/install.js b/bin/install.js index bd901cccf..6c2409f11 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6634,8 +6634,13 @@ function promptLocation(runtimes) { } /** - * Ensure `@gsd-build/sdk` (the `gsd-sdk` binary) is installed globally so - * workflow commands that shell out to `gsd-sdk query …` succeed. + * Build `@gsd-build/sdk` from the in-repo `sdk/` source tree and install the + * resulting `gsd-sdk` binary globally so workflow commands that shell out to + * `gsd-sdk query …` succeed. + * + * We build from source rather than `npm install -g @gsd-build/sdk` because the + * npm-published package lags the source tree and shipping a stale SDK breaks + * every /gsd-* command that depends on newer query handlers. * * Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit). * Failures are warnings, not fatal. @@ -6647,6 +6652,8 @@ function installSdkIfNeeded() { } const { spawnSync } = require('child_process'); + const path = require('path'); + const fs = require('fs'); if (!hasSdk) { const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['gsd-sdk'], { stdio: 'ignore' }); @@ -6656,19 +6663,48 @@ function installSdkIfNeeded() { } } - console.log(`\n ${cyan}Installing GSD SDK (@gsd-build/sdk)…${reset}`); - const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - const result = spawnSync(npmCmd, ['install', '-g', '@gsd-build/sdk'], { stdio: 'inherit' }); + // Locate the in-repo sdk/ directory relative to this installer file. + // For global npm installs this resolves inside the published package dir; + // for git-based installs (npx github:..., local clone) it resolves to the + // repo's sdk/ tree. Both contain the source tree because root package.json + // includes "sdk" in its `files` array. + const sdkDir = path.resolve(__dirname, '..', 'sdk'); + const sdkPackageJson = path.join(sdkDir, 'package.json'); const warnManual = (reason) => { console.warn(` ${yellow}⚠${reset} ${reason}`); - console.warn(` Run manually: ${cyan}npm install -g @gsd-build/sdk${reset}`); + console.warn(` Build manually from the repo sdk/ directory:`); + console.warn(` ${cyan}cd ${sdkDir} && npm install && npm run build && npm install -g .${reset}`); console.warn(` Then restart your shell so the updated PATH is picked up.`); console.warn(` Without it, /gsd-* commands will fail with "command not found: gsd-sdk".`); }; - if (result.status !== 0) { - warnManual('Failed to install @gsd-build/sdk automatically.'); + if (!fs.existsSync(sdkPackageJson)) { + warnManual(`SDK source tree not found at ${sdkDir}.`); + return; + } + + console.log(`\n ${cyan}Building GSD SDK from source (${sdkDir})…${reset}`); + const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + + // 1. Install sdk build-time dependencies (tsc, etc.) + const installResult = spawnSync(npmCmd, ['install'], { cwd: sdkDir, stdio: 'inherit' }); + if (installResult.status !== 0) { + warnManual('Failed to `npm install` in sdk/.'); + return; + } + + // 2. Compile TypeScript → sdk/dist/ + const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' }); + if (buildResult.status !== 0) { + warnManual('Failed to `npm run build` in sdk/.'); + return; + } + + // 3. Install the built package globally so `gsd-sdk` lands on PATH. + const globalResult = spawnSync(npmCmd, ['install', '-g', '.'], { cwd: sdkDir, stdio: 'inherit' }); + if (globalResult.status !== 0) { + warnManual('Failed to `npm install -g .` from sdk/.'); return; } @@ -6679,9 +6715,9 @@ function installSdkIfNeeded() { const resolverCmd = process.platform === 'win32' ? 'where' : 'which'; const verify = spawnSync(resolverCmd, ['gsd-sdk'], { encoding: 'utf-8' }); if (verify.status === 0 && verify.stdout && verify.stdout.trim()) { - console.log(` ${green}✓${reset} Installed @gsd-build/sdk (gsd-sdk resolved at ${verify.stdout.trim().split('\n')[0]})`); + console.log(` ${green}✓${reset} Built and installed GSD SDK from source (gsd-sdk resolved at ${verify.stdout.trim().split('\n')[0]})`); } else { - warnManual('Installed @gsd-build/sdk but gsd-sdk is not on PATH — npm global bin may not be in your PATH.'); + warnManual('Built and installed GSD SDK from source but gsd-sdk is not on PATH — npm global bin may not be in your PATH.'); if (verify.stderr) console.warn(` resolver stderr: ${verify.stderr.trim()}`); } } @@ -6701,9 +6737,12 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime)); const finalize = (shouldInstallStatusline) => { - // Install @gsd-build/sdk so `gsd-sdk` lands on PATH. - // Every /gsd-* command shells out to `gsd-sdk query …`; without this, - // commands fail with "command not found: gsd-sdk". + // Build @gsd-build/sdk from the in-repo sdk/ source and install it globally + // so `gsd-sdk` lands on PATH. Every /gsd-* command shells out to + // `gsd-sdk query …`; without this, commands fail with "command not found: + // gsd-sdk". The npm-published @gsd-build/sdk is kept intentionally frozen + // at an older version; we always build from source so users get the SDK + // that matches the installed GSD version. // Runs by default; skip with --no-sdk. Idempotent when already present. installSdkIfNeeded(); diff --git a/package.json b/package.json index 8087665b3..a106c9fba 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,12 @@ "get-shit-done", "agents", "hooks", - "scripts" + "scripts", + "sdk/src", + "sdk/prompts", + "sdk/package.json", + "sdk/package-lock.json", + "sdk/tsconfig.json" ], "keywords": [ "claude", diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 23acdadf4..81d09b38f 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@gsd-build/sdk", "version": "0.1.0", + "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.84", "ws": "^8.20.0" diff --git a/sdk/src/event-stream.ts b/sdk/src/event-stream.ts index d46fd6686..426701a19 100644 --- a/sdk/src/event-stream.ts +++ b/sdk/src/event-stream.ts @@ -309,8 +309,10 @@ export class GSDEventStream extends EventEmitter { ): GSDEvent | null { const events: GSDEvent[] = []; - // Extract text blocks — content blocks are a discriminated union with a 'type' field - const content = msg.message.content as Array<{ type: string; [key: string]: unknown }>; + // Extract text blocks — content blocks are a discriminated union with a 'type' field. + // Double-cast via unknown because BetaContentBlock's internal variants don't + // carry an index signature, so TS rejects the direct cast without a widening step. + const content = msg.message.content as unknown as Array<{ type: string; [key: string]: unknown }>; const textBlocks = content.filter( (b): b is { type: 'text'; text: string } => b.type === 'text', diff --git a/tests/bugs-1656-1657.test.cjs b/tests/bugs-1656-1657.test.cjs index c940f8ac0..f51527ad5 100644 --- a/tests/bugs-1656-1657.test.cjs +++ b/tests/bugs-1656-1657.test.cjs @@ -80,11 +80,35 @@ describe('#1657 / #2385: SDK install must be wired into installer source', () => ); }); - test('install.js installs @gsd-build/sdk by default (#2385)', () => { + test('install.js builds gsd-sdk from in-repo sdk/ source (#2385)', () => { src = src || fs.readFileSync(INSTALL_SRC, 'utf-8'); + // The installer must locate the in-repo sdk/ directory, run the build, + // and install it globally. We intentionally do NOT install + // @gsd-build/sdk from npm because that published version lags the source + // tree and shipping it breaks query handlers added since the last + // publish. assert.ok( - src.includes('@gsd-build/sdk'), - 'installer must reference @gsd-build/sdk so gsd-sdk lands on PATH' + src.includes("path.resolve(__dirname, '..', 'sdk')") || + src.includes('path.resolve(__dirname, "..", "sdk")'), + 'installer must locate the in-repo sdk/ directory' + ); + assert.ok( + src.includes("'npm install -g .'") || + src.includes("['install', '-g', '.']"), + 'installer must run `npm install -g .` from sdk/ to install the built package globally' + ); + assert.ok( + src.includes("['run', 'build']"), + 'installer must compile TypeScript via `npm run build` before installing globally' + ); + }); + + test('package.json ships sdk source in published tarball (#2385)', () => { + const rootPkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')); + const files = rootPkg.files || []; + assert.ok( + files.some((f) => f === 'sdk' || f.startsWith('sdk/')), + 'root package.json `files` must include sdk source so npm-registry installs can build gsd-sdk from source' ); }); });