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:
haozhe.yang 2026-06-12 16:09:21 +08:00
parent 2711c36760
commit c8b16e8eab
11 changed files with 243 additions and 58 deletions

View file

@ -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,

View file

@ -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 () => {

View file

@ -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' },

View file

@ -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', () => {

View file

@ -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`

View file

@ -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`

View file

@ -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',

View file

@ -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;
}

View file

@ -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>');
});

View file

@ -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);

View file

@ -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 () => {