From bf4dc22c2fa340e02f70bb2f70b94f5334ed4338 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:54:28 +0200 Subject: [PATCH] Explain that daemon set-password needs a terminal when stdin is piped (#5358) * Explain that daemon set-password needs a terminal when stdin is piped The password prompt only resolves on a keypress from a terminal. With stdin piped or redirected it never settled, so Node exited 13 with an unsettled top-level await warning and no message. The command now fails with PASSWORD_TTY_REQUIRED and points at PASEO_PASSWORD. Closes #5350 * Assert the set-password terminal error message --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../cli/src/commands/daemon/set-password.ts | 13 ++++++++++- .../cli/tests/32-daemon-set-password.test.ts | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) 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 }); }