mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-19 08:28:06 +02:00
feat: add runtime daemon start stop commands
This commit is contained in:
parent
a6cecd401d
commit
7eb1dd7606
5 changed files with 225 additions and 8 deletions
|
|
@ -4,9 +4,11 @@ import type { KtxRuntimeArgs } from '../runtime.js';
|
|||
|
||||
type RuntimeFeature = Extract<KtxRuntimeArgs, { command: 'install' }>['feature'];
|
||||
|
||||
const runtimeFeatureOption = new Option('--feature <feature>', 'Runtime feature level')
|
||||
.choices(['core', 'local-embeddings'])
|
||||
.default('core');
|
||||
function createRuntimeFeatureOption() {
|
||||
return new Option('--feature <feature>', 'Runtime feature level')
|
||||
.choices(['core', 'local-embeddings'])
|
||||
.default('core');
|
||||
}
|
||||
|
||||
async function runRuntimeArgs(context: KtxCliCommandContext, args: KtxRuntimeArgs): Promise<void> {
|
||||
const runner = context.deps.runtime ?? (await import('../runtime.js')).runKtxRuntime;
|
||||
|
|
@ -22,9 +24,10 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand
|
|||
runtime
|
||||
.command('install')
|
||||
.description('Install the bundled Python runtime wheel into the managed runtime')
|
||||
.addOption(runtimeFeatureOption)
|
||||
.addOption(createRuntimeFeatureOption())
|
||||
.option('--yes', 'Accept runtime installation without prompting', false)
|
||||
.option('--force', 'Reinstall even when the runtime already looks ready', false)
|
||||
.action(async (options: { feature: RuntimeFeature; force?: boolean }) => {
|
||||
.action(async (options: { feature: RuntimeFeature; yes?: boolean; force?: boolean }) => {
|
||||
await runRuntimeArgs(context, {
|
||||
command: 'install',
|
||||
cliVersion: context.packageInfo.version,
|
||||
|
|
@ -33,6 +36,30 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand
|
|||
});
|
||||
});
|
||||
|
||||
runtime
|
||||
.command('start')
|
||||
.description('Start the KTX-managed Python HTTP daemon')
|
||||
.addOption(createRuntimeFeatureOption())
|
||||
.option('--force', 'Restart even when a matching daemon is already running', false)
|
||||
.action(async (options: { feature: RuntimeFeature; force?: boolean }) => {
|
||||
await runRuntimeArgs(context, {
|
||||
command: 'start',
|
||||
cliVersion: context.packageInfo.version,
|
||||
feature: options.feature,
|
||||
force: options.force === true,
|
||||
});
|
||||
});
|
||||
|
||||
runtime
|
||||
.command('stop')
|
||||
.description('Stop the KTX-managed Python HTTP daemon')
|
||||
.action(async () => {
|
||||
await runRuntimeArgs(context, {
|
||||
command: 'stop',
|
||||
cliVersion: context.packageInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
runtime
|
||||
.command('status')
|
||||
.description('Show managed Python runtime status')
|
||||
|
|
|
|||
|
|
@ -127,13 +127,21 @@ describe('runKtxCli', () => {
|
|||
it('routes runtime management commands with the CLI package version', async () => {
|
||||
const runtime = vi.fn(async () => 0);
|
||||
const installIo = makeIo();
|
||||
const startIo = makeIo();
|
||||
const stopIo = makeIo();
|
||||
const statusIo = makeIo();
|
||||
const doctorIo = makeIo();
|
||||
const pruneIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(['runtime', 'install', '--feature', 'local-embeddings', '--force'], installIo.io, { runtime }),
|
||||
runKtxCli(['runtime', 'install', '--feature', 'local-embeddings', '--force', '--yes'], installIo.io, {
|
||||
runtime,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(['runtime', 'start', '--feature', 'local-embeddings', '--force'], startIo.io, { runtime }),
|
||||
).resolves.toBe(0);
|
||||
await expect(runKtxCli(['runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['runtime', 'doctor'], doctorIo.io, { runtime })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(0);
|
||||
|
|
@ -150,6 +158,24 @@ describe('runKtxCli', () => {
|
|||
);
|
||||
expect(runtime).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
command: 'start',
|
||||
cliVersion: '0.0.0-private',
|
||||
feature: 'local-embeddings',
|
||||
force: true,
|
||||
},
|
||||
startIo.io,
|
||||
);
|
||||
expect(runtime).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
{
|
||||
command: 'stop',
|
||||
cliVersion: '0.0.0-private',
|
||||
},
|
||||
stopIo.io,
|
||||
);
|
||||
expect(runtime).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
{
|
||||
command: 'status',
|
||||
cliVersion: '0.0.0-private',
|
||||
|
|
@ -158,7 +184,7 @@ describe('runKtxCli', () => {
|
|||
statusIo.io,
|
||||
);
|
||||
expect(runtime).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
5,
|
||||
{
|
||||
command: 'doctor',
|
||||
cliVersion: '0.0.0-private',
|
||||
|
|
@ -167,7 +193,7 @@ describe('runKtxCli', () => {
|
|||
doctorIo.io,
|
||||
);
|
||||
expect(runtime).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
6,
|
||||
{
|
||||
command: 'prune',
|
||||
cliVersion: '0.0.0-private',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@ export type {
|
|||
} from './setup-sources.js';
|
||||
export { runKtxSetupSourcesStep } from './setup-sources.js';
|
||||
export { runKtxRuntime, type KtxRuntimeArgs, type KtxRuntimeDeps } from './runtime.js';
|
||||
export {
|
||||
allocateDaemonPort,
|
||||
readManagedPythonDaemonStatus,
|
||||
startManagedPythonDaemon,
|
||||
stopManagedPythonDaemon,
|
||||
} from './managed-python-daemon.js';
|
||||
export type {
|
||||
ManagedPythonDaemonStartResult,
|
||||
ManagedPythonDaemonState,
|
||||
ManagedPythonDaemonStatus,
|
||||
ManagedPythonDaemonStopResult,
|
||||
} from './managed-python-daemon.js';
|
||||
export type { KtxMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
|
||||
export {
|
||||
renderMemoryFlowTui,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type {
|
||||
ManagedPythonDaemonStartResult,
|
||||
ManagedPythonDaemonStopResult,
|
||||
} from './managed-python-daemon.js';
|
||||
import type {
|
||||
ManagedPythonRuntimeDoctorCheck,
|
||||
ManagedPythonRuntimeInstallResult,
|
||||
|
|
@ -106,6 +110,102 @@ describe('runKtxRuntime', () => {
|
|||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('starts the managed Python daemon and prints the base URL', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
startDaemon: vi.fn(async (): Promise<ManagedPythonDaemonStartResult> => ({
|
||||
status: 'started',
|
||||
baseUrl: 'http://127.0.0.1:61234',
|
||||
layout: {
|
||||
cliVersion: '0.2.0',
|
||||
runtimeRoot: '/runtime',
|
||||
versionDir: '/runtime/0.2.0',
|
||||
venvDir: '/runtime/0.2.0/.venv',
|
||||
manifestPath: '/runtime/0.2.0/manifest.json',
|
||||
installLogPath: '/runtime/0.2.0/install.log',
|
||||
assetDir: '/assets/python',
|
||||
assetManifestPath: '/assets/python/manifest.json',
|
||||
pythonPath: '/runtime/0.2.0/.venv/bin/python',
|
||||
daemonPath: '/runtime/0.2.0/.venv/bin/ktx-daemon',
|
||||
daemonStatePath: '/runtime/0.2.0/daemon.json',
|
||||
daemonStdoutPath: '/runtime/0.2.0/daemon.stdout.log',
|
||||
daemonStderrPath: '/runtime/0.2.0/daemon.stderr.log',
|
||||
},
|
||||
state: {
|
||||
schemaVersion: 1,
|
||||
pid: 4242,
|
||||
host: '127.0.0.1',
|
||||
port: 61234,
|
||||
version: '0.2.0',
|
||||
features: ['core', 'local-embeddings'],
|
||||
startedAt: '2026-05-11T00:00:00.000Z',
|
||||
stdoutLog: '/runtime/0.2.0/daemon.stdout.log',
|
||||
stderrLog: '/runtime/0.2.0/daemon.stderr.log',
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
await expect(
|
||||
runKtxRuntime(
|
||||
{ command: 'start', cliVersion: '0.2.0', feature: 'local-embeddings', force: true },
|
||||
io.io,
|
||||
deps,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(deps.startDaemon).toHaveBeenCalledWith({
|
||||
cliVersion: '0.2.0',
|
||||
features: ['local-embeddings'],
|
||||
force: true,
|
||||
});
|
||||
expect(io.stdout()).toContain('Started KTX Python daemon');
|
||||
expect(io.stdout()).toContain('url: http://127.0.0.1:61234');
|
||||
expect(io.stdout()).toContain('pid: 4242');
|
||||
expect(io.stdout()).toContain('features: core, local-embeddings');
|
||||
expect(io.stdout()).toContain('stderr: /runtime/0.2.0/daemon.stderr.log');
|
||||
});
|
||||
|
||||
it('stops the managed Python daemon', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
stopDaemon: vi.fn(async (): Promise<ManagedPythonDaemonStopResult> => ({
|
||||
status: 'stopped',
|
||||
layout: {
|
||||
cliVersion: '0.2.0',
|
||||
runtimeRoot: '/runtime',
|
||||
versionDir: '/runtime/0.2.0',
|
||||
venvDir: '/runtime/0.2.0/.venv',
|
||||
manifestPath: '/runtime/0.2.0/manifest.json',
|
||||
installLogPath: '/runtime/0.2.0/install.log',
|
||||
assetDir: '/assets/python',
|
||||
assetManifestPath: '/assets/python/manifest.json',
|
||||
pythonPath: '/runtime/0.2.0/.venv/bin/python',
|
||||
daemonPath: '/runtime/0.2.0/.venv/bin/ktx-daemon',
|
||||
daemonStatePath: '/runtime/0.2.0/daemon.json',
|
||||
daemonStdoutPath: '/runtime/0.2.0/daemon.stdout.log',
|
||||
daemonStderrPath: '/runtime/0.2.0/daemon.stderr.log',
|
||||
},
|
||||
state: {
|
||||
schemaVersion: 1,
|
||||
pid: 4242,
|
||||
host: '127.0.0.1',
|
||||
port: 61234,
|
||||
version: '0.2.0',
|
||||
features: ['core'],
|
||||
startedAt: '2026-05-11T00:00:00.000Z',
|
||||
stdoutLog: '/runtime/0.2.0/daemon.stdout.log',
|
||||
stderrLog: '/runtime/0.2.0/daemon.stderr.log',
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
await expect(runKtxRuntime({ command: 'stop', cliVersion: '0.2.0' }, io.io, deps)).resolves.toBe(0);
|
||||
|
||||
expect(deps.stopDaemon).toHaveBeenCalledWith({ cliVersion: '0.2.0' });
|
||||
expect(io.stdout()).toContain('Stopped KTX Python daemon');
|
||||
expect(io.stdout()).toContain('pid: 4242');
|
||||
});
|
||||
|
||||
it('prints runtime status as JSON', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import {
|
||||
startManagedPythonDaemon,
|
||||
stopManagedPythonDaemon,
|
||||
type ManagedPythonDaemonStartResult,
|
||||
type ManagedPythonDaemonStopResult,
|
||||
} from './managed-python-daemon.js';
|
||||
import {
|
||||
doctorManagedPythonRuntime,
|
||||
installManagedPythonRuntime,
|
||||
|
|
@ -15,12 +21,20 @@ import {
|
|||
|
||||
export type KtxRuntimeArgs =
|
||||
| { command: 'install'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean }
|
||||
| { command: 'start'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean }
|
||||
| { command: 'stop'; cliVersion: string }
|
||||
| { command: 'status'; cliVersion: string; json: boolean }
|
||||
| { command: 'doctor'; cliVersion: string; json: boolean }
|
||||
| { command: 'prune'; cliVersion: string; dryRun: boolean; yes: boolean };
|
||||
|
||||
export interface KtxRuntimeDeps {
|
||||
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
||||
startDaemon?: (options: {
|
||||
cliVersion: string;
|
||||
features: KtxRuntimeFeature[];
|
||||
force?: boolean;
|
||||
}) => Promise<ManagedPythonDaemonStartResult>;
|
||||
stopDaemon?: (options: { cliVersion: string }) => Promise<ManagedPythonDaemonStopResult>;
|
||||
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
||||
doctorRuntime?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeDoctorCheck[]>;
|
||||
pruneRuntime?: (options: {
|
||||
|
|
@ -45,6 +59,28 @@ function writeInstallResult(io: KtxCliIo, result: ManagedPythonRuntimeInstallRes
|
|||
io.stdout.write(`install log: ${result.layout.installLogPath}\n`);
|
||||
}
|
||||
|
||||
function writeDaemonStart(io: KtxCliIo, result: ManagedPythonDaemonStartResult): void {
|
||||
const verb = result.status === 'reused' ? 'Using existing' : 'Started';
|
||||
io.stdout.write(`${verb} KTX Python daemon\n`);
|
||||
io.stdout.write(`url: ${result.baseUrl}\n`);
|
||||
io.stdout.write(`pid: ${result.state.pid}\n`);
|
||||
io.stdout.write(`version: ${result.state.version}\n`);
|
||||
io.stdout.write(`features: ${result.state.features.join(', ')}\n`);
|
||||
io.stdout.write(`state: ${result.layout.daemonStatePath}\n`);
|
||||
io.stdout.write(`stdout: ${result.state.stdoutLog}\n`);
|
||||
io.stdout.write(`stderr: ${result.state.stderrLog}\n`);
|
||||
}
|
||||
|
||||
function writeDaemonStop(io: KtxCliIo, result: ManagedPythonDaemonStopResult): void {
|
||||
if (result.status === 'already-stopped') {
|
||||
io.stdout.write('KTX Python daemon already stopped\n');
|
||||
return;
|
||||
}
|
||||
io.stdout.write('Stopped KTX Python daemon\n');
|
||||
io.stdout.write(`pid: ${result.state?.pid ?? 'unknown'}\n`);
|
||||
io.stdout.write(`state: ${result.layout.daemonStatePath}\n`);
|
||||
}
|
||||
|
||||
function writeStatus(io: KtxCliIo, status: ManagedPythonRuntimeStatus): void {
|
||||
io.stdout.write('KTX Python runtime\n');
|
||||
io.stdout.write(`status: ${status.kind}\n`);
|
||||
|
|
@ -95,6 +131,22 @@ export async function runKtxRuntime(
|
|||
writeInstallResult(io, result);
|
||||
return 0;
|
||||
}
|
||||
if (args.command === 'start') {
|
||||
const startDaemon = deps.startDaemon ?? startManagedPythonDaemon;
|
||||
const result = await startDaemon({
|
||||
cliVersion: args.cliVersion,
|
||||
features: [args.feature],
|
||||
force: args.force,
|
||||
});
|
||||
writeDaemonStart(io, result);
|
||||
return 0;
|
||||
}
|
||||
if (args.command === 'stop') {
|
||||
const stopDaemon = deps.stopDaemon ?? stopManagedPythonDaemon;
|
||||
const result = await stopDaemon({ cliVersion: args.cliVersion });
|
||||
writeDaemonStop(io, result);
|
||||
return 0;
|
||||
}
|
||||
if (args.command === 'status') {
|
||||
const readStatus = deps.readStatus ?? readManagedPythonRuntimeStatus;
|
||||
const status = await readStatus({ cliVersion: args.cliVersion });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue