diff --git a/package.json b/package.json index d65a2e23c..1b67d91f8 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "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", - "validate-pack": "bash scripts/validate-pack.sh", + "validate-pack": "node scripts/validate-pack.js", "typecheck:extensions": "tsc --noEmit --project tsconfig.extensions.json", "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 typecheck:extensions && npm run validate-pack" }, diff --git a/scripts/dev.js b/scripts/dev.js index dc87dce60..faf9a75d2 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -19,7 +19,7 @@ const procs = [ spawn('node', [resolve(__dirname, 'watch-resources.js')], { cwd: root, stdio: 'inherit' }), - spawn('npx', ['tsc', '--watch'], { + spawn(resolve(root, 'node_modules', '.bin', 'tsc'), ['--watch'], { cwd: root, stdio: 'inherit' }) ] diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js new file mode 100644 index 000000000..71a2e6754 --- /dev/null +++ b/scripts/validate-pack.js @@ -0,0 +1,116 @@ +// validate-pack.js — Verify the npm tarball is installable before publishing. +// +// Usage: npm run validate-pack (or node scripts/validate-pack.js) +// Exit 0 = safe to publish, Exit 1 = broken package. + +import { execSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT = resolve(__dirname, '..'); + +let tarball = null; +let installDir = null; + +try { + // --- Guard: workspace packages must not have @gsd/* cross-deps --- + console.log('==> Checking workspace packages for @gsd/* cross-deps...'); + const workspaces = ['native', 'pi-agent-core', 'pi-ai', 'pi-coding-agent', 'pi-tui']; + let crossFailed = false; + + for (const ws of workspaces) { + const pkgPath = join(ROOT, 'packages', ws, 'package.json'); + if (!existsSync(pkgPath)) continue; + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + const deps = Object.keys(pkg.dependencies || {}).filter(d => d.startsWith('@gsd/')); + if (deps.length) { + console.log(` LEAKED in ${ws}: ${deps.join(', ')}`); + crossFailed = true; + } + } + + if (crossFailed) { + console.log('ERROR: Workspace packages have @gsd/* cross-dependencies.'); + console.log(' These cause 404s when npm resolves them from the registry.'); + process.exit(1); + } + console.log(' No @gsd/* cross-dependencies.'); + + // --- Pack tarball --- + console.log('==> Packing tarball...'); + const packOutput = execSync('npm pack --ignore-scripts', { + cwd: ROOT, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + const tarballName = packOutput.trim().split('\n').pop(); + tarball = join(ROOT, tarballName); + + if (!existsSync(tarball)) { + console.log('ERROR: npm pack produced no tarball'); + process.exit(1); + } + + const stats = execSync(`du -h "${tarball}"`, { encoding: 'utf8' }).split('\t')[0].trim(); + console.log(`==> Tarball: ${tarballName} (${stats} compressed)`); + + // --- Check critical files using tar listing --- + console.log('==> Checking critical files...'); + const tarList = execSync(`tar tzf "${tarball}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }); + + const requiredFiles = [ + 'dist/loader.js', + 'packages/pi-coding-agent/dist/index.js', + 'scripts/link-workspace-packages.cjs', + ]; + + let missing = false; + for (const required of requiredFiles) { + if (!tarList.includes(`package/${required}`)) { + console.log(` MISSING: ${required}`); + missing = true; + } + } + + if (missing) { + console.log('ERROR: Critical files missing from tarball.'); + process.exit(1); + } + console.log(' Critical files present.'); + + // --- Install test --- + console.log('==> Testing install in isolated directory...'); + installDir = mkdtempSync(join(tmpdir(), 'validate-pack-')); + writeFileSync(join(installDir, 'package.json'), JSON.stringify({ name: 'test-install', version: '1.0.0', private: true }, null, 2)); + + try { + const installOutput = execSync(`npm install "${tarball}"`, { + cwd: installDir, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + console.log(installOutput); + console.log('==> Install succeeded.'); + } catch (err) { + console.log(''); + console.log('ERROR: npm install of tarball failed.'); + 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); +} finally { + if (installDir && existsSync(installDir)) { + rmSync(installDir, { recursive: true, force: true }); + } + if (tarball && existsSync(tarball)) { + rmSync(tarball, { force: true }); + } +} diff --git a/src/cli.ts b/src/cli.ts index 83f8b4de9..2d7b42f26 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,6 +19,7 @@ import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migrati import { shouldRunOnboarding, runOnboarding } from './onboarding.js' import chalk from 'chalk' import { checkForUpdates } from './update-check.js' +import { printHelp } from './help-text.js' // --------------------------------------------------------------------------- // Minimal CLI arg parser — detects print/subagent mode flags @@ -79,22 +80,7 @@ function parseCliArgs(argv: string[]): CliFlags { process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n') process.exit(0) } else if (arg === '--help' || arg === '-h') { - process.stdout.write(`GSD v${process.env.GSD_VERSION || '0.0.0'} — Get Shit Done\n\n`) - process.stdout.write('Usage: gsd [options] [message...]\n\n') - process.stdout.write('Options:\n') - process.stdout.write(' --mode Output mode (default: interactive)\n') - process.stdout.write(' --print, -p Single-shot print mode\n') - process.stdout.write(' --continue, -c Resume the most recent session\n') - process.stdout.write(' --model Override model (e.g. claude-opus-4-6)\n') - process.stdout.write(' --no-session Disable session persistence\n') - process.stdout.write(' --extension Load additional extension\n') - process.stdout.write(' --tools Restrict available tools\n') - process.stdout.write(' --list-models [search] List available models and exit\n') - process.stdout.write(' --version, -v Print version and exit\n') - process.stdout.write(' --help, -h Print this help and exit\n') - process.stdout.write('\nSubcommands:\n') - process.stdout.write(' config Re-run the setup wizard\n') - process.stdout.write(' update Update GSD to the latest version\n') + printHelp(process.env.GSD_VERSION || '0.0.0') process.exit(0) } else if (!arg.startsWith('--') && !arg.startsWith('-')) { flags.messages.push(arg) diff --git a/src/help-text.ts b/src/help-text.ts new file mode 100644 index 000000000..e35b652f2 --- /dev/null +++ b/src/help-text.ts @@ -0,0 +1,18 @@ +export function printHelp(version: string): void { + process.stdout.write(`GSD v${version} — Get Shit Done\n\n`) + process.stdout.write('Usage: gsd [options] [message...]\n\n') + process.stdout.write('Options:\n') + process.stdout.write(' --mode Output mode (default: interactive)\n') + process.stdout.write(' --print, -p Single-shot print mode\n') + process.stdout.write(' --continue, -c Resume the most recent session\n') + process.stdout.write(' --model Override model (e.g. claude-opus-4-6)\n') + process.stdout.write(' --no-session Disable session persistence\n') + process.stdout.write(' --extension Load additional extension\n') + process.stdout.write(' --tools Restrict available tools\n') + process.stdout.write(' --list-models [search] List available models and exit\n') + process.stdout.write(' --version, -v Print version and exit\n') + process.stdout.write(' --help, -h Print this help and exit\n') + process.stdout.write('\nSubcommands:\n') + process.stdout.write(' config Re-run the setup wizard\n') + process.stdout.write(' update Update GSD to the latest version\n') +} diff --git a/src/loader.ts b/src/loader.ts index 5bf3e5611..9d6b4ca50 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -28,22 +28,8 @@ if (firstArg === '--help' || firstArg === '-h') { const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8')) version = pkg.version || version } catch { /* ignore */ } - process.stdout.write(`GSD v${version} — Get Shit Done\n\n`) - process.stdout.write('Usage: gsd [options] [message...]\n\n') - process.stdout.write('Options:\n') - process.stdout.write(' --mode Output mode (default: interactive)\n') - process.stdout.write(' --print, -p Single-shot print mode\n') - process.stdout.write(' --continue, -c Resume the most recent session\n') - process.stdout.write(' --model Override model (e.g. claude-opus-4-6)\n') - process.stdout.write(' --no-session Disable session persistence\n') - process.stdout.write(' --extension Load additional extension\n') - process.stdout.write(' --tools Restrict available tools\n') - process.stdout.write(' --list-models [search] List available models and exit\n') - process.stdout.write(' --version, -v Print version and exit\n') - process.stdout.write(' --help, -h Print this help and exit\n') - process.stdout.write('\nSubcommands:\n') - process.stdout.write(' config Re-run the setup wizard\n') - process.stdout.write(' update Update GSD to the latest version\n') + const { printHelp } = await import('./help-text.js') + printHelp(version) process.exit(0) }