Skip to content

Commit 4428295

Browse files
alb-rlcursoragent
andauthored
fix: clean stdout for --config-only when appending SSH config (#192)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary `rli devbox ssh <id> --config-only >> ~/.ssh/config` was polluting the config file because: 1. **Readiness messages used stdout** — `waitForReady` logged via `console.log`, so lines like `Devbox … is ready!` were captured by the redirect. 2. **Text output wrapped the block** — `output({ config }, …)` rendered as `config: …` instead of a raw `Host` stanza. ## Changes - Route all `waitForReady` status lines to **stderr** (`console.error`). - For `--config-only` with default **text** output, print the generated SSH config with **`console.log(config)`** only (no `config:` prefix). JSON/YAML (`-o json|yaml`) still use the structured `output()` helper. ## Testing - `pnpm run build` - `pnpm test` <!-- CURSOR_AGENT_PR_BODY_END --> [Slack Thread](https://runloophq.slack.com/archives/C0AH4DJ5HB8/p1775174248460909?thread_ts=1775174248.460909&cid=C0AH4DJ5HB8) <div><a href="https://cursor.com/agents/bc-c0ca94a0-3e42-5fb6-9e97-56251263a648"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-c0ca94a0-3e42-5fb6-9e97-56251263a648"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 0903d89 commit 4428295

3 files changed

Lines changed: 43 additions & 13 deletions

File tree

src/commands/devbox/ssh.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { spawn } from "child_process";
66
import { getClient } from "../../utils/client.js";
7+
import { cliStatus } from "../../utils/cliStatus.js";
78
import { output, outputError } from "../../utils/output.js";
89
import { processUtils } from "../../utils/processUtils.js";
910
import {
@@ -36,11 +37,14 @@ export async function sshDevbox(devboxId: string, options: SSHOptions = {}) {
3637

3738
// Wait for devbox to be ready unless --no-wait is specified
3839
if (!options.noWait) {
39-
console.error(`Waiting for devbox ${devboxId} to be ready...`);
40+
if (!options.configOnly) {
41+
cliStatus(`Waiting for devbox ${devboxId} to be ready...`);
42+
}
4043
const isReady = await waitForReady(
4144
devboxId,
4245
options.timeout || 180,
4346
options.pollInterval || 3,
47+
{ quiet: options.configOnly },
4448
);
4549
if (!isReady) {
4650
outputError(`Devbox ${devboxId} is not ready. Please try again later.`);
@@ -64,7 +68,12 @@ export async function sshDevbox(devboxId: string, options: SSHOptions = {}) {
6468
sshInfo!.keyfilePath,
6569
sshInfo!.url,
6670
);
67-
output({ config }, { format: options.output, defaultFormat: "text" });
71+
const format = options.output ?? "text";
72+
if (format === "text") {
73+
console.log(config);
74+
} else {
75+
output({ config }, { format, defaultFormat: "text" });
76+
}
6877
return;
6978
}
7079

src/utils/cliStatus.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Status / progress lines for the CLI. Uses stderr so stdout stays free for
3+
* data users redirect or pipe (e.g. SSH config snippets, JSON).
4+
*
5+
* Prefer this over console.error for non-failure messages—console.error reads
6+
* like a runtime error to humans and tools.
7+
*/
8+
export function cliStatus(message: string): void {
9+
process.stderr.write(`${message}\n`);
10+
}

src/utils/ssh.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { writeFile, mkdir, chmod } from "fs/promises";
44
import { join } from "path";
55
import { homedir } from "os";
66
import { getClient } from "./client.js";
7+
import { cliStatus } from "./cliStatus.js";
78
import { processUtils } from "./processUtils.js";
89

910
const execAsync = promisify(exec);
@@ -71,14 +72,21 @@ export async function getSSHKey(devboxId: string): Promise<SSHKeyInfo | null> {
7172
}
7273
}
7374

75+
export interface WaitForReadyOptions {
76+
/** If true, omit periodic poll lines (e.g. when stdout must stay machine-clean). */
77+
quiet?: boolean;
78+
}
79+
7480
/**
7581
* Wait for a devbox to be ready
7682
*/
7783
export async function waitForReady(
7884
devboxId: string,
7985
timeoutSeconds: number = 180,
8086
pollIntervalSeconds: number = 3,
87+
waitOptions?: WaitForReadyOptions,
8188
): Promise<boolean> {
89+
const quiet = waitOptions?.quiet ?? false;
8290
const startTime = Date.now();
8391
const client = getClient();
8492

@@ -89,25 +97,26 @@ export async function waitForReady(
8997
const remaining = timeoutSeconds - elapsed;
9098

9199
if (devbox.status === "running") {
92-
console.log(`Devbox ${devboxId} is ready!`);
93100
return true;
94101
} else if (devbox.status === "failure") {
95-
console.log(
102+
cliStatus(
96103
`Devbox ${devboxId} failed to start (status: ${devbox.status})`,
97104
);
98105
return false;
99106
} else if (["shutdown", "suspended"].includes(devbox.status)) {
100-
console.log(
107+
cliStatus(
101108
`Devbox ${devboxId} is not running (status: ${devbox.status})`,
102109
);
103110
return false;
104111
} else {
105-
console.log(
106-
`Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`,
107-
);
112+
if (!quiet) {
113+
cliStatus(
114+
`Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`,
115+
);
116+
}
108117

109118
if (elapsed >= timeoutSeconds) {
110-
console.log(
119+
cliStatus(
111120
`Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds`,
112121
);
113122
return false;
@@ -120,15 +129,17 @@ export async function waitForReady(
120129
} catch (error) {
121130
const elapsed = (Date.now() - startTime) / 1000;
122131
if (elapsed >= timeoutSeconds) {
123-
console.log(
132+
cliStatus(
124133
`Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds (error: ${error})`,
125134
);
126135
return false;
127136
}
128137

129-
console.log(
130-
`Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`,
131-
);
138+
if (!quiet) {
139+
cliStatus(
140+
`Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`,
141+
);
142+
}
132143
await new Promise((resolve) =>
133144
setTimeout(resolve, pollIntervalSeconds * 1000),
134145
);

0 commit comments

Comments
 (0)