From 098ed35a50256b6d6729331d50caee2819789c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sat, 14 Mar 2026 10:04:12 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20broken=20npm=20install=20=E2=80=94=20rem?= =?UTF-8?q?ove=20bundleDependencies,=20use=20postinstall=20symlinks=20(#36?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove @gsd/* cross-deps that break npm install (#hotfix) Workspace packages declared @gsd/* as dependencies in their own package.json files. npm's bundleDependencies bundles packages into node_modules/ but still tries to resolve sub-dependencies from the registry — causing 404s for the unpublished @gsd/* scope. - Remove @gsd/* from all dependencies (root and workspace packages) - Add validate-pack.sh: tests tarball installability before publish - Wire validate-pack into CI (every PR) and publish pipeline - Bump to v2.10.10 - Update changelog Co-Authored-By: Claude Opus 4.6 (1M context) * fix: drop bundleDependencies, use postinstall symlinks instead bundleDependencies with workspace packages causes npm to resolve @gsd/* from the registry during install — 404 since they're not published. Replace with a postinstall script that creates node_modules/@gsd/* symlinks pointing to packages/*. - Remove @gsd/* from dependencies and bundleDependencies - Add link-workspace-packages.cjs (CJS, runs before ESM postinstall) - Update validate-pack to verify symlinks after install - Include link script in files array Co-Authored-By: Claude Opus 4.6 (1M context) * fix: robust validate-pack + fallback workspace linking - Keep @gsd/* in bundleDependencies (for npm pack bundling) - Remove @gsd/* from root dependencies (prevents 404 registry lookups) - Add link-workspace-packages.cjs fallback for when bundled symlinks aren't created - Simplified validate-pack with better error diagnostics Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove bundleDependencies — use postinstall symlinks only npm 10.x fetches packument metadata for ALL deps including bundled ones. @gsd/* packages don't exist on npm → 404 → hard install failure. bundleDependencies is fundamentally broken for unpublished workspace packages. Replace with: - packages/ shipped via files array (already was) - link-workspace-packages.cjs creates node_modules/@gsd/* symlinks in postinstall, pointing to packages/* - No @gsd/* in dependencies or bundleDependencies at all Tarball drops from 40M to 3M (no bundled node_modules). Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add .npmignore to prevent .gitignore from excluding dist/ .gitignore contains /dist/ and packages/*/dist/ which are needed in the published tarball. Without .npmignore, npm pack respects .gitignore and excludes them — even though "files" in package.json should override. An empty .npmignore causes npm to ignore .gitignore entirely, letting the "files" field control what's packed. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: avoid SIGPIPE in validate-pack on Linux tar | grep -q causes SIGPIPE (exit 141) on Linux when grep closes the pipe early. Write tar listing to a temp file and grep that instead. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/build-native.yml | 9 +-- .github/workflows/ci.yml | 3 + .npmignore | 8 +++ CHANGELOG.md | 9 ++- native/npm/darwin-arm64/package.json | 2 +- native/npm/darwin-x64/package.json | 2 +- native/npm/linux-arm64-gnu/package.json | 2 +- native/npm/linux-x64-gnu/package.json | 2 +- native/npm/win32-x64-msvc/package.json | 2 +- package-lock.json | 25 +------- package.json | 19 ++---- packages/pi-agent-core/package.json | 4 +- packages/pi-coding-agent/package.json | 4 -- packages/pi-tui/package.json | 1 - scripts/link-workspace-packages.cjs | 70 +++++++++++++++++++++ scripts/validate-pack.sh | 83 +++++++++++++++++++++++++ 16 files changed, 188 insertions(+), 57 deletions(-) create mode 100644 .npmignore create mode 100644 scripts/link-workspace-packages.cjs create mode 100755 scripts/validate-pack.sh diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index 134cbfe45..6b2132378 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -175,13 +175,8 @@ jobs: - name: Verify dist exists run: test -s dist/loader.js || { echo "::error::dist/loader.js missing or empty after build"; exit 1; } - - name: Verify tarball contents - run: | - npm pack --dry-run --ignore-scripts 2>&1 | tee /tmp/pack-output.txt - grep -q "dist/loader.js" /tmp/pack-output.txt || { - echo "::error::dist/loader.js not in tarball" - exit 1 - } + - name: Validate package is installable + run: npm run validate-pack - name: Publish main package env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a80dc9d4..bccb8ac90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: - name: Build run: npm run build + - name: Validate package is installable + run: npm run validate-pack + - name: Run unit tests run: npm run test:unit diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..1cd3f1586 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +# .npmignore — override .gitignore for npm pack +# When this file exists, npm ignores .gitignore entirely. +# The "files" field in package.json determines what's included. +# This file only needs to exclude things NOT covered by "files". + +# Everything not in the "files" array is excluded by default. +# This file exists solely to prevent .gitignore from interfering +# with npm pack (e.g., .gitignore excludes dist/ which we need). diff --git a/CHANGELOG.md b/CHANGELOG.md index 40425c809..7c3b1354c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Fallback `--backend=local` for offline faster-whisper on CPU - Venv-aware Python detection (`~/.gsd/voice-venv/bin/python3`) +## [2.10.10] - 2026-03-14 + +### Fixed +- Fix broken `npm install` / `npx gsd-pi@latest` caused by unpublished `@gsd/*` workspace packages leaking into npm dependencies. Workspace cross-references removed from published package metadata; packages resolve via bundled `node_modules/` at runtime. +- Add pre-publish tarball install validation (`validate-pack`) to CI and publish pipeline, preventing broken packages from reaching npm. + ## [2.10.9] - 2026-03-14 ### Added @@ -485,7 +491,8 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - License updated to MIT -[Unreleased]: https://github.com/gsd-build/gsd-2/compare/v2.10.9...HEAD +[Unreleased]: https://github.com/gsd-build/gsd-2/compare/v2.10.10...HEAD +[2.10.10]: https://github.com/gsd-build/gsd-2/compare/v2.10.9...v2.10.10 [2.10.9]: https://github.com/gsd-build/gsd-2/compare/v2.10.8...v2.10.9 [2.10.8]: https://github.com/gsd-build/gsd-2/compare/v2.10.7...v2.10.8 [2.10.7]: https://github.com/gsd-build/gsd-2/compare/v2.10.6...v2.10.7 diff --git a/native/npm/darwin-arm64/package.json b/native/npm/darwin-arm64/package.json index 3ed3dd914..9bebeea55 100644 --- a/native/npm/darwin-arm64/package.json +++ b/native/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@gsd-build/engine-darwin-arm64", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD native engine binary for macOS ARM64", "os": [ "darwin" diff --git a/native/npm/darwin-x64/package.json b/native/npm/darwin-x64/package.json index dd7edc07c..171b2e2d4 100644 --- a/native/npm/darwin-x64/package.json +++ b/native/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@gsd-build/engine-darwin-x64", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD native engine binary for macOS Intel", "os": [ "darwin" diff --git a/native/npm/linux-arm64-gnu/package.json b/native/npm/linux-arm64-gnu/package.json index 29ec15ae5..4cc9fea4f 100644 --- a/native/npm/linux-arm64-gnu/package.json +++ b/native/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@gsd-build/engine-linux-arm64-gnu", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD native engine binary for Linux ARM64 (glibc)", "os": [ "linux" diff --git a/native/npm/linux-x64-gnu/package.json b/native/npm/linux-x64-gnu/package.json index 26a153d42..ee27b9528 100644 --- a/native/npm/linux-x64-gnu/package.json +++ b/native/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@gsd-build/engine-linux-x64-gnu", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD native engine binary for Linux x64 (glibc)", "os": [ "linux" diff --git a/native/npm/win32-x64-msvc/package.json b/native/npm/win32-x64-msvc/package.json index 2058d4979..d74a36ba0 100644 --- a/native/npm/win32-x64-msvc/package.json +++ b/native/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@gsd-build/engine-win32-x64-msvc", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD native engine binary for Windows x64 (MSVC)", "os": [ "win32" diff --git a/package-lock.json b/package-lock.json index 4be46afa1..fd515be25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,12 @@ { "name": "gsd-pi", - "version": "2.10.8", + "version": "2.10.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gsd-pi", - "version": "2.10.8", - "bundleDependencies": [ - "@gsd/native", - "@gsd/pi-agent-core", - "@gsd/pi-ai", - "@gsd/pi-coding-agent", - "@gsd/pi-tui" - ], + "version": "2.10.10", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -21,10 +14,6 @@ ], "dependencies": { "@clack/prompts": "^1.1.0", - "@gsd/pi-agent-core": "*", - "@gsd/pi-ai": "*", - "@gsd/pi-coding-agent": "*", - "@gsd/pi-tui": "*", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "playwright": "^1.58.2", @@ -3733,10 +3722,7 @@ }, "packages/pi-agent-core": { "name": "@gsd/pi-agent-core", - "version": "0.57.1", - "dependencies": { - "@gsd/pi-ai": "*" - } + "version": "0.57.1" }, "packages/pi-ai": { "name": "@gsd/pi-ai", @@ -3760,10 +3746,6 @@ "name": "@gsd/pi-coding-agent", "version": "0.57.1", "dependencies": { - "@gsd/native": "*", - "@gsd/pi-agent-core": "*", - "@gsd/pi-ai": "*", - "@gsd/pi-tui": "*", "@mariozechner/jiti": "^2.6.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", @@ -3792,7 +3774,6 @@ "name": "@gsd/pi-tui", "version": "0.57.1", "dependencies": { - "@gsd/native": "*", "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", diff --git a/package.json b/package.json index 86e5efcf3..e48779347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gsd-pi", - "version": "2.10.9", + "version": "2.10.10", "description": "GSD — Get Shit Done coding agent", "license": "MIT", "repository": { @@ -25,6 +25,7 @@ "pkg", "src/resources", "scripts/postinstall.js", + "scripts/link-workspace-packages.cjs", "package.json", "README.md" ], @@ -52,31 +53,21 @@ "build:native": "node native/scripts/build.js", "build:native:dev": "node native/scripts/build.js --dev", "dev": "tsc --watch", - "postinstall": "node scripts/postinstall.js", + "postinstall": "node scripts/link-workspace-packages.cjs && node scripts/postinstall.js", "pi:install-global": "node scripts/install-pi-global.js", "pi:uninstall-global": "node scripts/uninstall-pi-global.js", "sync-pkg-version": "node scripts/sync-pkg-version.cjs", "sync-platform-versions": "node native/scripts/sync-platform-versions.cjs", - "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && git diff --exit-code || (echo 'ERROR: version sync changed files — commit them before publishing' && exit 1) && npm run build" + "validate-pack": "bash scripts/validate-pack.sh", + "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && git diff --exit-code || (echo 'ERROR: version sync changed files — commit them before publishing' && exit 1) && npm run build && npm run validate-pack" }, "dependencies": { "@clack/prompts": "^1.1.0", - "@gsd/pi-agent-core": "*", - "@gsd/pi-ai": "*", - "@gsd/pi-coding-agent": "*", - "@gsd/pi-tui": "*", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "playwright": "^1.58.2", "sharp": "^0.34.5" }, - "bundleDependencies": [ - "@gsd/native", - "@gsd/pi-agent-core", - "@gsd/pi-ai", - "@gsd/pi-coding-agent", - "@gsd/pi-tui" - ], "devDependencies": { "@types/node": "^22.0.0", "@types/picomatch": "^4.0.2", diff --git a/packages/pi-agent-core/package.json b/packages/pi-agent-core/package.json index 60202508c..18a713efb 100644 --- a/packages/pi-agent-core/package.json +++ b/packages/pi-agent-core/package.json @@ -8,7 +8,5 @@ "scripts": { "build": "tsc -p tsconfig.json" }, - "dependencies": { - "@gsd/pi-ai": "*" - } + "dependencies": {} } diff --git a/packages/pi-coding-agent/package.json b/packages/pi-coding-agent/package.json index 11e1231c1..81ab93229 100644 --- a/packages/pi-coding-agent/package.json +++ b/packages/pi-coding-agent/package.json @@ -24,10 +24,6 @@ "copy-assets": "node -e \"const{mkdirSync,cpSync}=require('fs');mkdirSync('dist/modes/interactive/theme',{recursive:true});cpSync('src/modes/interactive/theme','dist/modes/interactive/theme',{recursive:true,filter:(s)=>!s.endsWith('.ts')});mkdirSync('dist/core/export-html/vendor',{recursive:true});cpSync('src/core/export-html/template.html','dist/core/export-html/template.html');cpSync('src/core/export-html/template.css','dist/core/export-html/template.css');cpSync('src/core/export-html/template.js','dist/core/export-html/template.js');cpSync('src/core/export-html/vendor','dist/core/export-html/vendor',{recursive:true,filter:(s)=>!s.endsWith('.ts')});mkdirSync('dist/core/lsp',{recursive:true});cpSync('src/core/lsp/defaults.json','dist/core/lsp/defaults.json');cpSync('src/core/lsp/lsp.md','dist/core/lsp/lsp.md')\"" }, "dependencies": { - "@gsd/native": "*", - "@gsd/pi-agent-core": "*", - "@gsd/pi-ai": "*", - "@gsd/pi-tui": "*", "@mariozechner/jiti": "^2.6.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", diff --git a/packages/pi-tui/package.json b/packages/pi-tui/package.json index a16b6726d..c5611eff7 100644 --- a/packages/pi-tui/package.json +++ b/packages/pi-tui/package.json @@ -9,7 +9,6 @@ "build": "tsc -p tsconfig.json" }, "dependencies": { - "@gsd/native": "*", "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", diff --git a/scripts/link-workspace-packages.cjs b/scripts/link-workspace-packages.cjs new file mode 100644 index 000000000..43ee66a83 --- /dev/null +++ b/scripts/link-workspace-packages.cjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/** + * link-workspace-packages.cjs + * + * Creates node_modules/@gsd/* symlinks pointing to packages/* directories. + * + * During development, npm workspaces creates these automatically. But in the + * published tarball, workspace packages are shipped under packages/ (via the + * "files" field) and the @gsd/* imports in compiled code need node_modules/@gsd/* + * to resolve. This script bridges the gap. + * + * Runs as part of postinstall (before any ESM code that imports @gsd/*). + */ +const { existsSync, mkdirSync, symlinkSync, lstatSync, readlinkSync, unlinkSync, readdirSync } = require('fs') +const { resolve, join } = require('path') + +const root = resolve(__dirname, '..') +const packagesDir = join(root, 'packages') +const nodeModulesGsd = join(root, 'node_modules', '@gsd') + +// Map directory names to package names +const packageMap = { + 'native': 'native', + 'pi-agent-core': 'pi-agent-core', + 'pi-ai': 'pi-ai', + 'pi-coding-agent': 'pi-coding-agent', + 'pi-tui': 'pi-tui', +} + +// Ensure @gsd scope directory exists +if (!existsSync(nodeModulesGsd)) { + mkdirSync(nodeModulesGsd, { recursive: true }) +} + +let linked = 0 +for (const [dir, name] of Object.entries(packageMap)) { + const source = join(packagesDir, dir) + const target = join(nodeModulesGsd, name) + + if (!existsSync(source)) continue + + // Skip if already correctly linked or is a real directory (bundled) + if (existsSync(target)) { + try { + const stat = lstatSync(target) + if (stat.isSymbolicLink()) { + const linkTarget = readlinkSync(target) + if (resolve(join(nodeModulesGsd, linkTarget)) === source || linkTarget === source) { + continue // Already correct + } + unlinkSync(target) // Wrong target, relink + } else { + continue // Real directory (e.g., from bundleDependencies), don't touch + } + } catch { + continue + } + } + + try { + symlinkSync(source, target, 'junction') // junction works on Windows too + linked++ + } catch { + // Non-fatal — may fail in read-only environments + } +} + +if (linked > 0) { + process.stderr.write(` Linked ${linked} workspace packages\n`) +} diff --git a/scripts/validate-pack.sh b/scripts/validate-pack.sh new file mode 100755 index 000000000..85d62b36c --- /dev/null +++ b/scripts/validate-pack.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# validate-pack.sh — Verify the npm tarball is installable before publishing. +# +# Usage: npm run validate-pack (or bash scripts/validate-pack.sh) +# Exit 0 = safe to publish, Exit 1 = broken package. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT" + +# --- Guard: workspace packages must not have @gsd/* cross-deps --- +echo "==> Checking workspace packages for @gsd/* cross-deps..." +CROSS_FAILED=0 +for ws_pkg in native pi-agent-core pi-ai pi-coding-agent pi-tui; do + RESULT=$(node -e " + const pkg = require('./packages/${ws_pkg}/package.json'); + const deps = Object.keys(pkg.dependencies || {}).filter(d => d.startsWith('@gsd/')); + if (deps.length) { console.log(deps.join(', ')); process.exit(1); } + " 2>&1) || { + echo " LEAKED in ${ws_pkg}: $RESULT" + CROSS_FAILED=1 + true + } +done +if [ "$CROSS_FAILED" = "1" ]; then + echo "ERROR: Workspace packages have @gsd/* cross-dependencies." + echo " These cause 404s when npm resolves them from the registry." + exit 1 +fi +echo " No @gsd/* cross-dependencies." + +# --- Pack tarball --- +echo "==> Packing tarball..." +TARBALL_NAME=$(npm pack --ignore-scripts 2>/dev/null | tail -1) +TARBALL="$ROOT/$TARBALL_NAME" + +if [ ! -f "$TARBALL" ]; then + echo "ERROR: npm pack produced no tarball" + exit 1 +fi + +INSTALL_DIR=$(mktemp -d) +trap 'rm -rf "$INSTALL_DIR" "$TARBALL"' EXIT + +echo "==> Tarball: $TARBALL_NAME ($(du -h "$TARBALL" | cut -f1) compressed)" + +# --- Check critical files using tar listing dumped to a file --- +# (avoids SIGPIPE issues with tar | grep on Linux) +TAR_LIST=$(mktemp) +tar tzf "$TARBALL" > "$TAR_LIST" 2>/dev/null + +MISSING=0 +for required in dist/loader.js packages/pi-coding-agent/dist/index.js scripts/link-workspace-packages.cjs; do + if ! grep -q "package/${required}" "$TAR_LIST"; then + echo " MISSING: $required" + MISSING=1 + fi +done +rm -f "$TAR_LIST" + +if [ "$MISSING" = "1" ]; then + echo "ERROR: Critical files missing from tarball." + exit 1 +fi +echo " Critical files present." + +# --- Install test --- +echo "==> Testing install in isolated directory..." +cd "$INSTALL_DIR" +npm init -y > /dev/null 2>&1 + +if npm install "$TARBALL" 2>&1; then + echo "==> Install succeeded." +else + echo "" + echo "ERROR: npm install of tarball failed." + exit 1 +fi + +echo "" +echo "Package is installable. Safe to publish."