mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-26 00:54:36 +00:00
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:
parent
a53df07159
commit
680e6230bd
4 changed files with 154 additions and 29 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue