feat(setup): ping agent before chat, detect stale service, auto-install Claude
Round-trip confirmation before first chat. After cli-agent wires up the Terminal Agent, send `chat ping` through the CLI socket under a spinner with 30s timeout (shared helper in setup/lib/agent-ping.ts, also used by verify). Only after a real reply do we show "Your assistant is ready." and enter the chat loop. Ping failures surface a targeted note (socket_error vs no_reply) and skip the prompt — so users never type into the void. Checkout-mismatch detection. verify resolves the running service PID's script path via `ps -p <pid> -o command=` and compares to projectRoot. If the service is running from a sibling clone (common for developers with multiple checkouts), SERVICE comes back as running_other_checkout instead of running, AGENT_PING is skipped, and the failure note tells the user exactly which bootout + bootstrap pair to run. Native Claude Code install on demand. Only the subscription auth path needs `claude`; the paste-token and paste-API-key paths don't. So register-claude-token.sh now runs setup/install-claude.sh when `claude` is missing (curl -fsSL https://claude.ai/install.sh | bash), then prepends ~/.local/bin to PATH in-process so the rest of the script can see the fresh binary. Gutter-safe wrapping. wrapForGutter + dimWrap in lib/theme.ts hard-wrap text to `process.stdout.columns - gutter` on word boundaries, measuring visible length (ANSI-stripped). dimWrap applies the dim envelope per line because clack resets styling at each line break when rendering multi-line log content — a single outer dim() only colors the first line. Applied to the long "why" notes before container + onecli, the channel-skip info, the ping-failure note, and the checkout-mismatch remediation. Wordmark anchoring. printIntro always includes the NanoClaw wordmark in the clack intro line, whether or not nanoclaw.sh already printed one in bash. Worth ~1 line of redundancy so the brand stays visible at the top of the clack session after bootstrap output scrolls out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
setup/lib/agent-ping.ts
Normal file
50
setup/lib/agent-ping.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Round-trip check against the CLI Unix socket.
|
||||
*
|
||||
* Shared by `setup/verify.ts` (end-of-run health check) and `setup/auto.ts`
|
||||
* (confirm the freshly-wired agent actually responds before prompting the
|
||||
* user to chat with it).
|
||||
*
|
||||
* Exit-code contract follows `scripts/chat.ts`:
|
||||
* 0 → got a reply on stdout
|
||||
* 2 → socket unreachable (service not running or wrong checkout)
|
||||
* 3 → no reply before chat.ts's own 120s hard stop
|
||||
* This wrapper also guards with its own timeout in case chat.ts hangs.
|
||||
*/
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
export type PingResult = 'ok' | 'no_reply' | 'socket_error';
|
||||
|
||||
export function pingCliAgent(timeoutMs = 30_000): Promise<PingResult> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('pnpm', ['run', 'chat', 'ping'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = '';
|
||||
let settled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
child.kill('SIGKILL');
|
||||
resolve('no_reply');
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
stdout += chunk.toString('utf-8');
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (code === 2) resolve('socket_error');
|
||||
else if (code === 0 && stdout.trim().length > 0) resolve('ok');
|
||||
else resolve('no_reply');
|
||||
});
|
||||
child.on('error', () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve('socket_error');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -37,3 +37,66 @@ export function brandChip(s: string): string {
|
||||
}
|
||||
return k.bgCyan(k.black(k.bold(s)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text so it fits inside clack's gutter without the terminal's soft
|
||||
* wrap breaking the `│ …` bar on long lines. Works on a single string with
|
||||
* embedded `\n`s; each logical line is wrapped independently.
|
||||
*
|
||||
* The `gutter` argument is the total horizontal overhead clack adds for
|
||||
* the component the text lives in (e.g. 4 for `p.log.*`'s `│ ` prefix;
|
||||
* 6-ish for `p.note`'s box). Caller picks it; we just subtract from
|
||||
* `process.stdout.columns` and hard-wrap at word boundaries.
|
||||
*/
|
||||
export function wrapForGutter(text: string, gutter: number): string {
|
||||
const cols = process.stdout.columns ?? 80;
|
||||
const width = Math.max(30, cols - gutter);
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => wrapLine(line, width))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap + dim together. Needed instead of `k.dim(wrapForGutter(...))`
|
||||
* because clack resets styling at each line break when rendering
|
||||
* multi-line log content — a single outer dim envelope only colors the
|
||||
* first line. Applying dim per-line gives each wrapped row its own
|
||||
* `\x1b[2m…\x1b[0m` envelope so the whole block reads as one block.
|
||||
*/
|
||||
export function dimWrap(text: string, gutter: number): string {
|
||||
return wrapForGutter(text, gutter)
|
||||
.split('\n')
|
||||
.map((line) => k.dim(line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
function visibleLength(s: string): number {
|
||||
return s.replace(ANSI_RE, '').length;
|
||||
}
|
||||
|
||||
function wrapLine(line: string, width: number): string {
|
||||
if (visibleLength(line) <= width) return line;
|
||||
const words = line.split(' ');
|
||||
const rows: string[] = [];
|
||||
let cur = '';
|
||||
let curLen = 0;
|
||||
for (const word of words) {
|
||||
const wLen = visibleLength(word);
|
||||
if (curLen === 0) {
|
||||
cur = word;
|
||||
curLen = wLen;
|
||||
} else if (curLen + 1 + wLen <= width) {
|
||||
cur += ' ' + word;
|
||||
curLen += 1 + wLen;
|
||||
} else {
|
||||
rows.push(cur);
|
||||
cur = word;
|
||||
curLen = wLen;
|
||||
}
|
||||
}
|
||||
if (cur) rows.push(cur);
|
||||
return rows.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user