diff --git a/packages/cli/src/commands/daemon/set-password.ts b/packages/cli/src/commands/daemon/set-password.ts index d572269ad1..b5a79ae65d 100644 --- a/packages/cli/src/commands/daemon/set-password.ts +++ b/packages/cli/src/commands/daemon/set-password.ts @@ -57,6 +57,17 @@ function createCommandError(code: string, message: string, details?: string): Co return { code, message, ...(details ? { details } : {}) }; } +function terminalPasswordPrompt(): PromptPassword { + if (!process.stdin.isTTY) { + throw createCommandError( + "PASSWORD_TTY_REQUIRED", + "paseo daemon set-password needs a terminal to read the password", + "Run it in an interactive terminal, or set PASEO_PASSWORD in the daemon's environment instead.", + ); + } + return (message) => passwordPrompt({ message }); +} + async function promptForPassword(promptPassword: PromptPassword): Promise { const first = await promptPassword("New daemon password"); if (isCancel(first)) { @@ -112,7 +123,7 @@ export async function runSetPasswordCommand( const promptPassword = typeof options.promptPassword === "function" ? (options.promptPassword as PromptPassword) - : (message: string) => passwordPrompt({ message }); + : terminalPasswordPrompt(); const newPassword = await promptForPassword(promptPassword); const result = await setDaemonPasswordInConfig(newPassword, { home: options.daemonTarget.kind === "instance" ? options.daemonTarget.home : undefined, diff --git a/packages/cli/tests/32-daemon-set-password.test.ts b/packages/cli/tests/32-daemon-set-password.test.ts index 7b174e1631..7452b08f0a 100644 --- a/packages/cli/tests/32-daemon-set-password.test.ts +++ b/packages/cli/tests/32-daemon-set-password.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Command } from "commander"; import { isBearerTokenValid } from "@getpaseo/server/auth"; +import { runLocalPaseo } from "./helpers/local-cli.ts"; import { runSetPasswordCommand, setDaemonPasswordInConfig, @@ -105,6 +106,27 @@ try { ); console.log("✓ command refuses password mismatch\n"); } + + { + console.log("Test 4: piped stdin fails with a message instead of exiting silently"); + const pipedHome = join(root, "piped"); + await mkdir(pipedHome, { recursive: true }); + const run = runLocalPaseo(["daemon", "set-password", "--home", pipedHome, "--json"]); + run.stdin.end("x\nx\n"); + const result = await run; + + assert.strictEqual(result.exitCode, 1, result.stderr); + const error = JSON.parse(result.stderr).error as { + code: string; + message: string; + details: string; + }; + assert.strictEqual(error.code, "PASSWORD_TTY_REQUIRED"); + assert.match(error.message, /needs a terminal/); + assert.match(error.details, /PASEO_PASSWORD/); + await assert.rejects(readFile(join(pipedHome, "config.json"), "utf-8")); + console.log("✓ piped stdin reports that a terminal is required\n"); + } } finally { await rm(root, { recursive: true, force: true }); }