fix(cli): generate a UI password for bare --ui-password in daemon/serve mode

The grand tunnel restructuring removed the CLI's auto-generated UI
password, so `openchamber -d --ui-password` (no value) silently started
an unauthenticated server instead of creating a password as in 1.8.1.

Restore generation for an explicit --ui-password flag without a value:
the password is generated before either launch path, passed to the
daemon/foreground process via OPENCHAMBER_UI_PASSWORD, persisted in the
instance state file, and surfaced once in human/quiet/json output.

Refs OPE-216
This commit is contained in:
Serhii Dziupin
2026-08-05 11:24:10 +03:00
parent 34c221b07f
commit 41a2e3781d
5 changed files with 108 additions and 7 deletions
+33 -5
View File
@@ -2,7 +2,7 @@ import fs from 'fs';
import { pathToFileURL } from 'url';
import { spawn } from 'child_process';
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
import { buildLocalUrl, resolveServeHost, assertSafeBrowserPort, hasUiPasswordConfigured, assertAuthenticatedNetworkExposure } from './cli-network.js';
import { buildLocalUrl, resolveServeHost, assertSafeBrowserPort, resolveServeUiPassword, assertAuthenticatedNetworkExposure } from './cli-network.js';
import { fetchSystemInfoFromPort } from './cli-http.js';
import { isPortAvailable, resolveAvailablePort } from './cli-ports.js';
import { ensureLogsDir, getLogFilePath } from './cli-paths.js';
@@ -110,7 +110,13 @@ async function serveCommand(options) {
rotateLogFile(initialLogPath);
const logFd = fs.openSync(initialLogPath, 'a');
const effectiveUiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
// Resolve the effective UI password before either launch path so a
// password generated for `--ui-password` (no value) is set in the
// daemon/foreground environment before spawning and persisted in the
// instance state file the server and restart/status flows read.
const resolvedUiPassword = resolveServeUiPassword(options);
const effectiveUiPassword = resolvedUiPassword.password;
const autoGeneratedUiPassword = resolvedUiPassword.generated === true;
assertAuthenticatedNetworkExposure({
host: effectiveHost,
uiPassword: effectiveUiPassword,
@@ -214,8 +220,15 @@ async function serveCommand(options) {
if (isQuietMode(options)) {
if (!options.suppressQuietOutput) {
realStdoutWrite(`${resolvedPort}\n`);
realStdoutWrite(
autoGeneratedUiPassword
? `${resolvedPort} pass:${effectiveUiPassword}\n`
: `${resolvedPort}\n`
);
}
} else if (autoGeneratedUiPassword && showOutput && !options.suppressStartupSummary) {
console.log(`Generated UI password: ${effectiveUiPassword}`);
console.log('Save this password — it is not shown again.');
}
// Clean up PID / instance files.
@@ -365,7 +378,11 @@ async function serveCommand(options) {
};
if (isJsonMode(options)) {
printJson({ ...serveResult, messages: jsonMessages });
printJson({
...serveResult,
messages: jsonMessages,
...(autoGeneratedUiPassword ? { password: effectiveUiPassword } : {}),
});
return resolvedPort;
}
@@ -373,7 +390,14 @@ async function serveCommand(options) {
if (options.suppressQuietOutput) {
return resolvedPort;
}
process.stdout.write(`${resolvedPort}\n`);
// A generated password is essential result data for scripts: include it
// in the same compact `pass:` token form `openchamber status --quiet`
// already emits. Configured passwords are never echoed.
process.stdout.write(
autoGeneratedUiPassword
? `${resolvedPort} pass:${effectiveUiPassword}\n`
: `${resolvedPort}\n`
);
return resolvedPort;
}
@@ -382,6 +406,10 @@ async function serveCommand(options) {
if (!options.suppressStartupSummary && showOutput) {
clackIntro('OpenChamber Started');
logStatus('success', `port ${serveResult.port} (PID: ${serveResult.pid})`);
if (autoGeneratedUiPassword) {
logStatus('success', 'UI password', effectiveUiPassword);
logStatus('warning', 'save this password', 'it is not shown again');
}
logStatus('info', `visit: ${serveResult.url}`);
logStatus('info', `logs: ${serveResult.logs}`);
clackOutro('daemon running');