From f58d58241055b43f2d601021322302fbc7d3c17b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 00:00:25 +0000 Subject: [PATCH 1/2] fix(devbox ssh): stdout is clean for --config-only redirects - Send waitForReady progress messages to stderr so they are not appended to ~/.ssh/config when redirecting stdout. - For --config-only with default text output, print raw SSH config instead of key-value text that prefixed lines with 'config:'. --- src/commands/devbox/ssh.ts | 7 ++++++- src/utils/ssh.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/commands/devbox/ssh.ts b/src/commands/devbox/ssh.ts index e6b40030..1810223b 100644 --- a/src/commands/devbox/ssh.ts +++ b/src/commands/devbox/ssh.ts @@ -64,7 +64,12 @@ export async function sshDevbox(devboxId: string, options: SSHOptions = {}) { sshInfo!.keyfilePath, sshInfo!.url, ); - output({ config }, { format: options.output, defaultFormat: "text" }); + const format = options.output ?? "text"; + if (format === "text") { + console.log(config); + } else { + output({ config }, { format, defaultFormat: "text" }); + } return; } diff --git a/src/utils/ssh.ts b/src/utils/ssh.ts index 14b67e8a..b9357716 100644 --- a/src/utils/ssh.ts +++ b/src/utils/ssh.ts @@ -89,25 +89,25 @@ export async function waitForReady( const remaining = timeoutSeconds - elapsed; if (devbox.status === "running") { - console.log(`Devbox ${devboxId} is ready!`); + console.error(`Devbox ${devboxId} is ready!`); return true; } else if (devbox.status === "failure") { - console.log( + console.error( `Devbox ${devboxId} failed to start (status: ${devbox.status})`, ); return false; } else if (["shutdown", "suspended"].includes(devbox.status)) { - console.log( + console.error( `Devbox ${devboxId} is not running (status: ${devbox.status})`, ); return false; } else { - console.log( + console.error( `Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`, ); if (elapsed >= timeoutSeconds) { - console.log( + console.error( `Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds`, ); return false; @@ -120,13 +120,13 @@ export async function waitForReady( } catch (error) { const elapsed = (Date.now() - startTime) / 1000; if (elapsed >= timeoutSeconds) { - console.log( + console.error( `Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds (error: ${error})`, ); return false; } - console.log( + console.error( `Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`, ); await new Promise((resolve) => From f8dafafcb266860e5eca2b60221c1e1a4b8b6283 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 00:07:24 +0000 Subject: [PATCH 2/2] refactor(cli): replace console.error for devbox wait with cliStatus - Add cliStatus() writing to stderr (documented as progress, not errors). - Drop redundant 'is ready!' line after wait succeeds. - For --config-only, skip wait banner and poll chatter; still print timeout/failure/retry exhaustion on stderr. --- src/commands/devbox/ssh.ts | 6 +++++- src/utils/cliStatus.ts | 10 ++++++++++ src/utils/ssh.ts | 33 ++++++++++++++++++++++----------- 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 src/utils/cliStatus.ts diff --git a/src/commands/devbox/ssh.ts b/src/commands/devbox/ssh.ts index 1810223b..7b7bf020 100644 --- a/src/commands/devbox/ssh.ts +++ b/src/commands/devbox/ssh.ts @@ -4,6 +4,7 @@ import { spawn } from "child_process"; import { getClient } from "../../utils/client.js"; +import { cliStatus } from "../../utils/cliStatus.js"; import { output, outputError } from "../../utils/output.js"; import { processUtils } from "../../utils/processUtils.js"; import { @@ -36,11 +37,14 @@ export async function sshDevbox(devboxId: string, options: SSHOptions = {}) { // Wait for devbox to be ready unless --no-wait is specified if (!options.noWait) { - console.error(`Waiting for devbox ${devboxId} to be ready...`); + if (!options.configOnly) { + cliStatus(`Waiting for devbox ${devboxId} to be ready...`); + } const isReady = await waitForReady( devboxId, options.timeout || 180, options.pollInterval || 3, + { quiet: options.configOnly }, ); if (!isReady) { outputError(`Devbox ${devboxId} is not ready. Please try again later.`); diff --git a/src/utils/cliStatus.ts b/src/utils/cliStatus.ts new file mode 100644 index 00000000..ff190709 --- /dev/null +++ b/src/utils/cliStatus.ts @@ -0,0 +1,10 @@ +/** + * Status / progress lines for the CLI. Uses stderr so stdout stays free for + * data users redirect or pipe (e.g. SSH config snippets, JSON). + * + * Prefer this over console.error for non-failure messages—console.error reads + * like a runtime error to humans and tools. + */ +export function cliStatus(message: string): void { + process.stderr.write(`${message}\n`); +} diff --git a/src/utils/ssh.ts b/src/utils/ssh.ts index b9357716..641997e2 100644 --- a/src/utils/ssh.ts +++ b/src/utils/ssh.ts @@ -4,6 +4,7 @@ import { writeFile, mkdir, chmod } from "fs/promises"; import { join } from "path"; import { homedir } from "os"; import { getClient } from "./client.js"; +import { cliStatus } from "./cliStatus.js"; import { processUtils } from "./processUtils.js"; const execAsync = promisify(exec); @@ -71,6 +72,11 @@ export async function getSSHKey(devboxId: string): Promise { } } +export interface WaitForReadyOptions { + /** If true, omit periodic poll lines (e.g. when stdout must stay machine-clean). */ + quiet?: boolean; +} + /** * Wait for a devbox to be ready */ @@ -78,7 +84,9 @@ export async function waitForReady( devboxId: string, timeoutSeconds: number = 180, pollIntervalSeconds: number = 3, + waitOptions?: WaitForReadyOptions, ): Promise { + const quiet = waitOptions?.quiet ?? false; const startTime = Date.now(); const client = getClient(); @@ -89,25 +97,26 @@ export async function waitForReady( const remaining = timeoutSeconds - elapsed; if (devbox.status === "running") { - console.error(`Devbox ${devboxId} is ready!`); return true; } else if (devbox.status === "failure") { - console.error( + cliStatus( `Devbox ${devboxId} failed to start (status: ${devbox.status})`, ); return false; } else if (["shutdown", "suspended"].includes(devbox.status)) { - console.error( + cliStatus( `Devbox ${devboxId} is not running (status: ${devbox.status})`, ); return false; } else { - console.error( - `Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`, - ); + if (!quiet) { + cliStatus( + `Devbox ${devboxId} is still ${devbox.status}... (elapsed: ${elapsed.toFixed(0)}s, remaining: ${remaining.toFixed(0)}s)`, + ); + } if (elapsed >= timeoutSeconds) { - console.error( + cliStatus( `Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds`, ); return false; @@ -120,15 +129,17 @@ export async function waitForReady( } catch (error) { const elapsed = (Date.now() - startTime) / 1000; if (elapsed >= timeoutSeconds) { - console.error( + cliStatus( `Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds (error: ${error})`, ); return false; } - console.error( - `Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`, - ); + if (!quiet) { + cliStatus( + `Error checking devbox status: ${error}, retrying in ${pollIntervalSeconds} seconds...`, + ); + } await new Promise((resolve) => setTimeout(resolve, pollIntervalSeconds * 1000), );