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>
This commit is contained in:
paseo-bot[bot]
2026-09-25 06:54:28 +02:00
committed by GitHub
parent 5c9f767d51
commit bf4dc22c2f
2 changed files with 34 additions and 1 deletions
@@ -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<string> {
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,
@@ -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 });
}