diff --git a/package-lock.json b/package-lock.json index 144b23e41..fcdbe23ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@clack/prompts": "^1.1.0", "@mariozechner/pi-coding-agent": "^0.57.1", + "picocolors": "^1.1.1", "playwright": "^1.58.2" }, "bin": { @@ -777,6 +779,25 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@clack/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", + "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==", + "license": "MIT", + "dependencies": { + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz", + "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.1.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@google/genai": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.44.0.tgz", @@ -3557,6 +3578,12 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3795,6 +3822,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", diff --git a/package.json b/package.json index 860e931ff..dab2be015 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "prepublishOnly": "npm run sync-pkg-version && npm run build" }, "dependencies": { + "@clack/prompts": "^1.1.0", "@mariozechner/pi-coding-agent": "^0.57.1", + "picocolors": "^1.1.1", "playwright": "^1.58.2" }, "devDependencies": { diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 47d8af090..a9cb33f6e 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,18 +1,38 @@ #!/usr/bin/env node -import { execSync } from 'child_process' + +import { exec as execCb } from 'child_process' import { createRequire } from 'module' import os from 'os' -import { fileURLToPath } from 'url' import { dirname, resolve } from 'path' +import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) const require = createRequire(import.meta.url) const pkg = require(resolve(__dirname, '..', 'package.json')) +const cwd = resolve(__dirname, '..') -// Colors +// --------------------------------------------------------------------------- +// Async exec helper — captures stdout+stderr, never inherits to terminal +// --------------------------------------------------------------------------- +function run(cmd, options = {}) { + return new Promise((resolve) => { + execCb(cmd, { cwd, ...options }, (error, stdout, stderr) => { + resolve({ ok: !error, stdout, stderr, error }) + }) + }) +} + +// --------------------------------------------------------------------------- +// Redirect stdout → stderr so npm always shows postinstall output. +// npm ≥7 suppresses stdout from lifecycle scripts by default; stderr is +// always forwarded. Clack writes to process.stdout, so we reroute it. +// --------------------------------------------------------------------------- +process.stdout.write = process.stderr.write.bind(process.stderr) + +// --------------------------------------------------------------------------- +// ASCII banner — printed before clack UI for brand recognition +// --------------------------------------------------------------------------- const cyan = '\x1b[36m' -const green = '\x1b[32m' -const yellow = '\x1b[33m' const dim = '\x1b[2m' const reset = '\x1b[0m' @@ -27,58 +47,81 @@ const banner = ' ╚═════╝ ╚══════╝╚═════╝ ' + reset + '\n' + '\n' + - ` Get Shit Done ${dim}v${pkg.version}${reset}\n` + - ` A standalone coding agent that plans, executes, and ships.\n` + - '\n' + - ` ${green}✓${reset} Installed successfully\n` + - ` ${dim}Run ${reset}${cyan}gsd${reset}${dim} to get started.${reset}\n` + ` Get Shit Done ${dim}v${pkg.version}${reset}\n` -function run(command, options = {}) { - return execSync(command, { - cwd: resolve(__dirname, '..'), - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - ...options, - }) -} +// --------------------------------------------------------------------------- +// Main — wrapped in async IIFE, with graceful fallback if clack fails +// --------------------------------------------------------------------------- +;(async () => { + process.stderr.write(banner) -function printCaptured(output) { - if (output) process.stderr.write(output) -} + let p, pc -process.stderr.write(banner) - -// Apply patches to upstream dependencies (non-fatal) -try { - const output = run('npx patch-package') - printCaptured(output) - process.stderr.write(`\n ${green}✓${reset} Patches applied\n`) -} catch (error) { - printCaptured(error.stdout) - printCaptured(error.stderr) - process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`) -} - -// Install Playwright chromium for browser tools (non-fatal). -// We intentionally avoid --with-deps here because install scripts should not -// block on interactive sudo prompts. Playwright validates host requirements -// after download; if Linux libs are missing, suggest the explicit follow-up. -try { - const output = run('npx playwright install chromium') - printCaptured(output) - process.stderr.write(`\n ${green}✓${reset} Browser tools ready\n\n`) -} catch (error) { - const output = `${error.stdout ?? ''}${error.stderr ?? ''}` - printCaptured(output) - - if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) { - process.stderr.write( - `\n ${yellow}⚠${reset} Browser downloaded, but Linux system dependencies are missing.\n` + - ` ${dim}Run ${reset}${cyan}sudo npx playwright install-deps chromium${reset}${dim} to finish setup.${reset}\n\n` - ) - } else { - process.stderr.write( - `\n ${yellow}⚠${reset} Browser tools unavailable — run ${cyan}npx playwright install chromium${reset} to enable\n\n` - ) + try { + p = await import('@clack/prompts') + pc = (await import('picocolors')).default + } catch { + // Clack or picocolors unavailable — fall back to minimal output + process.stderr.write(` Run gsd to get started.\n\n`) + await run('npx patch-package') + await run('npx playwright install chromium') + return } -} + + // --- Branded intro ------------------------------------------------------- + p.intro('Setup') + + const results = [] + const s = p.spinner() + + // --- Step 1: Apply patches ----------------------------------------------- + s.start('Applying patches…') + const patchResult = await run('npx patch-package') + if (patchResult.ok) { + s.stop('Patches applied') + results.push({ label: 'Patches applied', ok: true }) + } else { + s.stop(pc.yellow('Patches — skipped (non-fatal)')) + results.push({ + label: 'Patches skipped — run ' + pc.cyan('npx patch-package') + ' manually', + ok: false, + }) + } + + // --- Step 2: Playwright browser ------------------------------------------ + // Avoid --with-deps: install scripts should not block on interactive sudo + // prompts. If Linux libs are missing, suggest the explicit follow-up. + s.start('Setting up browser tools…') + const pwResult = await run('npx playwright install chromium') + if (pwResult.ok) { + s.stop('Browser tools ready') + results.push({ label: 'Browser tools ready', ok: true }) + } else { + const output = `${pwResult.stdout ?? ''}${pwResult.stderr ?? ''}` + if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) { + s.stop(pc.yellow('Browser downloaded, missing Linux deps')) + results.push({ + label: 'Run ' + pc.cyan('sudo npx playwright install-deps chromium') + ' to finish setup', + ok: false, + }) + } else { + s.stop(pc.yellow('Browser tools — skipped (non-fatal)')) + results.push({ + label: 'Browser tools unavailable — run ' + pc.cyan('npx playwright install chromium'), + ok: false, + }) + } + } + + // --- Summary note -------------------------------------------------------- + const lines = results.map( + (r) => (r.ok ? pc.green('✓') : pc.yellow('⚠')) + ' ' + r.label + ) + lines.push('') + lines.push('Run ' + pc.cyan('gsd') + ' to get started.') + + p.note(lines.join('\n'), 'Installed') + + // --- Outro --------------------------------------------------------------- + p.outro(pc.green('Done!')) +})()