feat(cli): add daemon startup reporter for daemon and web commands

- add optional DaemonStartupReporter callback to ensureDaemonRunning\n- report startup progress in daemon and web CLI commands\n- update unit tests to cover reporter output
This commit is contained in:
haozhe.yang 2026-06-10 18:55:47 +08:00
parent a53df07159
commit 680e6230bd
4 changed files with 154 additions and 29 deletions

View file

@ -66,6 +66,8 @@ export type EnsureDaemonResult =
logPath: string;
};
export type DaemonStartupReporter = (message: string) => void;
export interface EnsureDaemonRunningDeps {
isDaemonHealthy(origin: string, timeoutMs: number): Promise<boolean>;
waitForDaemonHealthy(origin: string, timeoutMs: number): Promise<boolean>;
@ -81,7 +83,10 @@ export interface EnsureDaemonRunningDeps {
}
export interface DaemonCommandDeps {
ensureDaemonRunning(options: ParsedDaemonOptions): Promise<EnsureDaemonResult>;
ensureDaemonRunning(
options: ParsedDaemonOptions,
report?: DaemonStartupReporter,
): Promise<EnsureDaemonResult>;
startDaemonForeground(options: ParsedDaemonOptions): Promise<void>;
stdout: Pick<NodeJS.WriteStream, 'write'>;
stderr: Pick<NodeJS.WriteStream, 'write'>;
@ -144,7 +149,9 @@ export async function handleDaemonCommand(
return;
}
const result = await deps.ensureDaemonRunning(parsed);
const result = await deps.ensureDaemonRunning(parsed, (message) => {
writeDaemonStartupStatus(deps.stdout, message);
});
if (result.status === 'already-running') {
deps.stdout.write(
`Kimi daemon already running at ${result.origin}${formatPid(result.pid)}.\n`,
@ -192,10 +199,13 @@ export function parseLogLevel(raw: string | undefined): DaemonLogLevel {
export async function ensureDaemonRunning(
options: ParsedDaemonOptions,
deps: EnsureDaemonRunningDeps = DEFAULT_ENSURE_DAEMON_RUNNING_DEPS,
report: DaemonStartupReporter = () => {},
): Promise<EnsureDaemonResult> {
const origin = daemonOrigin(options.host, options.port);
report(`checking requested daemon at ${origin}`);
if (await deps.isDaemonHealthy(origin, 1000)) {
const lock = deps.readLiveDaemonLock();
report(`requested daemon is healthy; using ${origin}`);
return {
status: 'already-running',
origin,
@ -204,10 +214,17 @@ export async function ensureDaemonRunning(
};
}
report('requested daemon is not healthy');
report(`checking daemon lock at ${DEFAULT_LOCK_PATH}`);
const lock = deps.readLiveDaemonLock();
if (lock !== undefined) {
report(
`found live daemon lock (pid ${lock.pid}, port ${lock.port}, started ${lock.started_at})`,
);
const lockOrigin = daemonOrigin(options.host, lock.port);
report(`checking locked daemon at ${lockOrigin}`);
if (await deps.waitForDaemonHealthy(lockOrigin, 5000)) {
report(`locked daemon is healthy; reusing ${lockOrigin}`);
return {
status: 'already-running',
origin: lockOrigin,
@ -215,15 +232,21 @@ export async function ensureDaemonRunning(
logPath: deps.daemonLogPath(),
};
}
report(`locked daemon did not become healthy at ${lockOrigin}`);
} else {
report('no live daemon lock found');
}
report(`starting daemon in background at ${origin}`);
const started = deps.startDaemonBackground(options);
const ready = await deps.waitForDaemonHealthy(origin, 15_000);
if (!ready) {
report(`daemon did not become healthy at ${origin}`);
throw new Error(
`Kimi daemon did not become healthy at ${origin}. Check logs: ${started.logPath}`,
);
}
report(`daemon is healthy at ${origin}`);
return {
status: 'started',
origin,
@ -389,8 +412,16 @@ function formatPid(pid: number | undefined): string {
return pid === undefined ? '' : ` (pid ${pid})`;
}
function writeDaemonStartupStatus(
stdout: Pick<NodeJS.WriteStream, 'write'>,
message: string,
): void {
stdout.write(`Daemon startup: ${message}\n`);
}
const DEFAULT_DAEMON_COMMAND_DEPS: DaemonCommandDeps = {
ensureDaemonRunning,
ensureDaemonRunning: (options, report) =>
ensureDaemonRunning(options, DEFAULT_ENSURE_DAEMON_RUNNING_DEPS, report),
startDaemonForeground,
stdout: process.stdout,
stderr: process.stderr,

View file

@ -16,6 +16,7 @@ import {
DEFAULT_DAEMON_PORT,
ensureDaemonRunning,
parsePort,
type DaemonStartupReporter,
type EnsureDaemonResult,
} from './daemon';
@ -32,7 +33,7 @@ export interface WebCommandDeps {
port: number;
logLevel: 'info';
debugEndpoints: false;
}): Promise<EnsureDaemonResult>;
}, report?: DaemonStartupReporter): Promise<EnsureDaemonResult>;
ensureDaemonWebReady(origin: string): Promise<void>;
openUrl(url: string): void;
stdout: Pick<NodeJS.WriteStream, 'write'>;
@ -76,12 +77,17 @@ export async function handleWebCommand(
} else {
const host = opts.host ?? DEFAULT_DAEMON_HOST;
const port = parsePort(opts.port, '--port', DEFAULT_DAEMON_PORT);
const daemon = await deps.ensureDaemonRunning({
host,
port,
logLevel: 'info',
debugEndpoints: false,
});
const daemon = await deps.ensureDaemonRunning(
{
host,
port,
logLevel: 'info',
debugEndpoints: false,
},
(message) => {
deps.stdout.write(`Daemon startup: ${message}\n`);
},
);
webOrigin = daemon.origin;
if (daemon.status === 'started') {
deps.stdout.write(
@ -134,7 +140,7 @@ export async function ensureDaemonWebReady(origin: string): Promise<void> {
}
const DEFAULT_WEB_COMMAND_DEPS: WebCommandDeps = {
ensureDaemonRunning,
ensureDaemonRunning: (options, report) => ensureDaemonRunning(options, undefined, report),
ensureDaemonWebReady,
openUrl: defaultOpenUrl,
stdout: process.stdout,

View file

@ -52,12 +52,15 @@ describe('kimi daemon', () => {
await handleDaemonCommand({}, deps);
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith({
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT,
logLevel: 'info',
debugEndpoints: false,
});
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
{
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT,
logLevel: 'info',
debugEndpoints: false,
},
expect.any(Function),
);
expect(deps.startDaemonForeground).not.toHaveBeenCalled();
expect(stderr.join('')).toBe('');
expect(stdout.join('')).toContain('Kimi daemon started in background');
@ -76,6 +79,15 @@ describe('kimi daemon', () => {
await handleDaemonCommand({}, deps);
expect(deps.ensureDaemonRunning).toHaveBeenCalledTimes(1);
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
{
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT,
logLevel: 'info',
debugEndpoints: false,
},
expect.any(Function),
);
expect(deps.startDaemonForeground).not.toHaveBeenCalled();
expect(stderr.join('')).toBe('');
expect(stdout.join('')).toContain('Kimi daemon already running');
@ -136,4 +148,46 @@ describe('kimi daemon', () => {
15_000,
);
});
it('reports when a live daemon lock redirects to another daemon port', async () => {
const parsedOptions = {
host: DEFAULT_DAEMON_HOST,
port: 7999,
logLevel: 'info' as const,
debugEndpoints: false,
};
const status: string[] = [];
const deps: EnsureDaemonRunningDeps = {
isDaemonHealthy: vi.fn(async () => false),
waitForDaemonHealthy: vi.fn(async () => true),
readLiveDaemonLock: vi.fn(() => ({
pid: 4321,
started_at: '2026-06-10T00:00:00.000Z',
port: 7880,
})),
startDaemonBackground: vi.fn(() => ({
pid: 9876,
logPath: '/tmp/kimi-daemon.log',
})),
daemonLogPath: vi.fn(() => '/tmp/kimi-daemon.log'),
};
await expect(
ensureDaemonRunning(parsedOptions, deps, (message) => status.push(message)),
).resolves.toMatchObject({
status: 'already-running',
origin: 'http://127.0.0.1:7880',
pid: 4321,
});
expect(status).toEqual([
'checking requested daemon at http://127.0.0.1:7999',
'requested daemon is not healthy',
expect.stringContaining('checking daemon lock at '),
'found live daemon lock (pid 4321, port 7880, started 2026-06-10T00:00:00.000Z)',
'checking locked daemon at http://127.0.0.1:7880',
'locked daemon is healthy; reusing http://127.0.0.1:7880',
]);
expect(deps.startDaemonBackground).not.toHaveBeenCalled();
});
});

View file

@ -52,12 +52,15 @@ describe('kimi web', () => {
await handleWebCommand({}, deps);
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith({
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT,
logLevel: 'info',
debugEndpoints: false,
});
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
{
host: DEFAULT_DAEMON_HOST,
port: DEFAULT_DAEMON_PORT,
logLevel: 'info',
debugEndpoints: false,
},
expect.any(Function),
);
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith(DEFAULT_DAEMON_ORIGIN);
expect(deps.openUrl).toHaveBeenCalledWith(DEFAULT_DAEMON_ORIGIN);
expect(stderr.join('')).toBe('');
@ -85,16 +88,47 @@ describe('kimi web', () => {
await handleWebCommand({ host: '0.0.0.0', port: '8899' }, deps);
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith({
host: '0.0.0.0',
port: 8899,
logLevel: 'info',
debugEndpoints: false,
});
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
{
host: '0.0.0.0',
port: 8899,
logLevel: 'info',
debugEndpoints: false,
},
expect.any(Function),
);
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith('http://127.0.0.1:8899');
expect(deps.openUrl).toHaveBeenCalledWith('http://127.0.0.1:8899');
});
it('prints daemon startup trace from the local daemon resolver', async () => {
const { deps, stdout } = makeDeps({
ensureDaemonRunning: vi.fn(async (_options, report) => {
report?.('checking requested daemon at http://127.0.0.1:7999');
report?.('found live daemon lock (pid 4321, port 7880, started 2026-06-10T00:00:00.000Z)');
report?.('locked daemon is healthy; reusing http://127.0.0.1:7880');
return {
status: 'already-running' as const,
origin: 'http://127.0.0.1:7880',
pid: 4321,
};
}),
});
await handleWebCommand({ port: '7999', open: false }, deps);
expect(stdout.join('')).toContain(
'Daemon startup: checking requested daemon at http://127.0.0.1:7999',
);
expect(stdout.join('')).toContain(
'Daemon startup: found live daemon lock (pid 4321, port 7880, started 2026-06-10T00:00:00.000Z)',
);
expect(stdout.join('')).toContain(
'Daemon startup: locked daemon is healthy; reusing http://127.0.0.1:7880',
);
expect(stdout.join('')).toContain('Kimi web: http://127.0.0.1:7880');
});
it('respects --no-open while still printing the daemon-hosted web URL', async () => {
const { deps, stdout } = makeDeps();