fix: broken npm install — remove bundleDependencies, use postinstall symlinks (#369)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-14 10:04:12 -06:00 committed by GitHub
parent c3ceb077d9
commit 098ed35a50
16 changed files with 188 additions and 57 deletions

View file

@ -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:

View file

@ -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

8
.npmignore Normal file
View file

@ -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).

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

25
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -8,7 +8,5 @@
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@gsd/pi-ai": "*"
}
"dependencies": {}
}

View file

@ -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",

View file

@ -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",

View file

@ -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`)
}

83
scripts/validate-pack.sh Executable file
View file

@ -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."