diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index 47f9ef8a2..fa34ec94d 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -253,3 +253,20 @@ jobs: done echo "::error::Smoke test failed — gsd-pi@${VERSION} not installable" exit 1 + + - name: Verify dist-tag after publish + if: steps.version-check.outputs.is_prerelease == 'false' + run: | + VERSION=$(node -p "require('./package.json').version") + echo "Verifying npm dist-tag 'latest' points to ${VERSION}..." + for attempt in $(seq 1 10); do + LATEST=$(npm view gsd-pi dist-tags.latest 2>/dev/null || echo "") + if [ "${LATEST}" = "${VERSION}" ]; then + echo " ✓ npm dist-tags.latest = ${VERSION}" + exit 0 + fi + echo " Attempt ${attempt}/10: latest=${LATEST}, expected=${VERSION}, retrying in 15s..." + sleep 15 + done + echo "::error::dist-tags.latest is '${LATEST}' but expected '${VERSION}' — run: npm dist-tag add gsd-pi@${VERSION} latest" + exit 1 diff --git a/scripts/link-workspace-packages.cjs b/scripts/link-workspace-packages.cjs index 43ee66a83..f1faf9875 100644 --- a/scripts/link-workspace-packages.cjs +++ b/scripts/link-workspace-packages.cjs @@ -10,8 +10,12 @@ * to resolve. This script bridges the gap. * * Runs as part of postinstall (before any ESM code that imports @gsd/*). + * + * On Windows without Developer Mode or administrator rights, creating symlinks + * (even NTFS junctions) can fail with EPERM. In that case we fall back to + * cpSync (directory copy) which works universally. */ -const { existsSync, mkdirSync, symlinkSync, lstatSync, readlinkSync, unlinkSync, readdirSync } = require('fs') +const { existsSync, mkdirSync, symlinkSync, cpSync, lstatSync, readlinkSync, unlinkSync } = require('fs') const { resolve, join } = require('path') const root = resolve(__dirname, '..') @@ -33,6 +37,7 @@ if (!existsSync(nodeModulesGsd)) { } let linked = 0 +let copied = 0 for (const [dir, name] of Object.entries(packageMap)) { const source = join(packagesDir, dir) const target = join(nodeModulesGsd, name) @@ -50,21 +55,32 @@ for (const [dir, name] of Object.entries(packageMap)) { } unlinkSync(target) // Wrong target, relink } else { - continue // Real directory (e.g., from bundleDependencies), don't touch + continue // Real directory (e.g., copied or from bundleDependencies), don't touch } } catch { continue } } + let symlinkOk = false try { symlinkSync(source, target, 'junction') // junction works on Windows too + symlinkOk = true linked++ } catch { - // Non-fatal — may fail in read-only environments + // Symlink failed — common on Windows without Developer Mode or admin rights. + // Fall back to a directory copy so the package is still resolvable. + } + + if (!symlinkOk) { + try { + cpSync(source, target, { recursive: true }) + copied++ + } catch { + // Non-fatal — loader.ts will emit a clearer error if resolution still fails + } } } -if (linked > 0) { - process.stderr.write(` Linked ${linked} workspace packages\n`) -} +if (linked > 0) process.stderr.write(` Linked ${linked} workspace package${linked !== 1 ? 's' : ''}\n`) +if (copied > 0) process.stderr.write(` Copied ${copied} workspace package${copied !== 1 ? 's' : ''} (symlinks unavailable)\n`) diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js index 71a2e6754..d89fb9f34 100644 --- a/scripts/validate-pack.js +++ b/scripts/validate-pack.js @@ -103,6 +103,54 @@ try { process.exit(1); } + // --- Verify @gsd/* packages resolved correctly post-install --- + // This catches the Windows-style failure where symlinkSync fails silently and + // node_modules/@gsd/ is never populated, causing ERR_MODULE_NOT_FOUND at runtime. + console.log('==> Verifying @gsd/* workspace package resolution...'); + const installedRoot = join(installDir, 'node_modules', 'gsd-pi'); + const criticalPkgs = ['pi-coding-agent']; + let resolutionFailed = false; + for (const pkg of criticalPkgs) { + const pkgPath = join(installedRoot, 'node_modules', '@gsd', pkg); + const fallbackPath = join(installedRoot, 'packages', pkg); + if (!existsSync(pkgPath)) { + if (existsSync(fallbackPath)) { + console.log(` MISSING symlink/copy: node_modules/@gsd/${pkg} (packages/${pkg} exists — postinstall may not have run)`); + } else { + console.log(` MISSING: node_modules/@gsd/${pkg} (packages/${pkg} also absent — package is broken)`); + } + resolutionFailed = true; + } + } + if (resolutionFailed) { + console.log('ERROR: @gsd/* packages are not resolvable after install.'); + console.log(' This will cause ERR_MODULE_NOT_FOUND on first run (especially on Windows).'); + process.exit(1); + } + console.log(' @gsd/* packages are resolvable.'); + + // --- Run the binary to confirm end-to-end resolution --- + console.log('==> Running installed binary (gsd -v)...'); + const loaderPath = join(installedRoot, 'dist', 'loader.js'); + try { + const versionOutput = execSync(`node "${loaderPath}" -v`, { + cwd: installDir, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 15000, + }).trim(); + console.log(` gsd -v => ${versionOutput}`); + if (!versionOutput.match(/^\d+\.\d+\.\d+/)) { + console.log('ERROR: gsd -v returned unexpected output (expected a version string).'); + process.exit(1); + } + } catch (err) { + console.log('ERROR: Running gsd -v failed after install.'); + if (err.stdout) console.log(err.stdout); + if (err.stderr) console.log(err.stderr); + process.exit(1); + } + console.log(''); console.log('Package is installable. Safe to publish.'); process.exit(0); diff --git a/src/loader.ts b/src/loader.ts index 9d6b4ca50..42149656c 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -3,7 +3,7 @@ // Copyright (c) 2026 Jeremy McSpadden import { fileURLToPath } from 'url' import { dirname, resolve, join, delimiter } from 'path' -import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync } from 'fs' +import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync, cpSync } from 'fs' // Fast-path: handle --version/-v and --help/-h before importing any heavy // dependencies. This avoids loading the entire pi-coding-agent barrel import @@ -151,8 +151,12 @@ if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy setGlobalDispatcher(new EnvHttpProxyAgent()) } -// Ensure workspace packages are linked before importing cli.js (which imports @gsd/*). +// Ensure workspace packages are linked (or copied on Windows) before importing +// cli.js (which imports @gsd/*). // npm postinstall handles this normally, but npx --ignore-scripts skips postinstall. +// On Windows without Developer Mode or admin rights, symlinkSync will throw even for +// 'junction' type — so we fall back to cpSync (a full directory copy) which works +// everywhere without elevated permissions. const gsdScopeDir = join(gsdNodeModules, '@gsd') const packagesDir = join(gsdRoot, 'packages') const wsPackages = ['native', 'pi-agent-core', 'pi-ai', 'pi-coding-agent', 'pi-tui'] @@ -161,11 +165,37 @@ try { for (const pkg of wsPackages) { const target = join(gsdScopeDir, pkg) const source = join(packagesDir, pkg) - if (existsSync(source) && !existsSync(target)) { - try { symlinkSync(source, target, 'junction') } catch { /* non-fatal */ } + if (!existsSync(source) || existsSync(target)) continue + try { + symlinkSync(source, target, 'junction') + } catch { + // Symlink failed (common on Windows without Developer Mode / admin). + // Fall back to a directory copy — slower on first run but universally works. + try { cpSync(source, target, { recursive: true }) } catch { /* non-fatal */ } } } } catch { /* non-fatal */ } +// Validate critical workspace packages are resolvable. If still missing after the +// symlink+copy attempts, emit a clear diagnostic instead of a cryptic +// ERR_MODULE_NOT_FOUND from deep inside cli.js. +const criticalPackages = ['pi-coding-agent'] +const missingPackages = criticalPackages.filter(pkg => !existsSync(join(gsdScopeDir, pkg))) +if (missingPackages.length > 0) { + const missing = missingPackages.map(p => `@gsd/${p}`).join(', ') + process.stderr.write( + `\nError: GSD installation is broken — missing packages: ${missing}\n\n` + + `This is usually caused by one of:\n` + + ` • An outdated version installed from npm (run: npm install -g gsd-pi@latest)\n` + + ` • The packages/ directory was excluded from the installed tarball\n` + + ` • A filesystem error prevented linking or copying the workspace packages\n\n` + + `Fix it by reinstalling:\n\n` + + ` npm install -g gsd-pi@latest\n\n` + + `If the issue persists, please open an issue at:\n` + + ` https://github.com/gsd-build/gsd-2/issues\n` + ) + process.exit(1) +} + // Dynamic import defers ESM evaluation — config.js will see PI_PACKAGE_DIR above await import('./cli.js')