fix(install): fatal SDK install failures + CI smoke gate (#2439)
## Why #2386 added `installSdkIfNeeded()` to build @gsd-build/sdk from bundled source and `npm install -g .`, because the npm-published @gsd-build/sdk is intentionally frozen and version-mismatched with get-shit-done-cc. But every failure path in that function was warning-only — including the final `which gsd-sdk` verification. When npm's global bin is off a user's PATH (common on macOS), the installer printed a yellow warning then exited 0. Users saw "install complete" and then every `/gsd-*` command crashed with `command not found: gsd-sdk` (the #2439 symptom). No CI job executed the install path, so this class of regression could ship undetected — existing "install" tests only read bin/install.js as a string. ## What changed **bin/install.js — installSdkIfNeeded() is now transactional** - All build/install failures exit non-zero (not just warn). - Post-install `which gsd-sdk` check is fatal: if the binary landed globally but is off PATH, we exit 1 with a red banner showing the resolved npm bin dir, the user's shell, the target rc file, and the exact `export PATH=…` line to add. - Escape hatch: `GSD_ALLOW_OFF_PATH=1` downgrades off-PATH to exit 2 for users with intentionally restricted PATH who will wire up the binary manually. - Resolver uses POSIX `command -v` via `sh -c` (replaces `which`) so behavior is consistent across sh/bash/zsh/fish. - Factored `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()`. **.github/workflows/install-smoke.yml (new)** - Executes the real install path: `npm pack` → `npm install -g <tgz>` → run installer non-interactively → `command -v gsd-sdk` → run `gsd-sdk --version`. - PRs: path-filtered to installer-adjacent files, ubuntu + Node 22 only. - main/release branches: full matrix (ubuntu+macos × Node 22+24). - Reusable via workflow_call with `ref` input for release gating. **.github/workflows/release.yml — pre-publish gate** - New `install-smoke-rc` and `install-smoke-finalize` jobs invoke the reusable workflow against the release branch. `rc` and `finalize` now `needs: [validate-version, install-smoke-*]`, so a broken SDK install blocks `npm publish`. ## Test plan - Local full suite: 4154/4154 pass - install-smoke.yml will self-validate on this PR (ubuntu+Node22 only) Addresses root cause of #2439 (the per-command pre-flight in #2440 is the complementary defensive layer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
152
.github/workflows/install-smoke.yml
vendored
Normal file
152
.github/workflows/install-smoke.yml
vendored
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
name: Install Smoke
|
||||||
|
|
||||||
|
# Exercises the real install path: `npm pack` → `npm install -g <tarball>`
|
||||||
|
# → run `bin/install.js` → assert `gsd-sdk` is on PATH.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# - PRs: path-filtered, minimal runner (ubuntu + Node LTS) for fast signal.
|
||||||
|
# - Push to release branches / main: full matrix.
|
||||||
|
# - workflow_call: invoked from release.yml as a pre-publish gate.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'bin/install.js'
|
||||||
|
- 'sdk/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- '.github/workflows/install-smoke.yml'
|
||||||
|
- '.github/workflows/release.yml'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release/**'
|
||||||
|
- 'hotfix/**'
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: 'Git ref to check out (branch or SHA). Defaults to the triggering ref.'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: install-smoke-${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 12
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# PRs run the minimal path (ubuntu + LTS). Pushes / release branches
|
||||||
|
# and workflow_call add macOS + Node 24 coverage.
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
node-version: 22
|
||||||
|
full_only: false
|
||||||
|
- os: ubuntu-latest
|
||||||
|
node-version: 24
|
||||||
|
full_only: true
|
||||||
|
- os: macos-latest
|
||||||
|
node-version: 24
|
||||||
|
full_only: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Skip full-only matrix entry on PR
|
||||||
|
id: skip
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EVENT: ${{ github.event_name }}
|
||||||
|
FULL_ONLY: ${{ matrix.full_only }}
|
||||||
|
run: |
|
||||||
|
if [ "$EVENT" = "pull_request" ] && [ "$FULL_ONLY" = "true" ]; then
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
|
- name: Set up Node.js ${{ matrix.node-version }}
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install root deps
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Pack root tarball
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
id: pack
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
npm pack --silent
|
||||||
|
TARBALL=$(ls get-shit-done-cc-*.tgz | head -1)
|
||||||
|
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Packed: $TARBALL"
|
||||||
|
|
||||||
|
- name: Ensure npm global bin is on PATH (CI runner default may differ)
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
NPM_BIN="$(npm config get prefix)/bin"
|
||||||
|
echo "$NPM_BIN" >> "$GITHUB_PATH"
|
||||||
|
echo "npm global bin: $NPM_BIN"
|
||||||
|
|
||||||
|
- name: Install tarball globally (runs bin/install.js → installSdkIfNeeded)
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TARBALL: ${{ steps.pack.outputs.tarball }}
|
||||||
|
WORKSPACE: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TMPDIR_ROOT=$(mktemp -d)
|
||||||
|
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.
|
||||||
|
get-shit-done-cc --claude --local || true
|
||||||
|
|
||||||
|
- name: Assert gsd-sdk resolves on PATH
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
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 install — installSdkIfNeeded() regression"
|
||||||
|
NPM_BIN="$(npm config get prefix)/bin"
|
||||||
|
echo "npm global bin: $NPM_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
|
||||||
|
if: steps.skip.outputs.skip != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gsd-sdk --version || gsd-sdk --help
|
||||||
|
echo "✓ gsd-sdk is executable"
|
||||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -113,9 +113,18 @@ jobs:
|
|||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "Next: run this workflow with \`rc\` action to publish a pre-release to \`next\`" >> "$GITHUB_STEP_SUMMARY"
|
echo "Next: run this workflow with \`rc\` action to publish a pre-release to \`next\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
rc:
|
install-smoke-rc:
|
||||||
needs: validate-version
|
needs: validate-version
|
||||||
if: inputs.action == 'rc'
|
if: inputs.action == 'rc'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/install-smoke.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.validate-version.outputs.branch }}
|
||||||
|
|
||||||
|
rc:
|
||||||
|
needs: [validate-version, install-smoke-rc]
|
||||||
|
if: inputs.action == 'rc'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
@@ -251,9 +260,18 @@ jobs:
|
|||||||
echo "To publish another pre-release: run \`rc\` again" >> "$GITHUB_STEP_SUMMARY"
|
echo "To publish another pre-release: run \`rc\` again" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "To finalize: run \`finalize\` action" >> "$GITHUB_STEP_SUMMARY"
|
echo "To finalize: run \`finalize\` action" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
finalize:
|
install-smoke-finalize:
|
||||||
needs: validate-version
|
needs: validate-version
|
||||||
if: inputs.action == 'finalize'
|
if: inputs.action == 'finalize'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/install-smoke.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.validate-version.outputs.branch }}
|
||||||
|
|
||||||
|
finalize:
|
||||||
|
needs: [validate-version, install-smoke-finalize]
|
||||||
|
if: inputs.action == 'finalize'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
148
bin/install.js
148
bin/install.js
@@ -10,6 +10,8 @@ const crypto = require('crypto');
|
|||||||
const cyan = '\x1b[36m';
|
const cyan = '\x1b[36m';
|
||||||
const green = '\x1b[32m';
|
const green = '\x1b[32m';
|
||||||
const yellow = '\x1b[33m';
|
const yellow = '\x1b[33m';
|
||||||
|
const red = '\x1b[31m';
|
||||||
|
const bold = '\x1b[1m';
|
||||||
const dim = '\x1b[2m';
|
const dim = '\x1b[2m';
|
||||||
const reset = '\x1b[0m';
|
const reset = '\x1b[0m';
|
||||||
|
|
||||||
@@ -6643,8 +6645,94 @@ function promptLocation(runtimes) {
|
|||||||
* every /gsd-* command that depends on newer query handlers.
|
* every /gsd-* command that depends on newer query handlers.
|
||||||
*
|
*
|
||||||
* Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit).
|
* Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit).
|
||||||
* Failures are warnings, not fatal.
|
* 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.
|
||||||
|
*
|
||||||
|
* If exitCode is 2, this is the "off-PATH" case and GSD_ALLOW_OFF_PATH respect
|
||||||
|
* is applied by the caller; we only print.
|
||||||
|
*/
|
||||||
|
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() {
|
function installSdkIfNeeded() {
|
||||||
if (hasNoSdk) {
|
if (hasNoSdk) {
|
||||||
console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`);
|
console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`);
|
||||||
@@ -6656,9 +6744,9 @@ function installSdkIfNeeded() {
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
if (!hasSdk) {
|
if (!hasSdk) {
|
||||||
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['gsd-sdk'], { stdio: 'ignore' });
|
const resolved = resolveGsdSdk();
|
||||||
if (probe.status === 0) {
|
if (resolved) {
|
||||||
console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH)`);
|
console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH at ${resolved})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6671,17 +6759,8 @@ function installSdkIfNeeded() {
|
|||||||
const sdkDir = path.resolve(__dirname, '..', 'sdk');
|
const sdkDir = path.resolve(__dirname, '..', 'sdk');
|
||||||
const sdkPackageJson = path.join(sdkDir, 'package.json');
|
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)) {
|
if (!fs.existsSync(sdkPackageJson)) {
|
||||||
warnManual(`SDK source tree not found at ${sdkDir}.`);
|
emitSdkFatal(`SDK source tree not found at ${sdkDir}.`, { globalBin: null, exitCode: 1 });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n ${cyan}Building GSD SDK from source (${sdkDir})…${reset}`);
|
console.log(`\n ${cyan}Building GSD SDK from source (${sdkDir})…${reset}`);
|
||||||
@@ -6690,36 +6769,43 @@ function installSdkIfNeeded() {
|
|||||||
// 1. Install sdk build-time dependencies (tsc, etc.)
|
// 1. Install sdk build-time dependencies (tsc, etc.)
|
||||||
const installResult = spawnSync(npmCmd, ['install'], { cwd: sdkDir, stdio: 'inherit' });
|
const installResult = spawnSync(npmCmd, ['install'], { cwd: sdkDir, stdio: 'inherit' });
|
||||||
if (installResult.status !== 0) {
|
if (installResult.status !== 0) {
|
||||||
warnManual('Failed to `npm install` in sdk/.');
|
emitSdkFatal('Failed to `npm install` in sdk/.', { globalBin: null, exitCode: 1 });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Compile TypeScript → sdk/dist/
|
// 2. Compile TypeScript → sdk/dist/
|
||||||
const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
|
const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
|
||||||
if (buildResult.status !== 0) {
|
if (buildResult.status !== 0) {
|
||||||
warnManual('Failed to `npm run build` in sdk/.');
|
emitSdkFatal('Failed to `npm run build` in sdk/.', { globalBin: null, exitCode: 1 });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Install the built package globally so `gsd-sdk` lands on PATH.
|
// 3. Install the built package globally so `gsd-sdk` lands on PATH.
|
||||||
const globalResult = spawnSync(npmCmd, ['install', '-g', '.'], { cwd: sdkDir, stdio: 'inherit' });
|
const globalResult = spawnSync(npmCmd, ['install', '-g', '.'], { cwd: sdkDir, stdio: 'inherit' });
|
||||||
if (globalResult.status !== 0) {
|
if (globalResult.status !== 0) {
|
||||||
warnManual('Failed to `npm install -g .` from sdk/.');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify gsd-sdk is actually resolvable on PATH. npm's global bin dir is
|
// Off-PATH: resolve npm global bin dir for actionable remediation.
|
||||||
// not always on the current shell's PATH (Homebrew prefixes, nvm setups,
|
const prefixResult = spawnSync(npmCmd, ['config', 'get', 'prefix'], { encoding: 'utf-8' });
|
||||||
// unconfigured npm prefix), so a zero exit status from `npm install -g`
|
const prefix = prefixResult.status === 0 ? (prefixResult.stdout || '').trim() : null;
|
||||||
// alone is not proof of a working binary.
|
const globalBin = prefix
|
||||||
const resolverCmd = process.platform === 'win32' ? 'where' : 'which';
|
? (process.platform === 'win32' ? prefix : path.join(prefix, 'bin'))
|
||||||
const verify = spawnSync(resolverCmd, ['gsd-sdk'], { encoding: 'utf-8' });
|
: null;
|
||||||
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]})`);
|
const allowOffPath = process.env.GSD_ALLOW_OFF_PATH === '1';
|
||||||
} else {
|
emitSdkFatal(
|
||||||
warnManual('Built and installed GSD SDK from source but gsd-sdk is not on PATH — npm global bin may not be in your PATH.');
|
'Built and installed GSD SDK, but `gsd-sdk` is not on your PATH.',
|
||||||
if (verify.stderr) console.warn(` resolver stderr: ${verify.stderr.trim()}`);
|
{ globalBin, exitCode: allowOffPath ? 2 : 1 },
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user