refactor: deduplicate help text, cross-platform validate-pack, fix dev.js

- Extract duplicated help text from loader.ts and cli.ts into shared
  help-text.ts module (single source of truth)
- Convert validate-pack.sh to Node.js for Windows compatibility
- Fix dev.js using unnecessary npx for tsc (it's a devDependency,
  use node_modules/.bin/tsc directly)
This commit is contained in:
Jeremy McSpadden 2026-03-16 13:29:31 -05:00
parent 9c8a24042f
commit a79e953caa
6 changed files with 140 additions and 34 deletions

View file

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

View file

@ -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'
})
]

116
scripts/validate-pack.js Normal file
View file

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

View file

@ -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 <text|json|rpc> 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 <id> Override model (e.g. claude-opus-4-6)\n')
process.stdout.write(' --no-session Disable session persistence\n')
process.stdout.write(' --extension <path> Load additional extension\n')
process.stdout.write(' --tools <a,b,c> 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)

18
src/help-text.ts Normal file
View file

@ -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 <text|json|rpc> 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 <id> Override model (e.g. claude-opus-4-6)\n')
process.stdout.write(' --no-session Disable session persistence\n')
process.stdout.write(' --extension <path> Load additional extension\n')
process.stdout.write(' --tools <a,b,c> 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')
}

View file

@ -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 <text|json|rpc> 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 <id> Override model (e.g. claude-opus-4-6)\n')
process.stdout.write(' --no-session Disable session persistence\n')
process.stdout.write(' --extension <path> Load additional extension\n')
process.stdout.write(' --tools <a,b,c> 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)
}