import type { KtxCliIo } from './cli-runtime.js'; import { stopAllManagedPythonDaemons, startManagedPythonDaemon, stopManagedPythonDaemon, type ManagedPythonDaemonStopAllResult, type ManagedPythonDaemonStartResult, type ManagedPythonDaemonStopResult, } from './managed-python-daemon.js'; import { doctorManagedPythonRuntime, installManagedPythonRuntime, readManagedPythonRuntimeStatus, type KtxRuntimeFeature, type ManagedPythonRuntimeDoctorCheck, type ManagedPythonRuntimeInstallOptions, type ManagedPythonRuntimeInstallResult, type ManagedPythonRuntimeLayoutOptions, type ManagedPythonRuntimeStatus, } from './managed-python-runtime.js'; export type KtxRuntimeArgs = | { command: 'install'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean } | { command: 'start'; cliVersion: string; projectDir: string; feature: KtxRuntimeFeature; force: boolean } | { command: 'stop'; cliVersion: string; projectDir: string; all: boolean } | { command: 'status'; cliVersion: string; json: boolean }; export interface KtxRuntimeDeps { installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise; startDaemon?: (options: { cliVersion: string; projectDir: string; features: KtxRuntimeFeature[]; force?: boolean; }) => Promise; stopDaemon?: (options: { cliVersion: string; projectDir: string }) => Promise; stopAllDaemons?: (options: { cliVersion: string; projectDir: string }) => Promise; readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise; doctorRuntime?: (options: ManagedPythonRuntimeLayoutOptions) => Promise; } function writeJson(io: KtxCliIo, value: unknown): void { io.stdout.write(`${JSON.stringify(value, null, 2)}\n`); } function writeInstallResult(io: KtxCliIo, result: ManagedPythonRuntimeInstallResult): void { const verb = result.status === 'ready' ? 'Using existing' : 'Installed'; io.stdout.write(`${verb} KTX Python runtime\n`); io.stdout.write(`version: ${result.manifest.cliVersion}\n`); io.stdout.write(`features: ${result.manifest.features.join(', ')}\n`); io.stdout.write(`python: ${result.manifest.python.executable}\n`); io.stdout.write(`daemon: ${result.manifest.python.daemonExecutable}\n`); io.stdout.write(`manifest: ${result.layout.manifestPath}\n`); 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 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 daemon already stopped\n'); return; } io.stdout.write('Stopped KTX daemon\n'); io.stdout.write(`pid: ${result.state?.pid ?? 'unknown'}\n`); io.stdout.write(`state: ${result.layout.daemonStatePath}\n`); } function writeStopAllEntry(io: KtxCliIo, entry: { pid: number; source: string; url?: string; health?: string; detail?: string }): void { io.stdout.write( `pid: ${entry.pid} source: ${entry.source}${entry.url ? ` url: ${entry.url}` : ''}${ entry.health ? ` health: ${entry.health}` : '' }${ entry.detail ? ` detail: ${entry.detail}` : '' }\n`, ); } function writeDaemonStopAll(io: KtxCliIo, result: ManagedPythonDaemonStopAllResult): number { const failed = result.failed.length + result.scanErrors.length; if ( result.stopped.length === 0 && result.stale.length === 0 && result.failed.length === 0 && result.scanErrors.length === 0 ) { io.stdout.write('No KTX daemons found\n'); return 0; } if (failed === 0) { io.stdout.write(`Stopped ${result.stopped.length} KTX daemons\n`); if (result.stale.length > 0) { io.stdout.write(`Cleaned ${result.stale.length} stale daemon states\n`); } for (const entry of result.stopped) { writeStopAllEntry(io, entry); } for (const entry of result.stale) { writeStopAllEntry(io, entry); } return 0; } io.stderr.write( `Stopped ${result.stopped.length} KTX daemons; failed ${result.failed.length}${ result.stale.length > 0 ? `; cleaned stale ${result.stale.length}` : '' }\n`, ); for (const entry of result.failed) { io.stderr.write( `pid: ${entry.pid} source: ${entry.source}${entry.url ? ` url: ${entry.url}` : ''}${ entry.health ? ` health: ${entry.health}` : '' } detail: ${entry.detail}\n`, ); } for (const error of result.scanErrors) { io.stderr.write(`process scan: ${error}\n`); } return 1; } function writeStatus(io: KtxCliIo, status: ManagedPythonRuntimeStatus): void { io.stdout.write('KTX Python runtime\n'); io.stdout.write(`status: ${status.kind}\n`); io.stdout.write(`detail: ${status.detail}\n`); io.stdout.write(`runtime root: ${status.layout.runtimeRoot}\n`); io.stdout.write(`version dir: ${status.layout.versionDir}\n`); if (status.manifest) { io.stdout.write(`features: ${status.manifest.features.join(', ')}\n`); io.stdout.write(`python: ${status.manifest.python.executable}\n`); io.stdout.write(`daemon: ${status.manifest.python.daemonExecutable}\n`); } } function writeRuntimeChecks(io: KtxCliIo, checks: ManagedPythonRuntimeDoctorCheck[]): void { io.stdout.write('KTX Python runtime checks\n'); for (const check of checks) { io.stdout.write(`${check.status.toUpperCase()} ${check.label}: ${check.detail}\n`); if (check.fix) { io.stdout.write(` Fix: ${check.fix}\n`); } } } function hasRuntimeStatusFailure(status: ManagedPythonRuntimeStatus): boolean { return status.kind !== 'ready'; } export async function runKtxRuntime( args: KtxRuntimeArgs, io: KtxCliIo = process, deps: KtxRuntimeDeps = {}, ): Promise { try { if (args.command === 'install') { const installRuntime = deps.installRuntime ?? installManagedPythonRuntime; const result = await installRuntime({ cliVersion: args.cliVersion, features: [args.feature], force: args.force, }); writeInstallResult(io, result); return 0; } if (args.command === 'start') { const startDaemon = deps.startDaemon ?? startManagedPythonDaemon; const result = await startDaemon({ cliVersion: args.cliVersion, projectDir: args.projectDir, features: [args.feature], force: args.force, }); writeDaemonStart(io, result); return 0; } if (args.command === 'stop') { if (args.all) { const stopAllDaemons = deps.stopAllDaemons ?? stopAllManagedPythonDaemons; const result = await stopAllDaemons({ cliVersion: args.cliVersion, projectDir: args.projectDir }); return writeDaemonStopAll(io, result); } else { const stopDaemon = deps.stopDaemon ?? stopManagedPythonDaemon; const result = await stopDaemon({ cliVersion: args.cliVersion, projectDir: args.projectDir }); writeDaemonStop(io, result); return 0; } } if (args.command === 'status') { const readStatus = deps.readStatus ?? readManagedPythonRuntimeStatus; const doctorRuntime = deps.doctorRuntime ?? doctorManagedPythonRuntime; const status = await readStatus({ cliVersion: args.cliVersion }); const checks = await doctorRuntime({ cliVersion: args.cliVersion }); if (args.json) { writeJson(io, { ...status, checks }); } else { writeStatus(io, status); writeRuntimeChecks(io, checks); } return hasRuntimeStatusFailure(status) ? 1 : 0; } const _exhaustive: never = args; return _exhaustive; } catch (error) { io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); return 1; } }