Skip to content

Commit 1568cc7

Browse files
tonychang04claude
andauthored
fix(setup): Railway-clean onboarding output — summarize skill install, don't stream it (#42)
* fix(install): symlink into an on-PATH dir so `insta` runs immediately Root cause of 'command not found: insta' after the one-liner: we installed only to ~/.insta/bin (never on PATH), so the recommended 'curl … | sh && insta project create' chain called insta in a shell whose PATH the piped script can't change. Railway avoids this by installing where PATH already looks (and sudo-escalating if needed). Now: after installing to ~/.insta/bin, symlink into a writable on-PATH dir (/usr/local/bin, else ~/.local/bin) so bare 'insta' resolves in the same shell with no reload; fall back to profile-append + an explicit this-shell export only when no such dir exists. Verified live: fresh install → 'command -v insta' resolves, v0.0.13, no manual step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GMbAe5K1RfwaAcge5inX7P * fix(setup): Railway-clean onboarding output — summarize skill install, don't stream it The agent one-liner (curl agents.instacloud.com | sh) shelled `insta setup agent` out to `npx skills add …` and streamed its clack UI verbatim: a frame-by-frame clone spinner, an 'Installing to all 73 agents' banner, a 70-line install-path box, and a third-party 'Security Risk Assessment' that flags our OWN first-party skill as 'Critical Risk'. Next to Railway's two clean ✓ lines it read noisy and alarming. Now we CAPTURE the tool's output and print one Railway-style summary line naming the well-known agents with the long tail rolled into '+N more': setting up coding-agent skills … (~20s) ✓ Agent skills — Universal (.agents), Claude Code, Windsurf, Factory Droid, Goose, Aider, +47 more every coding agent on this machine now knows InstaCloud (review skills before use …) On failure we still surface the real error (captured tail, minus the expected no-global-support noise) plus the manual command. Also drops a duplicate 'setting up coding-agent skills …' echo in install.sh. Tests cover the summarizer against the tool's real boxed output format; 75/75 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GMbAe5K1RfwaAcge5inX7P --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent fa5bdad commit 1568cc7

4 files changed

Lines changed: 122 additions & 38 deletions

File tree

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ fi
139139
# ---- agent setup (--agents) ----
140140
if [ "$AGENTS" = "1" ]; then
141141
echo
142-
echo "setting up coding-agent skills …"
142+
# `insta setup agent` prints its own "setting up coding-agent skills …" line + clean summary.
143143
if [ "$YES" = "1" ]; then
144144
"$INSTALL_DIR/$BIN" setup agent -y || echo "warn: agent setup failed — run: insta setup agent"
145145
else

src/commands/setup.ts

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,83 @@
88
import { spawn } from 'node:child_process'
99
import { info } from '../util.js'
1010

11-
// The skills tool reports agents that don't support user-global installs (Eve, PromptScript, …)
12-
// as red failures. That's expected reality, not a problem with THIS machine — showing red in the
13-
// first 30s of onboarding erodes trust (Railway's installer reads all-green for a reason).
14-
// Classify each output line: drop that expected noise, keep everything real.
11+
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
12+
// "Installing to all N agents" banner, a full N-line install-path box, and a third-party
13+
// "Security Risk Assessment" that flags our OWN first-party skill as "Critical Risk". Streamed
14+
// verbatim during onboarding that reads as noisy and alarming — the opposite of Railway's two
15+
// clean ✓ lines. So we CAPTURE its output and print our own one-line summary instead. This
16+
// classifier decides which captured lines are worth showing if the install FAILS (surface the
17+
// real error; drop the expected no-global-support noise).
1518
export function classifyInstallLine(line: string): 'keep' | 'skip' {
1619
if (/does not support global skill installation/.test(line)) return 'skip'
1720
if (/Failed to install \d+/.test(line)) return 'skip'
1821
return 'keep'
1922
}
2023

21-
export type Runner = (cmd: string, args: string[]) => Promise<{ ok: boolean }>
24+
// Map a skill-install target directory to a human agent name. `-a '*'` installs to every agent
25+
// dir the tool knows (~70+); we name the well-known ones (Railway-style) and roll the long tail
26+
// into "+N more" rather than dumping every path. Order = display priority.
27+
const AGENT_NAMES: Array<[RegExp, string]> = [
28+
[/\.agents\b/, 'Universal (.agents)'],
29+
[/\.claude\b/, 'Claude Code'],
30+
[/\.codex\b/, 'OpenAI Codex'],
31+
[/\.cursor\b/, 'Cursor'],
32+
[/opencode\b/, 'OpenCode'],
33+
[/copilot\b/, 'GitHub Copilot'],
34+
[/\.gemini\b/, 'Gemini CLI'],
35+
[/windsurf\b/, 'Windsurf'],
36+
[/\.factory\b/, 'Factory Droid'],
37+
[/goose\b/, 'Goose'],
38+
[/aider\b/, 'Aider'],
39+
[/\.continue\b/, 'Continue'],
40+
[/\.roo\b/, 'Roo'],
41+
[/kilocode\b/, 'Kilo Code'],
42+
[/\.qwen\b/, 'Qwen'],
43+
]
2244

