fix(sdk): decouple SDK from build-from-source install path, close #2441 and #2453

Ship sdk/dist prebuilt in the tarball and replace the npm-install-g
sub-install with a parent-package bin shim (bin/gsd-sdk.js). npm chmods
bin entries from a packed tarball correctly, eliminating the mode-644
failure (#2453) and the full class of NPM_CONFIG_PREFIX/ignore-scripts/
corepack/air-gapped failure modes that caused #2439 and #2441.

Changes:
- sdk/package.json: prepublishOnly runs `rm -rf dist && tsc && chmod +x
  dist/cli.js` (stale-build guard + execute-bit fix at publish time)
- package.json: add "gsd-sdk": "bin/gsd-sdk.js" bin entry; add sdk/dist
  to files so the prebuilt CLI ships in the tarball
- bin/gsd-sdk.js: new back-compat shim — resolves sdk/dist/cli.js relative
  to the package root and delegates via `node`, so all existing PATH call
  sites (slash commands, agents, hooks) continue to work unchanged (S1 shim)
- bin/install.js: replace installSdkIfNeeded() build-from-source + global-
  install dance with a dist-verify + chmod-in-place guard; delete
  resolveGsdSdk(), detectShellRc(), emitSdkFatal() helpers now unused
- .github/workflows/install-smoke.yml: add smoke-unpacked job that strips
  execute bit from sdk/dist/cli.js before install to reproduce the exact
  #2453 failure mode
- tests/bug-2441-sdk-decouple.test.cjs: new regression tests asserting all
  invariants (no npm install -g from sdk/, shim exists, sdk/dist in files,
  prepublishOnly has rm -rf + chmod)
- tests/bugs-1656-1657.test.cjs: update stale assertions that required
  build-from-source behavior (now asserts new prebuilt-dist invariants)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeremy McSpadden
2026-04-19 20:49:38 -05:00
parent 278082a51d
commit 0f6903df0a
7 changed files with 344 additions and 194 deletions

View File

@@ -1,10 +1,13 @@
name: Install Smoke
# Exercises the real install path: `npm pack` → `npm install -g <tarball>`
# → run `bin/install.js` → assert `gsd-sdk` is on PATH.
# Exercises the real install paths:
# tarball: `npm pack` → `npm install -g <tarball>` → assert gsd-sdk on PATH
# unpacked: `npm install -g <dir>` (no pack) → assert gsd-sdk on PATH + executable
#
# Closes the CI gap that let #2439 ship: the rest of the suite only reads
# `bin/install.js` as a string and never executes it.
# The tarball path is the canonical ship path. The unpacked path reproduces the
# mode-644 failure class (issue #2453): npm does NOT chmod bin targets when
# installing from an unpacked local directory, so any stale tsc output lacking
# execute bits will be caught by the unpacked job before release.
#
# - PRs: path-filtered, minimal runner (ubuntu + Node LTS) for fast signal.
# - Push to release branches / main: full matrix.
@@ -16,6 +19,7 @@ on:
- main
paths:
- 'bin/install.js'
- 'bin/gsd-sdk.js'
- 'sdk/**'
- 'package.json'
- 'package-lock.json'
@@ -40,6 +44,9 @@ concurrency:
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------------------
# Job 1: tarball install (existing canonical path)
# ---------------------------------------------------------------------------
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 12
@@ -109,7 +116,7 @@ jobs:
echo "$NPM_BIN" >> "$GITHUB_PATH"
echo "npm global bin: $NPM_BIN"
- name: Install tarball globally (runs bin/install.js → installSdkIfNeeded)
- name: Install tarball globally
if: steps.skip.outputs.skip != 'true'
shell: bash
env:
@@ -121,12 +128,8 @@ jobs:
cd "$TMPDIR_ROOT"
npm install -g "$WORKSPACE/$TARBALL"
command -v get-shit-done-cc
# `--claude --local` is the non-interactive code path (see
# install.js main block: when both a runtime and location are set,
# installAllRuntimes runs with isInteractive=false, no prompts).
# We tolerate non-zero here because the authoritative assertion is
# the next step: gsd-sdk must land on PATH. Some runtime targets
# may exit before the SDK step for unrelated reasons on CI.
# `--claude --local` is the non-interactive code path.
# Tolerate non-zero: the authoritative assertion is the next step.
get-shit-done-cc --claude --local || true
- name: Assert gsd-sdk resolves on PATH
@@ -135,7 +138,7 @@ jobs:
run: |
set -euo pipefail
if ! command -v gsd-sdk >/dev/null 2>&1; then
echo "::error::gsd-sdk is not on PATH after install installSdkIfNeeded() regression"
echo "::error::gsd-sdk is not on PATH after tarball install — shim regression"
NPM_BIN="$(npm config get prefix)/bin"
echo "npm global bin: $NPM_BIN"
ls -la "$NPM_BIN" | grep -i gsd || true
@@ -150,3 +153,76 @@ jobs:
set -euo pipefail
gsd-sdk --version || gsd-sdk --help
echo "✓ gsd-sdk is executable"
# ---------------------------------------------------------------------------
# Job 2: unpacked-dir install — reproduces the mode-644 failure class (#2453)
#
# `npm install -g <directory>` does NOT chmod bin targets when the source
# file was produced by a build script (tsc emits 0o644). This job catches
# regressions where sdk/dist/cli.js loses its execute bit before publish.
# ---------------------------------------------------------------------------
smoke-unpacked:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Node.js 22
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: 'npm'
- name: Install root deps
run: npm ci
- name: Ensure npm global bin is on PATH
shell: bash
run: |
NPM_BIN="$(npm config get prefix)/bin"
echo "$NPM_BIN" >> "$GITHUB_PATH"
echo "npm global bin: $NPM_BIN"
- name: Strip execute bit from sdk/dist/cli.js to simulate tsc-fresh output
shell: bash
run: |
set -euo pipefail
# Simulate the exact state tsc produces: cli.js at mode 644.
chmod 644 sdk/dist/cli.js
echo "Stripped execute bit: $(stat -c '%a' sdk/dist/cli.js 2>/dev/null || stat -f '%p' sdk/dist/cli.js)"
- name: Install from unpacked directory (no npm pack)
shell: bash
run: |
set -euo pipefail
TMPDIR_ROOT=$(mktemp -d)
cd "$TMPDIR_ROOT"
npm install -g "$GITHUB_WORKSPACE"
command -v get-shit-done-cc
get-shit-done-cc --claude --local || true
- name: Assert gsd-sdk resolves on PATH after unpacked install
shell: bash
run: |
set -euo pipefail
if ! command -v gsd-sdk >/dev/null 2>&1; then
echo "::error::gsd-sdk is not on PATH after unpacked install — #2453 regression"
NPM_BIN="$(npm config get prefix)/bin"
ls -la "$NPM_BIN" | grep -i gsd || true
exit 1
fi
echo "✓ gsd-sdk resolves at: $(command -v gsd-sdk)"
- name: Assert gsd-sdk is executable after unpacked install (#2453)
shell: bash
run: |
set -euo pipefail
# This is the exact check that would have caught #2453 before release.
# The shim (bin/gsd-sdk.js) invokes sdk/dist/cli.js via `node`, so
# the execute bit on cli.js is not needed for the shim path. However
# installSdkIfNeeded() also chmods cli.js in-place as a safety net.
gsd-sdk --version || gsd-sdk --help
echo "✓ gsd-sdk is executable after unpacked install"

32
bin/gsd-sdk.js Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* bin/gsd-sdk.js — back-compat shim for external callers of `gsd-sdk`.
*
* When the parent package is installed globally (`npm install -g get-shit-done-cc`
* or `npx get-shit-done-cc`), npm creates a `gsd-sdk` symlink in the global bin
* directory pointing at this file. npm correctly chmods bin entries from a tarball,
* so the execute-bit problem that afflicted the sub-install approach (issue #2453)
* cannot occur here.
*
* This shim resolves sdk/dist/cli.js relative to its own location and delegates
* to it via `node`, so `gsd-sdk <args>` behaves identically to
* `node <packageDir>/sdk/dist/cli.js <args>`.
*
* Call sites (slash commands, agent prompts, hook scripts) continue to work without
* changes because `gsd-sdk` still resolves on PATH — it just comes from this shim
* in the parent package rather than from a separately installed @gsd-build/sdk.
*/
'use strict';
const path = require('path');
const { spawnSync } = require('child_process');
const cliPath = path.resolve(__dirname, '..', 'sdk', 'dist', 'cli.js');
const result = spawnSync(process.execPath, [cliPath, ...process.argv.slice(2)], {
stdio: 'inherit',
env: process.env,
});
process.exit(result.status ?? 1);

View File

@@ -6636,176 +6636,63 @@ 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.
* Verify the prebuilt SDK dist is present and the gsd-sdk shim is wired up.
*
* 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.
* As of fix/2441-sdk-decouple, sdk/dist/ is shipped prebuilt inside the
* get-shit-done-cc npm tarball. The parent package declares a bin entry
* "gsd-sdk": "bin/gsd-sdk.js" so npm chmods the shim correctly when
* installing from a packed tarball — eliminating the mode-644 failure
* (issue #2453) and the build-from-source failure modes (#2439, #2441).
*
* Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit).
* Failures are FATAL — we exit non-zero so install does not complete with a
* silently broken SDK (issue #2439). Set GSD_ALLOW_OFF_PATH=1 to downgrade the
* post-install PATH verification to a warning (exit code 2) for users with an
* intentionally restricted PATH who will wire things up manually.
*/
/**
* Resolve `gsd-sdk` on PATH. Uses `command -v` via `sh -c` on POSIX (portable
* across sh/bash/zsh) and `where` on Windows. Returns trimmed path or null.
*/
function resolveGsdSdk() {
const { spawnSync } = require('child_process');
if (process.platform === 'win32') {
const r = spawnSync('where', ['gsd-sdk'], { encoding: 'utf-8' });
if (r.status === 0 && r.stdout && r.stdout.trim()) {
return r.stdout.trim().split('\n')[0].trim();
}
return null;
}
const r = spawnSync('sh', ['-c', 'command -v gsd-sdk'], { encoding: 'utf-8' });
if (r.status === 0 && r.stdout && r.stdout.trim()) {
return r.stdout.trim();
}
return null;
}
/**
* Best-effort detection of the user's shell rc file for PATH remediation hints.
*/
function detectShellRc() {
const path = require('path');
const shell = process.env.SHELL || '';
const home = process.env.HOME || '~';
if (/\/zsh$/.test(shell)) return { shell: 'zsh', rc: path.join(home, '.zshrc') };
if (/\/bash$/.test(shell)) return { shell: 'bash', rc: path.join(home, '.bashrc') };
if (/\/fish$/.test(shell)) return { shell: 'fish', rc: path.join(home, '.config', 'fish', 'config.fish') };
return { shell: 'sh', rc: path.join(home, '.profile') };
}
/**
* Emit a red fatal banner and exit. Prints actionable PATH remediation when
* the global install succeeded but the bin dir is not on PATH.
* This function verifies the invariant: sdk/dist/cli.js exists and is
* executable. If the execute bit is missing (possible in dev/clone setups
* where sdk/dist was committed without +x), we fix it in-place.
*
* If exitCode is 2, this is the "off-PATH" case and GSD_ALLOW_OFF_PATH respect
* is applied by the caller; we only print.
* --no-sdk skips the check entirely (back-compat).
* --sdk forces the check even if it would otherwise be skipped.
*/
function emitSdkFatal(reason, { globalBin, exitCode }) {
const { shell, rc } = detectShellRc();
const bar = '━'.repeat(72);
const redBold = `${red}${bold}`;
console.error('');
console.error(`${redBold}${bar}${reset}`);
console.error(`${redBold} ✗ GSD SDK install failed — /gsd-* commands will not work${reset}`);
console.error(`${redBold}${bar}${reset}`);
console.error(` ${red}Reason:${reset} ${reason}`);
if (globalBin) {
console.error('');
console.error(` ${yellow}gsd-sdk was installed to:${reset}`);
console.error(` ${cyan}${globalBin}${reset}`);
console.error('');
console.error(` ${yellow}Your shell's PATH does not include this directory.${reset}`);
console.error(` Add it by running:`);
if (shell === 'fish') {
console.error(` ${cyan}fish_add_path "${globalBin}"${reset}`);
console.error(` (or append to ${rc})`);
} else {
console.error(` ${cyan}echo 'export PATH="${globalBin}:$PATH"' >> ${rc}${reset}`);
console.error(` ${cyan}source ${rc}${reset}`);
}
console.error('');
console.error(` Then verify: ${cyan}command -v gsd-sdk${reset}`);
if (exitCode === 2) {
console.error('');
console.error(` ${dim}(GSD_ALLOW_OFF_PATH=1 set → exit ${exitCode} instead of hard failure)${reset}`);
}
} else {
console.error('');
console.error(` Build manually to retry:`);
console.error(` ${cyan}cd <install-dir>/sdk && npm install && npm run build && npm install -g .${reset}`);
}
console.error(`${redBold}${bar}${reset}`);
console.error('');
process.exit(exitCode);
}
function installSdkIfNeeded() {
if (hasNoSdk) {
console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`);
console.log(`\n ${dim}Skipping GSD SDK check (--no-sdk)${reset}`);
return;
}
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
if (!hasSdk) {
const resolved = resolveGsdSdk();
if (resolved) {
console.log(` ${green}${reset} GSD SDK already installed (gsd-sdk on PATH at ${resolved})`);
return;
const sdkCliPath = path.resolve(__dirname, '..', 'sdk', 'dist', 'cli.js');
if (!fs.existsSync(sdkCliPath)) {
const bar = '━'.repeat(72);
const redBold = `${red}${bold}`;
console.error('');
console.error(`${redBold}${bar}${reset}`);
console.error(`${redBold} ✗ GSD SDK dist not found — /gsd-* commands will not work${reset}`);
console.error(`${redBold}${bar}${reset}`);
console.error(` ${red}Reason:${reset} sdk/dist/cli.js not found at ${sdkCliPath}`);
console.error('');
console.error(` This should not happen with a published tarball install.`);
console.error(` If you are running from a git clone, build the SDK first:`);
console.error(` ${cyan}cd sdk && npm install && npm run build${reset}`);
console.error(`${redBold}${bar}${reset}`);
console.error('');
process.exit(1);
}
// Ensure execute bit is set. tsc emits files at 0o644; git clone preserves
// whatever mode was committed. Fix in-place so node-invoked paths work too.
try {
const stat = fs.statSync(sdkCliPath);
const isExecutable = !!(stat.mode & 0o111);
if (!isExecutable) {
fs.chmodSync(sdkCliPath, stat.mode | 0o111);
}
} catch {
// Non-fatal: if chmod fails (e.g. read-only fs) the shim still works via
// `node sdkCliPath` invocation in bin/gsd-sdk.js.
}
// 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');
if (!fs.existsSync(sdkPackageJson)) {
emitSdkFatal(`SDK source tree not found at ${sdkDir}.`, { globalBin: null, exitCode: 1 });
}
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) {
emitSdkFatal('Failed to `npm install` in sdk/.', { globalBin: null, exitCode: 1 });
}
// 2. Compile TypeScript → sdk/dist/
const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
if (buildResult.status !== 0) {
emitSdkFatal('Failed to `npm run build` in sdk/.', { globalBin: null, exitCode: 1 });
}
// 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) {
emitSdkFatal('Failed to `npm install -g .` from sdk/.', { globalBin: null, exitCode: 1 });
}
// 4. 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 (issue #2439 root cause).
const resolved = resolveGsdSdk();
if (resolved) {
console.log(` ${green}${reset} Built and installed GSD SDK from source (gsd-sdk resolved at ${resolved})`);
return;
}
// Off-PATH: resolve npm global bin dir for actionable remediation.
const prefixResult = spawnSync(npmCmd, ['config', 'get', 'prefix'], { encoding: 'utf-8' });
const prefix = prefixResult.status === 0 ? (prefixResult.stdout || '').trim() : null;
const globalBin = prefix
? (process.platform === 'win32' ? prefix : path.join(prefix, 'bin'))
: null;
const allowOffPath = process.env.GSD_ALLOW_OFF_PATH === '1';
emitSdkFatal(
'Built and installed GSD SDK, but `gsd-sdk` is not on your PATH.',
{ globalBin, exitCode: allowOffPath ? 2 : 1 },
);
console.log(` ${green}${reset} GSD SDK ready (sdk/dist/cli.js)`);
}
/**
@@ -6823,13 +6710,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
const finalize = (shouldInstallStatusline) => {
// 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.
// Verify sdk/dist/cli.js is present and executable. The dist is shipped
// prebuilt in the tarball (fix/2441-sdk-decouple); gsd-sdk reaches users via
// the parent package's bin/gsd-sdk.js shim, so no sub-install is needed.
// Skip with --no-sdk.
installSdkIfNeeded();
const printSummaries = () => {

View File

@@ -1,9 +1,10 @@
{
"name": "get-shit-done-cc",
"version": "1.37.1",
"version": "1.38.1",
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
"bin": {
"get-shit-done-cc": "bin/install.js"
"get-shit-done-cc": "bin/install.js",
"gsd-sdk": "bin/gsd-sdk.js"
},
"files": [
"bin",
@@ -14,6 +15,7 @@
"scripts",
"sdk/src",
"sdk/prompts",
"sdk/dist",
"sdk/package.json",
"sdk/package-lock.json",
"sdk/tsconfig.json"

View File

@@ -34,7 +34,7 @@
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build",
"prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js",
"test": "vitest run",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration"

View File

@@ -0,0 +1,149 @@
/**
* Regression tests for fix/2441-sdk-decouple
*
* Verifies the architectural invariants introduced by the SDK decouple:
*
* (a) bin/install.js does NOT invoke `npm install -g` for the SDK at all.
* The old `installSdkIfNeeded()` built from source and ran `npm install -g .`
* in sdk/; the new version only verifies the prebuilt dist.
*
* (b) The parent package.json declares a `gsd-sdk` bin entry pointing at
* bin/gsd-sdk.js (the back-compat shim), so npm chmods it correctly.
*
* (c) sdk/dist/ is in the parent package `files` so it ships in the tarball.
*
* (d) sdk/package.json `prepublishOnly` runs `rm -rf dist && tsc && chmod +x dist/cli.js`
* (guards against the mode-644 bug and npm's stale-prepublishOnly issue).
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const INSTALL_JS = path.join(__dirname, '..', 'bin', 'install.js');
const ROOT_PKG = path.join(__dirname, '..', 'package.json');
const SDK_PKG = path.join(__dirname, '..', 'sdk', 'package.json');
const GSD_SDK_SHIM = path.join(__dirname, '..', 'bin', 'gsd-sdk.js');
const installContent = fs.readFileSync(INSTALL_JS, 'utf-8');
const rootPkg = JSON.parse(fs.readFileSync(ROOT_PKG, 'utf-8'));
const sdkPkg = JSON.parse(fs.readFileSync(SDK_PKG, 'utf-8'));
describe('fix #2441: SDK decouple — installer no longer builds from source', () => {
test('bin/install.js does not call npm install -g in sdk/', () => {
// The old approach ran `npm install -g .` from sdk/. This must be gone.
// We check for the specific pattern that installed the SDK globally.
const hasGlobalInstallFromSdk =
/spawnSync\(npmCmd,\s*\[['"]install['"],\s*['"](-g|--global)['"]/m.test(installContent) &&
/cwd:\s*sdkDir/.test(installContent);
assert.ok(
!hasGlobalInstallFromSdk,
'bin/install.js must not run `npm install -g .` from sdk/. ' +
'The SDK is shipped prebuilt in the tarball (fix #2441).'
);
});
test('bin/install.js does not run npm run build in sdk/', () => {
// The old approach ran `npm run build` (tsc) at install time.
const hasBuildStep =
/spawnSync\(npmCmd,\s*\[['"]run['"],\s*['"]build['"]\]/m.test(installContent) &&
/cwd:\s*sdkDir/.test(installContent);
assert.ok(
!hasBuildStep,
'bin/install.js must not run `npm run build` in sdk/ at install time. ' +
'TypeScript compilation happens at publish time via prepublishOnly.'
);
});
test('installSdkIfNeeded checks sdk/dist/cli.js exists instead of building', () => {
assert.ok(
installContent.includes('sdk/dist/cli.js') || installContent.includes("'dist', 'cli.js'"),
'installSdkIfNeeded() must reference sdk/dist/cli.js to verify the prebuilt dist.'
);
});
});
describe('fix #2441: back-compat shim — parent package bin entry', () => {
test('root package.json declares gsd-sdk bin entry', () => {
assert.ok(
rootPkg.bin && rootPkg.bin['gsd-sdk'],
'root package.json must have a bin["gsd-sdk"] entry for the back-compat shim.'
);
});
test('gsd-sdk bin entry points at bin/gsd-sdk.js', () => {
assert.equal(
rootPkg.bin['gsd-sdk'],
'bin/gsd-sdk.js',
'bin["gsd-sdk"] must point at bin/gsd-sdk.js'
);
});
test('bin/gsd-sdk.js shim file exists', () => {
assert.ok(
fs.existsSync(GSD_SDK_SHIM),
'bin/gsd-sdk.js must exist as the back-compat PATH shim.'
);
});
test('bin/gsd-sdk.js forwards to sdk/dist/cli.js', () => {
const shimContent = fs.readFileSync(GSD_SDK_SHIM, 'utf-8');
assert.ok(
shimContent.includes('sdk') && shimContent.includes('dist') && shimContent.includes('cli.js'),
'bin/gsd-sdk.js must resolve and delegate to sdk/dist/cli.js.'
);
});
test('bin/gsd-sdk.js uses node to invoke cli.js (no direct exec dependency)', () => {
const shimContent = fs.readFileSync(GSD_SDK_SHIM, 'utf-8');
assert.ok(
shimContent.includes('process.execPath') || shimContent.includes('spawnSync') || shimContent.includes('node'),
'bin/gsd-sdk.js must invoke sdk/dist/cli.js via node, not rely on execute bit.'
);
});
});
describe('fix #2441: sdk/dist shipped in tarball', () => {
test('root package.json files includes sdk/dist', () => {
assert.ok(
Array.isArray(rootPkg.files) && rootPkg.files.some(f => f === 'sdk/dist' || f.startsWith('sdk/dist')),
'root package.json files must include "sdk/dist" so the prebuilt CLI ships in the tarball.'
);
});
test('root package.json files still includes sdk/src (for dev/clone builds)', () => {
assert.ok(
Array.isArray(rootPkg.files) && rootPkg.files.some(f => f === 'sdk/src' || f.startsWith('sdk/src')),
'root package.json files should still include sdk/src for developer builds.'
);
});
});
describe('fix #2453: sdk/package.json prepublishOnly guards execute bit', () => {
test('sdk prepublishOnly deletes old dist before build (npm stale-prepublishOnly guard)', () => {
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
assert.ok(
prepub && prepub.includes('rm -rf dist'),
'sdk/package.json prepublishOnly must start with `rm -rf dist` to avoid stale build output.'
);
});
test('sdk prepublishOnly chmods dist/cli.js after tsc', () => {
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
assert.ok(
prepub && prepub.includes('chmod +x dist/cli.js'),
'sdk/package.json prepublishOnly must run `chmod +x dist/cli.js` after tsc to fix mode-644 (#2453).'
);
});
test('sdk prepublishOnly runs tsc', () => {
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
assert.ok(
prepub && prepub.includes('tsc'),
'sdk/package.json prepublishOnly must include tsc to compile TypeScript.'
);
});
});

View File

@@ -80,35 +80,42 @@ describe('#1657 / #2385: SDK install must be wired into installer source', () =>
);
});
test('install.js builds gsd-sdk from in-repo sdk/ source (#2385)', () => {
test('install.js verifies prebuilt sdk/dist/cli.js instead of building from source (#2441)', () => {
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.
// As of fix/2441-sdk-decouple, the installer no longer runs `npm run build`
// or `npm install -g .` from sdk/. Instead it verifies sdk/dist/cli.js exists
// (shipped prebuilt in the tarball) and optionally chmods it.
assert.ok(
src.includes("path.resolve(__dirname, '..', 'sdk')") ||
src.includes('path.resolve(__dirname, "..", "sdk")'),
'installer must locate the in-repo sdk/ directory'
src.includes('sdk/dist/cli.js') || src.includes("'dist', 'cli.js'"),
'installer must reference sdk/dist/cli.js to verify the prebuilt dist (#2441)'
);
// Confirm the old build-from-source pattern is gone.
const hasBuildFromSource =
src.includes("['run', 'build']") &&
src.includes("cwd: sdkDir");
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'
!hasBuildFromSource,
'installer must NOT run `npm run build` from sdk/ at install time (#2441)'
);
const hasGlobalInstall =
(src.includes("['install', '-g', '.']") || src.includes("'npm install -g .'")) &&
src.includes("cwd: sdkDir");
assert.ok(
src.includes("['run', 'build']"),
'installer must compile TypeScript via `npm run build` before installing globally'
!hasGlobalInstall,
'installer must NOT run `npm install -g .` from sdk/ (#2441)'
);
});
test('package.json ships sdk source in published tarball (#2385)', () => {
test('package.json ships sdk dist and source in published tarball (#2441)', () => {
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'
files.some((f) => f === 'sdk/src' || f.startsWith('sdk/src')),
'root package.json `files` must include sdk/src'
);
assert.ok(
files.some((f) => f === 'sdk/dist' || f.startsWith('sdk/dist')),
'root package.json `files` must include sdk/dist so the prebuilt CLI ships in the tarball (#2441)'
);
});
});