diff --git a/src/commands/devbox/ssh.ts b/src/commands/devbox/ssh.ts index e6b4003..7b7bf02 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.`); @@ -64,7 +68,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/cliStatus.ts b/src/utils/cliStatus.ts new file mode 100644 index 0000000..ff19070 --- /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 14b67e8..641997e 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.log(`Devbox ${devboxId} is ready!`); return true; } else if (devbox.status === "failure") { - console.log( + cliStatus( `Devbox ${devboxId} failed to start (status: ${devbox.status})`, ); return false; } else if (["shutdown", "suspended"].includes(devbox.status)) { - console.log( + cliStatus( `Devbox ${devboxId} is not running (status: ${devbox.status})`, ); return false; } else { - console.log( - `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.log( + 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.log( + cliStatus( `Timeout waiting for devbox ${devboxId} to be ready after ${timeoutSeconds} seconds (error: ${error})`, ); return false; } - console.log( - `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), );