45+
// Pull install-target paths out of the skills tool's summary lines ("→ ~/.claude/skills/insta")
46+
// and resolve the well-known ones to names. Returns the total install count + named agents.
47+
export function parseInstalledAgents(output: string): { count: number; names: string[] } {
48+
const paths = new Set<string>()
49+
for (const line of output.split('\n')) {
50+
const plain = line.replace(/\x1b\[[0-9;]*m/g, '')
51+
// The tool boxes each line ("│ → ~/.claude/skills/insta │"), so don't anchor to EOL.
52+
const m = plain.match(/\s*(\S+)\/skills\/[A-Za-z0-9_-]+/)
53+
if (m && m[1]) paths.add(m[1])
54+
}
55+
const names: string[] = []
56+
for (const [re, name] of AGENT_NAMES) {
57+
if ([...paths].some((p) => re.test(p))) names.push(name)
58+
}
59+
return { count: paths.size, names }
60+
}
61+
62+
// The Railway-style one-liner: a few named agents, the rest as "+N more".
63+
export function summarizeInstall(output: string): string {
64+
const { count, names } = parseInstalledAgents(output)
65+
if (count === 0) return '✓ insta skill installed for your coding agents'
66+
const shown = names.slice(0, 6)
67+
const more = count - shown.length
68+
const list = shown.length
69+
? shown.join(', ') + (more > 0 ? `, +${more} more` : '')
70+
: `${count} agent${count === 1 ? '' : 's'}`
71+
return `✓ Agent skills — ${list}`
72+
}
73+
74+
export type Runner = (cmd: string, args: string[]) => Promise<{ ok: boolean; output?: string }>
75+
76+
// Capture stdout+stderr silently (don't stream) so we can print our own clean summary. We keep
77+
// the child's stdin inherited in case the tool ever needs a TTY, but with -y it shouldn't.
2378
const defaultRunner: Runner = (cmd, args) =>
2479
new Promise((resolve) => {
25-
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta', FORCE_COLOR: '1' }
80+
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta', FORCE_COLOR: '0' }
2681
const p = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'pipe'], env })
27-
let skipped = 0
28-
const sift = (chunk: Buffer) => {
29-
for (const line of chunk.toString().split('\n')) {
30-
// strip ANSI before classifying; print the original line to preserve the tool's styling
31-
const plain = line.replace(/\x1b\[[0-9;]*m/g, '')
32-
if (classifyInstallLine(plain) === 'skip') { skipped++; continue }
33-
if (line.trim()) process.stdout.write(line + '\n')
34-
}
35-
}
36-
p.stdout?.on('data', sift)
37-
p.stderr?.on('data', sift)
38-
p.on('error', () => resolve({ ok: false }))
39-
p.on('close', (code) => {
40-
if (skipped) info(` (skipped ${skipped >= 3 ? 'some' : skipped} agent targets that don't support user-global installs — expected)`)
41-
resolve({ ok: code === 0 })
42-
})
82+
let output = ''
83+
const grab = (chunk: Buffer) => { output += chunk.toString() }
84+
p.stdout?.on('data', grab)
85+
p.stderr?.on('data', grab)
86+
p.on('error', () => resolve({ ok: false, output }))
87+
p.on('close', (code) => resolve({ ok: code === 0, output }))
4388
})
4489

4590
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
@@ -50,14 +95,21 @@ export async function setupAgent(opts: { yes?: boolean }, run: Runner = defaultR
5095
if (!opts.yes && !process.stdout.isTTY) {
5196
info('non-interactive shell — assuming -y')
5297
}
53-
info('installing the insta skill (user-global, all agents) …')
98+
info('setting up coding-agent skills … (~20s)')
5499
const res = await run('npx', SETUP_ARGS)
55100
if (!res.ok) {
56101
info(' skill install failed — install manually with:')
57102
info(' npx skills add InsForge/insta-skills -s insta -a "*" -g -y --copy')
103+
// Surface the REAL error: the captured tail, minus the expected no-global-support noise.
104+
const tail = (res.output ?? '')
105+
.split('\n')
106+
.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').trimEnd())
107+
.filter((l) => l.trim() && classifyInstallLine(l) === 'keep')
108+
.slice(-6)
109+
for (const l of tail) info(' ' + l)
58110
process.exitCode = 1
59111
return
60112
}
61-
info('done — agents on this machine now know InstaCloud.')
62-
info('next: `insta login --oauth github` (cloud) or run instad locally (insta-oss), then `insta project create <name>`.')
113+
info(summarizeInstall(res.output ?? ''))
114+
info(' every coding agent on this machine now knows InstaCloud (review skills before use — they run with full permissions).')
63115
}

