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..6c2409f11 100755 --- a/bin/install.js +++ b/bin/install.js @@ -77,6 +77,13 @@ 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'); + +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 = []; @@ -6626,6 +6633,95 @@ function promptLocation(runtimes) { }); } +/** + * 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. + */ +function installSdkIfNeeded() { + if (hasNoSdk) { + console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`); + return; + } + + 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' }); + if (probe.status === 0) { + console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH)`); + return; + } + } + + // 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(` 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 (!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; + } + + // 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} Built and installed GSD SDK from source (gsd-sdk resolved at ${verify.stdout.trim().split('\n')[0]})`); + } else { + 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()}`); + } +} + /** * Install GSD for all selected runtimes */ @@ -6641,7 +6737,15 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime)); const finalize = (shouldInstallStatusline) => { - // Handle SDK installation before printing final summaries + // 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(); + const printSummaries = () => { for (const result of results) { const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; 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 7fd2306c7..f51527ad5 100644 --- a/tests/bugs-1656-1657.test.cjs +++ b/tests/bugs-1656-1657.test.cjs @@ -48,22 +48,67 @@ 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 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("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' ); }); });