mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
feat(server): local-only foreground server with TUI ready banner
- remove --host option from server run/install/web commands (force 127.0.0.1)\n- default foreground logs to silent; add styled ready banner when logs off\n- change foreground stop suggestion from pkill to kill -TERM <pid>\n- update service install plans, tests, and bilingual docs accordingly
This commit is contained in:
parent
2711c36760
commit
c8b16e8eab
11 changed files with 243 additions and 58 deletions
|
|
@ -32,7 +32,6 @@ import {
|
|||
} from './shared';
|
||||
|
||||
export interface InstallCliOptions {
|
||||
host?: string;
|
||||
port?: string;
|
||||
logLevel?: string;
|
||||
force?: boolean;
|
||||
|
|
@ -63,7 +62,6 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps
|
|||
parent
|
||||
.command('install')
|
||||
.description('Install the Kimi server as an OS-managed service (launchd/systemd/schtasks).')
|
||||
.option('--host <host>', `Bind host (default ${DEFAULT_SERVER_HOST})`, DEFAULT_SERVER_HOST)
|
||||
.option('--port <port>', `Bind port (default ${DEFAULT_SERVER_PORT})`, String(DEFAULT_SERVER_PORT))
|
||||
.option(
|
||||
'--log-level <level>',
|
||||
|
|
@ -76,7 +74,7 @@ export function addLifecycleCommands(parent: Command, deps: LifecycleCommandDeps
|
|||
.action(async (opts: InstallCliOptions) => {
|
||||
await runLifecycle(deps, opts.json === true, async (mgr) => {
|
||||
const args: InstallArgs = {
|
||||
host: opts.host ?? DEFAULT_SERVER_HOST,
|
||||
host: DEFAULT_SERVER_HOST,
|
||||
port: parsePort(opts.port, '--port', DEFAULT_SERVER_PORT),
|
||||
logLevel: parseLogLevel(opts.logLevel),
|
||||
force: opts.force === true,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
* registered in `./web-alias.ts`.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
||||
|
||||
import { join } from 'node:path';
|
||||
|
||||
|
|
@ -21,11 +23,12 @@ import {
|
|||
} from '@moonshot-ai/server';
|
||||
|
||||
import { getNativeWebAssetsDir } from '#/native/web-assets';
|
||||
import { darkColors } from '#/tui/theme/colors';
|
||||
import { openUrl as defaultOpenUrl } from '#/utils/open-url';
|
||||
|
||||
import { createKimiCodeHostIdentity, getHostPackageRoot, getVersion } from '../../version';
|
||||
import {
|
||||
DEFAULT_LOG_LEVEL,
|
||||
DEFAULT_FOREGROUND_LOG_LEVEL,
|
||||
DEFAULT_SERVER_HOST,
|
||||
DEFAULT_SERVER_PORT,
|
||||
parseServerOptions,
|
||||
|
|
@ -36,6 +39,7 @@ import {
|
|||
} from './shared';
|
||||
|
||||
const WEB_ASSETS_DIR = 'dist-web';
|
||||
const READY_PANEL_WIDTH = 72;
|
||||
|
||||
export interface RunCliOptions extends ServerCliOptions {
|
||||
open?: boolean;
|
||||
|
|
@ -52,11 +56,6 @@ export interface RunCommandDeps {
|
|||
/** Build the `run` subcommand, mounted under a parent (`server` or top-level). */
|
||||
export function buildRunCommand(cmd: Command, options: { defaultOpen: boolean }): Command {
|
||||
return cmd
|
||||
.option(
|
||||
'--host <host>',
|
||||
`Bind host (default ${DEFAULT_SERVER_HOST})`,
|
||||
DEFAULT_SERVER_HOST,
|
||||
)
|
||||
.option(
|
||||
'--port <port>',
|
||||
`Bind port (default ${DEFAULT_SERVER_PORT})`,
|
||||
|
|
@ -64,8 +63,7 @@ export function buildRunCommand(cmd: Command, options: { defaultOpen: boolean })
|
|||
)
|
||||
.option(
|
||||
'--log-level <level>',
|
||||
`Log level: ${VALID_LOG_LEVELS.join('|')} (default ${DEFAULT_LOG_LEVEL})`,
|
||||
DEFAULT_LOG_LEVEL,
|
||||
`Enable foreground logs at level: ${VALID_LOG_LEVELS.join('|')}. Omit to keep logs off.`,
|
||||
)
|
||||
.option(
|
||||
'--debug-endpoints',
|
||||
|
|
@ -100,6 +98,7 @@ export async function handleRunCommand(
|
|||
): Promise<void> {
|
||||
const parsed = parseServerOptions(opts);
|
||||
let outcome: { origin: string };
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
outcome = await deps.startServerForeground(parsed);
|
||||
} catch (error) {
|
||||
|
|
@ -112,7 +111,12 @@ export async function handleRunCommand(
|
|||
}
|
||||
throw error;
|
||||
}
|
||||
deps.stdout.write(`Kimi server: ${outcome.origin}\n`);
|
||||
const readyMs = Date.now() - startedAt;
|
||||
deps.stdout.write(
|
||||
parsed.logLevel === DEFAULT_FOREGROUND_LOG_LEVEL
|
||||
? formatForegroundReadyBanner(outcome.origin, readyMs)
|
||||
: `Kimi server: ${outcome.origin}\n`,
|
||||
);
|
||||
if (opts.open === true) {
|
||||
deps.openUrl(outcome.origin);
|
||||
}
|
||||
|
|
@ -183,7 +187,7 @@ function describeAlreadyRunning(
|
|||
mode,
|
||||
pid: existing.pid,
|
||||
url: serverOrigin(host === '0.0.0.0' ? DEFAULT_SERVER_HOST : host, status?.port ?? existing.port),
|
||||
stopCommand: mode === 'background' ? 'kimi server stop' : foregroundStopCommand(existing.pid),
|
||||
stopCommand: mode === 'background' ? 'kimi server stop' : formatForegroundStopCommand(existing.pid),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -197,9 +201,12 @@ function isBackgroundServer(
|
|||
return status.installed;
|
||||
}
|
||||
|
||||
function foregroundStopCommand(pid: number): string {
|
||||
if (process.platform === 'win32') return `taskkill /PID ${String(pid)} /T /F`;
|
||||
return 'pkill -f "kimi server run"';
|
||||
export function formatForegroundStopCommand(
|
||||
pid: number,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string {
|
||||
if (platform === 'win32') return `taskkill /PID ${String(pid)} /T /F`;
|
||||
return `kill -TERM ${String(pid)}`;
|
||||
}
|
||||
|
||||
function formatAlreadyRunning(details: AlreadyRunningDetails): string {
|
||||
|
|
@ -211,6 +218,61 @@ function formatAlreadyRunning(details: AlreadyRunningDetails): string {
|
|||
].join('\n');
|
||||
}
|
||||
|
||||
function formatForegroundReadyBanner(origin: string, readyMs: number): string {
|
||||
const primary = (text: string): string => chalk.hex(darkColors.primary)(text);
|
||||
const title = (text: string): string => chalk.bold.hex(darkColors.primary)(text);
|
||||
const dim = (text: string): string => chalk.hex(darkColors.textDim)(text);
|
||||
const muted = (text: string): string => chalk.hex(darkColors.textMuted)(text);
|
||||
const label = (text: string): string => chalk.bold.hex(darkColors.textDim)(text);
|
||||
const url = chalk.hex(darkColors.accent)(displayOrigin(origin));
|
||||
const width = READY_PANEL_WIDTH;
|
||||
const innerWidth = width - 4;
|
||||
const pad = ' ';
|
||||
|
||||
const logo = ['▐█▛█▛█▌', '▐█████▌'] as const;
|
||||
const logoWidth = Math.max(...logo.map((row) => visibleWidth(row)));
|
||||
const gap = ' ';
|
||||
const textWidth = innerWidth - logoWidth - gap.length;
|
||||
const headerLines = [
|
||||
primary(logo[0].padEnd(logoWidth)) +
|
||||
gap +
|
||||
truncateToWidth(title('Kimi server ready'), textWidth, '…'),
|
||||
primary(logo[1].padEnd(logoWidth)) +
|
||||
gap +
|
||||
truncateToWidth(dim('Local web UI is available from this machine.'), textWidth, '…'),
|
||||
];
|
||||
const infoLines = [
|
||||
label('URL: ') + url,
|
||||
label('Network: ') + muted('local only'),
|
||||
label('Logs: ') + muted('off') + dim(' use --log-level info to enable'),
|
||||
label('Stop: ') + muted('Ctrl+C'),
|
||||
label('Ready: ') + muted(`${String(Math.max(0, readyMs))} ms`),
|
||||
label('Version: ') + muted(getVersion()),
|
||||
];
|
||||
const contentLines = [...headerLines, '', ...infoLines];
|
||||
|
||||
const lines = [
|
||||
'',
|
||||
primary('╭' + '─'.repeat(width - 2) + '╮'),
|
||||
primary('│') + ' '.repeat(width - 2) + primary('│'),
|
||||
];
|
||||
|
||||
for (const content of contentLines) {
|
||||
const truncated = truncateToWidth(content, innerWidth, '…');
|
||||
const rightPad = Math.max(0, innerWidth - visibleWidth(truncated));
|
||||
lines.push(primary('│') + pad + truncated + ' '.repeat(rightPad) + primary('│'));
|
||||
}
|
||||
|
||||
lines.push(primary('│') + ' '.repeat(width - 2) + primary('│'));
|
||||
lines.push(primary('╰' + '─'.repeat(width - 2) + '╯'));
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function displayOrigin(origin: string): string {
|
||||
return origin.endsWith('/') ? origin : `${origin}/`;
|
||||
}
|
||||
|
||||
const DEFAULT_RUN_COMMAND_DEPS: RunCommandDeps = {
|
||||
startServerForeground,
|
||||
getServiceStatus: async () => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const DEFAULT_SERVER_PORT = 7878;
|
|||
export const DEFAULT_SERVER_ORIGIN = serverOrigin(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
|
||||
|
||||
export const DEFAULT_LOG_LEVEL: ServerLogLevel = 'info';
|
||||
export const DEFAULT_FOREGROUND_LOG_LEVEL: ServerLogLevel = 'silent';
|
||||
|
||||
export const VALID_LOG_LEVELS: readonly ServerLogLevel[] = [
|
||||
'fatal',
|
||||
|
|
@ -43,7 +44,7 @@ export function parseServerOptions(opts: ServerCliOptions): ParsedServerOptions
|
|||
return {
|
||||
host: opts.host ?? DEFAULT_SERVER_HOST,
|
||||
port: parsePort(opts.port, '--port', DEFAULT_SERVER_PORT),
|
||||
logLevel: parseLogLevel(opts.logLevel),
|
||||
logLevel: parseLogLevel(opts.logLevel ?? DEFAULT_FOREGROUND_LOG_LEVEL),
|
||||
debugEndpoints: opts.debugEndpoints === true,
|
||||
swagger: opts.swagger === true,
|
||||
};
|
||||
|
|
@ -84,7 +85,9 @@ export function normalizeServerOrigin(value: string): string {
|
|||
/** Single probe of `/api/v1/healthz`. Returns true if the response envelope reports `code: 0`. */
|
||||
export async function isServerHealthy(origin: string, timeoutMs: number): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
try {
|
||||
const response = await fetch(`${origin}/api/v1/healthz`, {
|
||||
signal: controller.signal,
|
||||
|
|
@ -123,7 +126,9 @@ export async function waitForServerHealthy(origin: string, timeoutMs: number): P
|
|||
*/
|
||||
export async function ensureServerWebReady(origin: string): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 3000);
|
||||
try {
|
||||
const response = await fetch(`${origin}/`, {
|
||||
headers: { accept: 'text/html' },
|
||||
|
|
|
|||
|
|
@ -10,11 +10,17 @@
|
|||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import chalk, { Chalk } from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { registerServerCommand } from '#/cli/sub/server';
|
||||
import { addLifecycleCommands } from '#/cli/sub/server/lifecycle';
|
||||
import { darkColors } from '#/tui/theme/colors';
|
||||
|
||||
function stripAnsi(text: string): string {
|
||||
return text.replaceAll(/\u001B\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
function makeProgram(): Command {
|
||||
// `commander` exitOverride avoids killing the test runner when --help/error fires.
|
||||
|
|
@ -40,14 +46,14 @@ describe('kimi server', () => {
|
|||
expect(subs).toEqual(['install', 'restart', 'run', 'start', 'status', 'stop', 'uninstall']);
|
||||
});
|
||||
|
||||
it('`server run` exposes --host, --port, --log-level, --debug-endpoints, --swagger, --open', () => {
|
||||
it('`server run` exposes local-only foreground options', () => {
|
||||
const program = makeProgram();
|
||||
const run = program.commands
|
||||
.find((c) => c.name() === 'server')
|
||||
?.commands.find((c) => c.name() === 'run');
|
||||
expect(run).toBeDefined();
|
||||
const longs = run!.options.map((o) => o.long).filter(Boolean);
|
||||
expect(longs).toContain('--host');
|
||||
expect(longs).not.toContain('--host');
|
||||
expect(longs).toContain('--port');
|
||||
expect(longs).toContain('--log-level');
|
||||
expect(longs).toContain('--debug-endpoints');
|
||||
|
|
@ -56,14 +62,14 @@ describe('kimi server', () => {
|
|||
expect(longs).toContain('--open');
|
||||
});
|
||||
|
||||
it('`server install` exposes --host, --port, --log-level, --force, --no-open, --json', () => {
|
||||
it('`server install` exposes local-only service options', () => {
|
||||
const program = makeProgram();
|
||||
const install = program.commands
|
||||
.find((c) => c.name() === 'server')
|
||||
?.commands.find((c) => c.name() === 'install');
|
||||
expect(install).toBeDefined();
|
||||
const longs = install!.options.map((o) => o.long).filter(Boolean);
|
||||
expect(longs).toContain('--host');
|
||||
expect(longs).not.toContain('--host');
|
||||
expect(longs).toContain('--port');
|
||||
expect(longs).toContain('--log-level');
|
||||
expect(longs).toContain('--force');
|
||||
|
|
@ -78,7 +84,7 @@ describe('kimi server', () => {
|
|||
const longs = web!.options.map((o) => o.long).filter(Boolean);
|
||||
// web defaults to opening → the option is the negative form --no-open
|
||||
expect(longs).toContain('--no-open');
|
||||
expect(longs).toContain('--host');
|
||||
expect(longs).not.toContain('--host');
|
||||
expect(longs).toContain('--port');
|
||||
});
|
||||
});
|
||||
|
|
@ -148,7 +154,7 @@ describe('`kimi server` lifecycle handles unavailable service managers', () => {
|
|||
ok: false,
|
||||
action: 'unavailable',
|
||||
platform: 'linux',
|
||||
message: expect.stringContaining('server run --host 0.0.0.0'),
|
||||
message: expect.stringContaining('server run --port <port>'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -268,7 +274,7 @@ describe('`kimi server` lifecycle output', () => {
|
|||
});
|
||||
|
||||
describe('`kimi server run` already-running handling', () => {
|
||||
it('passes --swagger through to foreground startup options', async () => {
|
||||
it('defaults foreground logs off and passes --swagger through to startup options', async () => {
|
||||
const { handleRunCommand } = await import('#/cli/sub/server/run');
|
||||
let parsed: unknown;
|
||||
|
||||
|
|
@ -294,7 +300,116 @@ describe('`kimi server run` already-running handling', () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(parsed).toMatchObject({ swagger: true });
|
||||
expect(parsed).toMatchObject({ logLevel: 'silent', swagger: true });
|
||||
});
|
||||
|
||||
it('enables foreground logs only when --log-level is provided', async () => {
|
||||
const { handleRunCommand } = await import('#/cli/sub/server/run');
|
||||
let parsed: unknown;
|
||||
|
||||
await handleRunCommand(
|
||||
{ port: '7878', logLevel: 'debug' },
|
||||
{
|
||||
startServerForeground: async (options) => {
|
||||
parsed = options;
|
||||
return { origin: 'http://127.0.0.1:7878' };
|
||||
},
|
||||
getServiceStatus: async () => undefined,
|
||||
openUrl: vi.fn(),
|
||||
stdout: {
|
||||
write() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(parsed).toMatchObject({ logLevel: 'debug' });
|
||||
});
|
||||
|
||||
it('prints a TUI-style welcome panel when foreground logs are off', async () => {
|
||||
const { handleRunCommand } = await import('#/cli/sub/server/run');
|
||||
let stdout = '';
|
||||
|
||||
await handleRunCommand(
|
||||
{ port: '7878' },
|
||||
{
|
||||
startServerForeground: async () => ({ origin: 'http://127.0.0.1:7878' }),
|
||||
getServiceStatus: async () => undefined,
|
||||
openUrl: vi.fn(),
|
||||
stdout: {
|
||||
write(chunk: string | Uint8Array) {
|
||||
stdout += String(chunk);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const plain = stripAnsi(stdout);
|
||||
expect(plain).toContain('╭');
|
||||
expect(plain).toContain('╰');
|
||||
expect(plain).toContain('▐█▛█▛█▌');
|
||||
expect(plain).toContain('▐█████▌');
|
||||
expect(plain).toContain('Kimi server ready');
|
||||
expect(plain).toContain('URL:');
|
||||
expect(plain).toContain('http://127.0.0.1:7878/');
|
||||
expect(plain).toContain('Network:');
|
||||
expect(plain).toContain('local only');
|
||||
expect(plain).toContain('Logs:');
|
||||
expect(plain).toContain('off');
|
||||
expect(plain).toContain('Stop:');
|
||||
expect(plain).toContain('Ctrl+C');
|
||||
expect(plain).not.toContain('➜');
|
||||
expect(plain).not.toContain('Kimi server:');
|
||||
});
|
||||
|
||||
it('uses the TUI dark palette for the foreground ready banner', async () => {
|
||||
const { handleRunCommand } = await import('#/cli/sub/server/run');
|
||||
let stdout = '';
|
||||
const previousChalkLevel = chalk.level;
|
||||
chalk.level = 3;
|
||||
|
||||
try {
|
||||
await handleRunCommand(
|
||||
{ port: '7878' },
|
||||
{
|
||||
startServerForeground: async () => ({ origin: 'http://127.0.0.1:7878' }),
|
||||
getServiceStatus: async () => undefined,
|
||||
openUrl: vi.fn(),
|
||||
stdout: {
|
||||
write(chunk: string | Uint8Array) {
|
||||
stdout += String(chunk);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
chalk.level = previousChalkLevel;
|
||||
}
|
||||
|
||||
const color = new Chalk({ level: 3 });
|
||||
expect(stdout).toContain(color.hex(darkColors.primary)('▐█▛█▛█▌'));
|
||||
expect(stdout).toContain(color.bold.hex(darkColors.primary)('Kimi server ready'));
|
||||
expect(stdout).toContain(color.hex(darkColors.accent)('http://127.0.0.1:7878/'));
|
||||
expect(stdout).toContain(color.bold.hex(darkColors.textDim)('URL: '));
|
||||
expect(stdout).toContain(color.hex(darkColors.textMuted)('local only'));
|
||||
});
|
||||
|
||||
it('reports a background service conflict, suggests server stop, and opens the existing URL', async () => {
|
||||
|
|
@ -344,7 +459,7 @@ describe('`kimi server run` already-running handling', () => {
|
|||
expect(openUrl).toHaveBeenCalledWith('http://127.0.0.1:9999');
|
||||
});
|
||||
|
||||
it('reports a foreground process conflict, suggests pkill, and opens the existing URL', async () => {
|
||||
it('reports a foreground process conflict, suggests a pid-based stop command, and opens the existing URL', async () => {
|
||||
const { ServerLockedError } = await import('@moonshot-ai/server');
|
||||
const { handleRunCommand } = await import('#/cli/sub/server/run');
|
||||
let stdout = '';
|
||||
|
|
@ -382,10 +497,18 @@ describe('`kimi server run` already-running handling', () => {
|
|||
|
||||
expect(stdout).toContain('already running in foreground');
|
||||
expect(stdout).toContain('URL: http://127.0.0.1:10001');
|
||||
expect(stdout).toContain('Stop: pkill -f "kimi server run"');
|
||||
expect(stdout).toContain('Stop: kill -TERM 5678');
|
||||
expect(stdout).not.toContain('kimi server stop');
|
||||
expect(openUrl).toHaveBeenCalledWith('http://127.0.0.1:10001');
|
||||
});
|
||||
|
||||
it('formats foreground stop commands by platform and pid', async () => {
|
||||
const { formatForegroundStopCommand } = await import('#/cli/sub/server/run');
|
||||
|
||||
expect(formatForegroundStopCommand(1234, 'darwin')).toBe('kill -TERM 1234');
|
||||
expect(formatForegroundStopCommand(1234, 'linux')).toBe('kill -TERM 1234');
|
||||
expect(formatForegroundStopCommand(1234, 'win32')).toBe('taskkill /PID 1234 /T /F');
|
||||
});
|
||||
});
|
||||
|
||||
describe('`kimi server` does not register a legacy `daemon` command', () => {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ Run, install, and manage the local Kimi server — a single process that exposes
|
|||
When the server is running, `GET /openapi.json` returns the REST OpenAPI document and `GET /asyncapi.json` returns the local WebSocket AsyncAPI document. The Swagger UI is separate and is mounted at `/documentation` only when the server is started with `--swagger`.
|
||||
|
||||
```sh
|
||||
kimi server run # foreground (logs in the current terminal)
|
||||
kimi server run # foreground (logs off unless --log-level is set)
|
||||
kimi server install # register with launchd / systemd / schtasks
|
||||
kimi server start # start the OS-managed service
|
||||
kimi server status # snapshot of installed/running state
|
||||
|
|
@ -158,14 +158,13 @@ kimi server status # snapshot of installed/running state
|
|||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--host <host>` | Bind host; defaults to `127.0.0.1` |
|
||||
| `--port <port>` | Bind port; defaults to `7878` |
|
||||
| `--log-level <level>` | Log level; defaults to `info` |
|
||||
| `--log-level <level>` | Enable foreground logs at the selected level; omitted by default |
|
||||
| `--debug-endpoints` | Mount `/api/v1/debug/*` routes (off by default) |
|
||||
| `--swagger` | Mount the Swagger UI at `/documentation` (off by default) |
|
||||
| `--open` | Open the web UI in the default browser once the server is healthy |
|
||||
|
||||
`kimi server run` does not return — it stays attached to the current terminal and shuts down cleanly on `SIGINT` / `SIGTERM`. For background operation, use the OS-service path below.
|
||||
`kimi server run` binds to local loopback only and does not return — it stays attached to the current terminal and shuts down cleanly on `SIGINT` / `SIGTERM`. For background operation, use the OS-service path below.
|
||||
|
||||
#### `kimi server install`
|
||||
|
||||
|
|
@ -177,13 +176,12 @@ Register the server as an OS-managed service so it starts at login and restarts
|
|||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--host <host>` | Bind host the supervised server uses; defaults to `127.0.0.1` |
|
||||
| `--port <port>` | Bind port the supervised server uses; defaults to `7878` |
|
||||
| `--log-level <level>` | Log level recorded in the generated unit |
|
||||
| `--force` | Replace an existing install instead of failing |
|
||||
| `--json` | Output JSON instead of a human-readable line |
|
||||
|
||||
The chosen host / port / log-level are recorded to `~/.kimi-code/server/install.json` so `kimi server status` can report them even when the service is stopped.
|
||||
The loopback host, chosen port, and log level are recorded to `~/.kimi-code/server/install.json` so `kimi server status` can report them even when the service is stopped.
|
||||
|
||||
#### Lifecycle subcommands
|
||||
|
||||
|
|
@ -204,7 +202,7 @@ kimi web # foreground + open browser
|
|||
kimi web --no-open # equivalent to `kimi server run`
|
||||
```
|
||||
|
||||
The same `--host`, `--port`, `--log-level`, and `--debug-endpoints` flags work as on `kimi server run`.
|
||||
The same `--port`, `--log-level`, `--debug-endpoints`, and `--swagger` flags work as on `kimi server run`.
|
||||
|
||||
### `kimi doctor`
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ kimi acp
|
|||
服务运行时,`GET /openapi.json` 会返回 REST OpenAPI 文档,`GET /asyncapi.json` 会返回本地 WebSocket 协议的 AsyncAPI 文档。Swagger UI 独立于这两个 JSON 接口,只有用 `--swagger` 启动时才会挂载到 `/documentation`。
|
||||
|
||||
```sh
|
||||
kimi server run # 前台运行(日志输出到当前终端)
|
||||
kimi server run # 前台运行(除非设置 --log-level,否则不输出日志)
|
||||
kimi server install # 注册到 launchd / systemd / schtasks
|
||||
kimi server start # 启动 OS 管理的服务
|
||||
kimi server status # 查看安装与运行状态
|
||||
|
|
@ -158,14 +158,13 @@ kimi server status # 查看安装与运行状态
|
|||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `--host <host>` | 绑定地址;默认 `127.0.0.1` |
|
||||
| `--port <port>` | 绑定端口;默认 `7878` |
|
||||
| `--log-level <level>` | 日志级别;默认 `info` |
|
||||
| `--log-level <level>` | 按所选级别开启前台日志;默认不输出 |
|
||||
| `--debug-endpoints` | 挂载 `/api/v1/debug/*` 调试路由(默认关闭) |
|
||||
| `--swagger` | 在 `/documentation` 挂载 Swagger UI(默认关闭) |
|
||||
| `--open` | 服务健康后用默认浏览器打开 web UI |
|
||||
|
||||
`kimi server run` 不会返回——保持挂在当前终端,在 `SIGINT` / `SIGTERM` 时干净退出。后台运行请走下面的 OS 服务方式。
|
||||
`kimi server run` 只绑定本机 loopback 地址,不会返回——保持挂在当前终端,在 `SIGINT` / `SIGTERM` 时干净退出。后台运行请走下面的 OS 服务方式。
|
||||
|
||||
#### `kimi server install`
|
||||
|
||||
|
|
@ -177,13 +176,12 @@ kimi server status # 查看安装与运行状态
|
|||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `--host <host>` | 被托管的服务绑定地址;默认 `127.0.0.1` |
|
||||
| `--port <port>` | 被托管的服务绑定端口;默认 `7878` |
|
||||
| `--log-level <level>` | 写入生成 unit 的日志级别 |
|
||||
| `--force` | 已安装时强制覆盖 |
|
||||
| `--json` | 用 JSON 替代人类可读输出 |
|
||||
|
||||
选定的 host / port / log-level 会写入 `~/.kimi-code/server/install.json`,即便服务停掉 `kimi server status` 也能读到。
|
||||
本机地址、选定的端口和日志级别会写入 `~/.kimi-code/server/install.json`,即便服务停掉 `kimi server status` 也能读到。
|
||||
|
||||
#### 生命周期子命令
|
||||
|
||||
|
|
@ -204,7 +202,7 @@ kimi web # 前台 + 自动打开浏览器
|
|||
kimi web --no-open # 等价于 `kimi server run`
|
||||
```
|
||||
|
||||
`--host`、`--port`、`--log-level`、`--debug-endpoints` 与 `kimi server run` 完全一致。
|
||||
`--port`、`--log-level`、`--debug-endpoints`、`--swagger` 与 `kimi server run` 完全一致。
|
||||
|
||||
### `kimi doctor`
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { dirname } from 'node:path';
|
|||
import { installPlanPath } from './paths';
|
||||
import type { InstallArgs } from './types';
|
||||
|
||||
const SUPERVISED_SERVER_HOST = '127.0.0.1';
|
||||
|
||||
export interface InstallPlan {
|
||||
|
||||
host: string;
|
||||
|
|
@ -35,7 +37,7 @@ export interface BuildInstallPlanInput extends InstallArgs {
|
|||
|
||||
export function buildInstallPlan(input: BuildInstallPlanInput): InstallPlan {
|
||||
return {
|
||||
host: input.host,
|
||||
host: SUPERVISED_SERVER_HOST,
|
||||
port: input.port,
|
||||
logLevel: input.logLevel,
|
||||
program: input.program,
|
||||
|
|
@ -43,8 +45,6 @@ export function buildInstallPlan(input: BuildInstallPlanInput): InstallPlan {
|
|||
input.program,
|
||||
'server',
|
||||
'run',
|
||||
'--host',
|
||||
input.host,
|
||||
'--port',
|
||||
String(input.port),
|
||||
'--log-level',
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export class ServiceUnavailableError extends Error {
|
|||
|
||||
constructor(platform: string, reason: string) {
|
||||
super(
|
||||
`${reason} Run \`kimi server run --host 0.0.0.0 --port <port>\` directly when running inside Docker or another container supervisor.`,
|
||||
`${reason} Run \`kimi server run --port <port>\` directly when running inside Docker or another container supervisor.`,
|
||||
);
|
||||
this.platform = platform;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('buildLaunchAgentPlist', () => {
|
|||
it('renders a well-formed plist with label, ProgramArguments, and stdio paths', () => {
|
||||
const xml = buildLaunchAgentPlist({
|
||||
label: KIMI_SERVER_LABEL,
|
||||
programArguments: ['/usr/local/bin/kimi', 'server', 'run', '--host', '127.0.0.1', '--port', '7878'],
|
||||
programArguments: ['/usr/local/bin/kimi', 'server', 'run', '--port', '7878'],
|
||||
stdoutPath: '/tmp/x.log',
|
||||
stderrPath: '/tmp/x.log',
|
||||
});
|
||||
|
|
@ -93,8 +93,7 @@ describe('buildLaunchAgentPlist', () => {
|
|||
expect(xml).toContain('<string>/usr/local/bin/kimi</string>');
|
||||
expect(xml).toContain('<string>server</string>');
|
||||
expect(xml).toContain('<string>run</string>');
|
||||
expect(xml).toContain('<string>--host</string>');
|
||||
expect(xml).toContain('<string>127.0.0.1</string>');
|
||||
expect(xml).not.toContain('<string>--host</string>');
|
||||
expect(xml).toContain('<string>--port</string>');
|
||||
expect(xml).toContain('<string>7878</string>');
|
||||
expect(xml).toContain('<key>StandardOutPath</key>\n <string>/tmp/x.log</string>');
|
||||
|
|
@ -189,7 +188,7 @@ describe('launchd manager — install', () => {
|
|||
expect(calls[0]?.args[0]).toBe('bootout');
|
||||
expect(calls[1]?.args[0]).toBe('bootstrap');
|
||||
const xml = readFileSync(plistPath, 'utf8');
|
||||
expect(xml).toContain('<string>0.0.0.0</string>');
|
||||
expect(xml).not.toContain('<string>0.0.0.0</string>');
|
||||
expect(xml).toContain('<string>9999</string>');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ describe('buildScheduledTaskXml', () => {
|
|||
const xml = buildScheduledTaskXml({
|
||||
description: 'test desc',
|
||||
command: 'C:\\bin\\kimi.exe',
|
||||
arguments: 'server run --host 127.0.0.1 --port 7878',
|
||||
arguments: 'server run --port 7878',
|
||||
});
|
||||
expect(xml).toContain('<?xml version="1.0" encoding="UTF-16"?>');
|
||||
expect(xml).toContain('<Description>test desc</Description>');
|
||||
expect(xml).toContain('<Command>C:\\bin\\kimi.exe</Command>');
|
||||
expect(xml).toContain('<Arguments>server run --host 127.0.0.1 --port 7878</Arguments>');
|
||||
expect(xml).toContain('<Arguments>server run --port 7878</Arguments>');
|
||||
expect(xml).toContain('<LogonTrigger>');
|
||||
expect(xml).toContain('<RunLevel>LeastPrivilege</RunLevel>');
|
||||
});
|
||||
|
|
@ -167,7 +167,7 @@ describe('schtasks manager — install', () => {
|
|||
expect(result.taskName).toBe(KIMI_SERVER_TASK_NAME);
|
||||
expect(writtenXmls.length).toBe(1);
|
||||
expect(writtenXmls[0]).toContain(`<Description>Kimi Code local server`);
|
||||
expect(writtenXmls[0]).toContain('--host 127.0.0.1');
|
||||
expect(writtenXmls[0]).not.toContain('--host');
|
||||
expect(writtenXmls[0]).toContain('--port 7878');
|
||||
|
||||
expect(calls.length).toBe(2);
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ afterEach(() => {
|
|||
describe('buildSystemdUnit', () => {
|
||||
it('renders the standard [Unit]/[Service]/[Install] triple', () => {
|
||||
const unit = buildSystemdUnit({
|
||||
programArguments: ['/usr/local/bin/kimi', 'server', 'run', '--host', '127.0.0.1', '--port', '7878'],
|
||||
programArguments: ['/usr/local/bin/kimi', 'server', 'run', '--port', '7878'],
|
||||
});
|
||||
expect(unit).toContain('[Unit]');
|
||||
expect(unit).toContain('[Service]');
|
||||
expect(unit).toContain('[Install]');
|
||||
expect(unit).toContain('Description=Kimi Code local server');
|
||||
expect(unit).toContain('ExecStart=/usr/local/bin/kimi server run --host 127.0.0.1 --port 7878');
|
||||
expect(unit).toContain('ExecStart=/usr/local/bin/kimi server run --port 7878');
|
||||
expect(unit).toContain('Restart=always');
|
||||
expect(unit).toContain('WantedBy=default.target');
|
||||
});
|
||||
|
|
@ -149,7 +149,8 @@ describe('systemd manager — install', () => {
|
|||
expect(result.unitPath).toBe(unitPath);
|
||||
expect(existsSync(unitPath)).toBe(true);
|
||||
const text = readFileSync(unitPath, 'utf8');
|
||||
expect(text).toContain('ExecStart=/usr/local/bin/kimi server run --host 127.0.0.1 --port 7878 --log-level info');
|
||||
expect(text).toContain('ExecStart=/usr/local/bin/kimi server run --port 7878 --log-level info');
|
||||
expect(text).not.toContain('--host');
|
||||
|
||||
expect(calls.length).toBe(3);
|
||||
expect(calls[0]?.args).toEqual(['show-environment']);
|
||||
|
|
@ -184,7 +185,8 @@ describe('systemd manager — install', () => {
|
|||
const result = await mgr.install({ host: '0.0.0.0', port: 9999, logLevel: 'debug', force: true });
|
||||
expect(result.status).toBe('replaced');
|
||||
const text = readFileSync(unitPath, 'utf8');
|
||||
expect(text).toContain('ExecStart=/usr/local/bin/kimi server run --host 0.0.0.0 --port 9999 --log-level debug');
|
||||
expect(text).toContain('ExecStart=/usr/local/bin/kimi server run --port 9999 --log-level debug');
|
||||
expect(text).not.toContain('0.0.0.0');
|
||||
});
|
||||
|
||||
it('fails before writing files when user systemd is unavailable', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue