fix(ux): deduplicate install messages, add newlines to SSH polling, clarify completion messages (#2900)

- Suppress stdout+stderr from `claude install --force` to prevent duplicate
  "successfully installed" messages (was printed up to 4x)
- Make logStepInline fall back to newline-separated output when stderr is not
  a TTY, so SSH port polling status is readable in piped/captured contexts
- Consolidate post-install completion messages into a single clear milestone:
  "Agent setup complete -- {agent} is ready on {cloud}"
- Bump CLI version to 0.25.16

Fixes #2899

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-23 01:26:34 -07:00 committed by GitHub
parent a96522829b
commit 7aba20e327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 20 additions and 13 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.25.15",
"version": "0.25.16",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -85,17 +85,19 @@ describe("logging functions", () => {
expect(stderrOutput.join("")).toContain("test step");
});
it("logStepInline writes without newline", () => {
it("logStepInline writes message (newline-terminated in non-TTY)", () => {
logStepInline("inline msg");
const output = stderrOutput.join("");
expect(output).toContain("inline msg");
expect(output).not.toEndWith("\n");
// In non-TTY (test environment), output ends with newline instead of \r overwrite
expect(output).toEndWith("\n");
});
it("logStepDone clears the line", () => {
it("logStepDone is no-op in non-TTY", () => {
logStepDone();
const output = stderrOutput.join("");
expect(output).toContain("\r");
// In non-TTY (test environment), logStepDone writes nothing
expect(output).toBe("");
});
it("logDebug only outputs when SPAWN_DEBUG=1", () => {

View file

@ -102,7 +102,7 @@ async function installClaudeCode(runner: CloudRunner): Promise<void> {
const claudePath = "$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$HOME/.n/bin";
const pathSetup = `for rc in ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc; do grep -q '.claude/local/bin' "$rc" 2>/dev/null || printf '\\n# Claude Code PATH\\nexport PATH="$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"\\n' >> "$rc"; done`;
const finalize = `claude install --force 2>/dev/null || true; ${pathSetup}`;
const finalize = `claude install --force >/dev/null 2>&1 || true; ${pathSetup}`;
const script = [
`export PATH="${claudePath}:$PATH"`,
@ -125,7 +125,7 @@ async function installClaudeCode(runner: CloudRunner): Promise<void> {
logError("Claude Code installation failed");
throw new Error("Claude Code install failed");
}
logInfo("Claude Code installed");
logInfo("Claude Code agent installed successfully");
}
async function setupClaudeCodeConfig(runner: CloudRunner, apiKey: string): Promise<void> {

View file

@ -532,9 +532,7 @@ async function postInstall(
}
// Launch agent
logInfo(`${agent.name} is ready`);
process.stderr.write("\n");
logInfo(`${cloud.cloudLabel} setup completed successfully!`);
logInfo(`Agent setup complete — ${agent.name} is ready on ${cloud.cloudLabel}`);
process.stderr.write("\n");
const launchCmd = agent.launchCmd();

View file

@ -38,14 +38,21 @@ export function logStep(msg: string): void {
process.stderr.write(`${CYAN}${msg}${NC}\n`);
}
/** Overwrite the current line with a status message (no newline). Call logStepDone() when finished. */
/** Overwrite the current line with a status message (no newline). Call logStepDone() when finished.
* Falls back to newline-separated output when stderr is not a TTY (e.g., piped or captured). */
export function logStepInline(msg: string): void {
process.stderr.write(`\r${CYAN}${msg}${NC}\x1b[K`);
if (process.stderr.isTTY) {
process.stderr.write(`\r${CYAN}${msg}${NC}\x1b[K`);
} else {
process.stderr.write(`${CYAN}${msg}${NC}\n`);
}
}
/** End an inline status line by moving to the next line. */
export function logStepDone(): void {
process.stderr.write("\r\x1b[K");
if (process.stderr.isTTY) {
process.stderr.write("\r\x1b[K");
}
}
/** Prompt for a line of user input. Throws if non-interactive.