test/setup-agent.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setupAgent, SETUP_ARGS } from '../src/commands/setup.js'
33

44
test('setup agent installs the insta skill user-globally for all agents', async () => {
55
const runs: string[][] = []
6-
await setupAgent({ yes: true }, async (_cmd, args) => { runs.push(args); return { ok: true } })
6+
await setupAgent({ yes: true }, async (_cmd, args) => { runs.push(args); return { ok: true, output: '' } })
77
expect(runs).toHaveLength(1)
88
expect(runs[0]).toEqual(SETUP_ARGS)
99
expect(SETUP_ARGS).toContain('-g') // user-level, not per-project
@@ -14,7 +14,7 @@ test('setup agent installs the insta skill user-globally for all agents', async
1414

1515
test('failed install sets exit code and prints the manual fallback', async () => {
1616
const prev = process.exitCode
17-
await setupAgent({ yes: true }, async () => ({ ok: false }))
17+
await setupAgent({ yes: true }, async () => ({ ok: false, output: '' }))
1818
expect(process.exitCode).toBe(1)
1919
process.exitCode = prev
2020
})

test/setup-output.test.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
1-
// Railway-lesson: the install moment must read all-green. The skills tool prints a red
2-
// "Failed to install N" for agents that don't support global installs (Eve, PromptScript) —
3-
// expected noise, not failure. Filter classifies each line: keep real output, drop the
4-
// expected-noise lines, and summarize what was skipped.
1+
// Railway-lesson: the install moment must read clean. We CAPTURE the skills tool's clack UI
2+
// (clone spinner, "Installing to all N agents", the full path box, the third-party security
3+
// box) and print our own one-line summary instead of streaming it. These tests pin the summary
4+
// and the error-path classifier (which still decides what to surface if the install FAILS).
55
import { test, expect } from 'vitest'
6-
import { classifyInstallLine } from '../src/commands/setup.js'
6+
import { classifyInstallLine, parseInstalledAgents, summarizeInstall } from '../src/commands/setup.js'
77

8-
test('drops expected no-global-support failures and the red banner', () => {
8+
// Representative slice of what `skills add … -a '*'` actually prints on success — including the
9+
// box borders the tool wraps each path in (the exact format the parser must tolerate).
10+
const SAMPLE = `
11+
● Installing to all 73 agents
12+
◇ Installed 1 skill ────────────────────╮
13+
│ │
14+
│ ✓ insta (copied) │
15+
│ → ~/.agents/skills/insta │
16+
│ → ~/.claude/skills/insta │
17+
│ → ~/.cursor/skills/insta │
18+
│ → ~/.config/goose/skills/insta │
19+
│ → ~/.codeium/windsurf/skills/insta │
20+
│ → ~/.aider-desk/skills/insta │
21+
│ → ~/.kilocode/skills/insta │
22+
├────────────────────────────────────────╯
23+
`
24+
25+
test('summary names the well-known agents and rolls the rest into +N more', () => {
26+
const { count, names } = parseInstalledAgents(SAMPLE)
27+
expect(count).toBe(7)
28+
expect(names).toContain('Claude Code')
29+
expect(names).toContain('Cursor')
30+
expect(names).toContain('Universal (.agents)')
31+
const line = summarizeInstall(SAMPLE)
32+
expect(line.startsWith('✓ Agent skills —')).toBe(true)
33+
expect(line).toContain('Claude Code')
34+
// never dumps raw paths or the scary "73 agents" banner
35+
expect(line).not.toContain('skills/insta')
36+
expect(line).not.toContain('73 agents')
37+
})
38+
39+
test('summary degrades gracefully when no paths are parseable', () => {
40+
expect(summarizeInstall('some unexpected output')).toBe('✓ insta skill installed for your coding agents')
41+
})
42+
43+
test('error-path classifier drops expected no-global-support noise', () => {
944
expect(classifyInstallLine('✗ insta → Eve: Eve does not support global skill installation')).toBe('skip')
10-
expect(classifyInstallLine('✗ insta → PromptScript: PromptScript does not support global skill installation')).toBe('skip')
1145
expect(classifyInstallLine('■ Failed to install 2')).toBe('skip')
1246
})
1347

14-
test('keeps real progress and REAL failures', () => {
15-
expect(classifyInstallLine('→ ~/.claude/skills/insta')).toBe('keep')
16-
expect(classifyInstallLine('✓ Repository cloned')).toBe('keep')
48+
test('error-path classifier keeps REAL failures', () => {
1749
expect(classifyInstallLine('✗ insta → Claude Code: EACCES permission denied')).toBe('keep')
1850
})

0 commit comments

Comments
 (0)