feat: branded postinstall with @clack/prompts (#115)
* feat: branded postinstall with @clack/prompts Replace raw ANSI ASCII art dump with structured, branded installer flow using @clack/prompts and picocolors: - Branded intro header with product name and version - Animated spinners during patch and Playwright install steps - Subprocess output captured (no more raw npm/Playwright noise) - Boxed summary note with status indicators (✓/⚠) - Clean outro with next-step instructions - Graceful fallback to minimal output if clack unavailable - All output routed to stderr for npm lifecycle visibility - Async subprocess execution (not execSync) so spinners animate * fix: restore ASCII banner alongside clack postinstall UI The branded ASCII art banner is a key differentiator. Keep it as the first thing users see, then follow with clack spinner steps for the setup progress. Fallback path also simplified since the banner already shows the version. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a292e1981
commit
46c88e6494
3 changed files with 134 additions and 56 deletions
33
package-lock.json
generated
33
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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!'))
|
||||
})()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue