* docs: add npm managed python runtime design * build: add bundled python runtime wheel builder * build: make local embedding dependencies optional * build: bundle python runtime wheel in cli artifacts * build: track bundled python runtime release artifact * test: verify bundled python runtime wheel * docs: add plan for bundled python runtime wheel * test: cover managed python runtime lifecycle * feat: add managed python runtime installer * feat: add runtime command runner * feat: expose runtime management commands * test: verify managed python runtime commands * docs: add plan for managed python runtime installer * feat: add managed python command helper * feat: use managed runtime for sl query compute * feat: route sl query managed runtime policy * docs: add plan for managed runtime sl query integration * feat: add managed runtime daemon metadata * feat: manage python daemon lifecycle * feat: add runtime daemon start stop commands * fix: verify managed runtime daemon lifecycle * docs: add plan for managed runtime daemon lifecycle * feat: add managed local embeddings config marker * feat: add managed local embeddings daemon helper * feat: use managed runtime for local embedding setup * feat: pass managed runtime policy through setup * docs: add plan for managed local embeddings runtime * feat: read CLI package metadata dynamically * feat: assemble public kaelio ktx npm package * feat: release one public kaelio ktx npm artifact * test: cover public kaelio ktx package invocations * chore: verify public kaelio ktx package artifacts * docs: add plan for public kaelio ktx npm package * test: verify managed runtime in public package smoke * test: finalize managed runtime release smoke * docs: add plan for managed runtime release smoke * test: specify local embeddings release smoke * feat: add local embeddings runtime smoke * chore: register local embeddings smoke * fix: verify local embeddings smoke * fix: restore artifact smoke python env helper * docs: add plan for managed local embeddings release smoke * refactor: share managed runtime install policy parsing * feat: use managed runtime for agent semantic queries * feat: use managed runtime for MCP semantic compute * docs: add plan for managed agent and MCP semantic runtime * feat(cli): add managed daemon HTTP helpers * feat(cli): route local adapters through managed daemon * feat(cli): use managed daemon for ingest helpers * feat(cli): pass managed daemon options to scan * feat(context): pass MCP ingest pull config options * feat(cli): pass managed daemon options to serve ingest * test: verify managed local ingest daemon runtime * docs: add plan for managed local ingest daemon runtime * docs: align managed runtime examples * docs: add plan for managed runtime docs cleanup * test: cover published package runtime smoke commands * test: validate published package smoke outputs * docs: add plan for published package runtime smoke * build: stamp public npm package version * release: add npm public release policy * release: add guarded npm publish script * release: document public npm release handoff * docs: add plan for public npm release handoff * test: cover managed runtime prune in package smoke * docs: document managed runtime prune * docs: add plan for managed runtime prune smoke and docs * chore: encode uv runtime prerequisite policy * fix: clarify missing uv runtime error * docs: document uv runtime prerequisite * docs: add plan for uv runtime prerequisite contract * refactor: limit release artifacts to public package runtime * chore: align release policy with bundled runtime wheel * docs: describe single public runtime artifact surface * test: verify single public runtime artifact contract * docs: add plan for single public runtime artifact cleanup * fix: align local embeddings smoke with public version * docs: add plan for local embeddings smoke public version * release: soft-launch as @kaelio/ktx@0.1.0-rc.0 on next tag Publish target moves to the pre-release version 0.1.0-rc.0 under the next dist-tag so npm install @kaelio/ktx (which resolves to latest) does not pick up the soft-launch build. Users opt in via @kaelio/ktx@next. * Fix release script boundary checks * Remove PostHog from public package bundle
57 KiB
Managed Python Runtime Installer Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Install and inspect the bundled kaelio-ktx Python wheel in a
versioned KTX-managed runtime directory.
Architecture: Add a CLI-owned managed-runtime module that knows where the
bundled wheel asset lives, verifies its checksum, creates a versioned virtual
environment with uv, installs the requested feature set, and writes an
installed-runtime manifest. Add ktx runtime install, status, doctor, and
prune commands that expose this behavior without changing normal
Python-backed commands yet.
Tech Stack: TypeScript, Node 22 ESM, Commander, Vitest, zod, uv, npm
package assets.
Existing status
This plan is based on
docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md.
Plan 1, docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md,
is implemented in this worktree. The implemented source includes
scripts/build-python-runtime-wheel.mjs,
scripts/build-python-runtime-wheel.test.mjs, runtime-wheel handling in
scripts/package-artifacts.mjs, test coverage in
scripts/package-artifacts.test.mjs, and the kaelio-ktx release-policy
entry. The targeted verification command passes:
node --test scripts/build-python-runtime-wheel.test.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs
Expected current result:
# pass 38
# fail 0
No other plan files currently reference the npm-managed Python runtime spec.
This plan implements the next prerequisite:
- Platform-specific managed runtime roots.
- Versioned runtime directories keyed by the CLI package version.
- Runtime asset manifest reading and wheel checksum verification.
uvvirtual environment creation.- Core and
local-embeddingsfeature installation levels. - Installed-runtime manifest writing.
ktx runtime install,ktx runtime status,ktx runtime doctor, andktx runtime prune.
This plan intentionally leaves the following spec requirements for later plans:
- Lazy install from normal commands such as
ktx sl query. ktx runtime startandktx runtime stop.- Daemon state, health checks, reuse, and stale-daemon repair.
- Public npm package renaming from
@ktx/clito@kaelio/ktx.
File structure
- Create
packages/cli/src/managed-python-runtime.ts: pure managed-runtime library for path calculation, asset verification, install/status/doctor, and pruning. - Create
packages/cli/src/managed-python-runtime.test.ts: unit tests for runtime roots, manifest validation, install command shape, status checks, and prune safety. - Create
packages/cli/src/runtime.ts: command runner that formatsinstall,status,doctor, andpruneoutput. - Create
packages/cli/src/runtime.test.ts: command-runner tests with injected managed-runtime dependencies. - Create
packages/cli/src/commands/runtime-commands.ts: Commander registration forktx runtime .... - Modify
packages/cli/src/cli-runtime.ts: add the runtime command runner to CLI dependency injection. - Modify
packages/cli/src/cli-program.ts: pass package info into command registration and register the runtime command group. - Modify
packages/cli/src/index.ts: export runtime command types and the runner for tests and programmatic use. - Modify
packages/cli/src/index.test.ts: assert root help exposesruntimeand Commander routes runtime subcommands correctly.
Task 1: Add failing managed-runtime library tests
Files:
-
Create:
packages/cli/src/managed-python-runtime.test.ts -
Test:
packages/cli/src/managed-python-runtime.test.ts -
Step 1: Write the failing test file
Create packages/cli/src/managed-python-runtime.test.ts with this content:
import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
doctorManagedPythonRuntime,
installManagedPythonRuntime,
managedPythonRuntimeLayout,
pruneManagedPythonRuntimes,
readManagedPythonRuntimeStatus,
verifyRuntimeAsset,
type ManagedPythonRuntimeExec,
} from './managed-python-runtime.js';
async function writeAsset(root: string, contents = 'wheel-bytes') {
const assetDir = join(root, 'assets', 'python');
await mkdir(assetDir, { recursive: true });
const wheelPath = join(assetDir, 'kaelio_ktx-0.1.0-py3-none-any.whl');
await writeFile(wheelPath, contents);
await writeFile(
join(assetDir, 'manifest.json'),
`${JSON.stringify(
{
schemaVersion: 1,
distributionName: 'kaelio-ktx',
normalizedName: 'kaelio_ktx',
version: '0.1.0',
wheel: {
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
sha256: createHash('sha256').update(contents).digest('hex'),
bytes: Buffer.byteLength(contents),
},
},
null,
2,
)}\n`,
);
return { assetDir, wheelPath };
}
describe('managedPythonRuntimeLayout', () => {
it('uses the macOS application-support runtime root', () => {
const layout = managedPythonRuntimeLayout({
cliVersion: '0.2.0',
platform: 'darwin',
env: {},
homeDir: '/Users/alex',
assetDir: '/repo/packages/cli/assets/python',
});
expect(layout.runtimeRoot).toBe('/Users/alex/Library/Application Support/kaelio/ktx/runtime');
expect(layout.versionDir).toBe('/Users/alex/Library/Application Support/kaelio/ktx/runtime/0.2.0');
expect(layout.venvDir).toBe('/Users/alex/Library/Application Support/kaelio/ktx/runtime/0.2.0/.venv');
expect(layout.pythonPath).toBe(
'/Users/alex/Library/Application Support/kaelio/ktx/runtime/0.2.0/.venv/bin/python',
);
expect(layout.daemonPath).toBe(
'/Users/alex/Library/Application Support/kaelio/ktx/runtime/0.2.0/.venv/bin/ktx-daemon',
);
expect(layout.assetManifestPath).toBe('/repo/packages/cli/assets/python/manifest.json');
});
it('honors XDG_DATA_HOME on Linux', () => {
const layout = managedPythonRuntimeLayout({
cliVersion: '0.2.0',
platform: 'linux',
env: { XDG_DATA_HOME: '/var/xdg' },
homeDir: '/home/alex',
assetDir: '/repo/packages/cli/assets/python',
});
expect(layout.runtimeRoot).toBe('/var/xdg/kaelio/ktx/runtime');
expect(layout.versionDir).toBe('/var/xdg/kaelio/ktx/runtime/0.2.0');
});
it('uses LocalAppData on Windows', () => {
const layout = managedPythonRuntimeLayout({
cliVersion: '0.2.0',
platform: 'win32',
env: { LOCALAPPDATA: 'C:\\Users\\Alex\\AppData\\Local' },
homeDir: 'C:\\Users\\Alex',
assetDir: 'C:\\repo\\packages\\cli\\assets\\python',
});
expect(layout.runtimeRoot).toBe('C:\\Users\\Alex\\AppData\\Local/Kaelio/KTX/runtime');
expect(layout.pythonPath).toBe('C:\\Users\\Alex\\AppData\\Local/Kaelio/KTX/runtime/0.2.0/.venv/Scripts/python.exe');
expect(layout.daemonPath).toBe('C:\\Users\\Alex\\AppData\\Local/Kaelio/KTX/runtime/0.2.0/.venv/Scripts/ktx-daemon.exe');
});
});
describe('verifyRuntimeAsset', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-runtime-asset-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('reads the manifest and verifies the wheel checksum', async () => {
const { assetDir, wheelPath } = await writeAsset(tempDir, 'valid-wheel');
const asset = await verifyRuntimeAsset({ assetDir });
expect(asset.manifest.distributionName).toBe('kaelio-ktx');
expect(asset.manifest.normalizedName).toBe('kaelio_ktx');
expect(asset.wheelPath).toBe(wheelPath);
});
it('rejects a wheel whose checksum does not match the manifest', async () => {
const { assetDir, wheelPath } = await writeAsset(tempDir, 'original');
await writeFile(wheelPath, 'tampered');
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(
/Bundled Python runtime wheel checksum mismatch/,
);
});
it('rejects an unsafe wheel filename in the manifest', async () => {
const { assetDir } = await writeAsset(tempDir, 'valid-wheel');
await writeFile(
join(assetDir, 'manifest.json'),
`${JSON.stringify({
schemaVersion: 1,
distributionName: 'kaelio-ktx',
normalizedName: 'kaelio_ktx',
version: '0.1.0',
wheel: {
file: '../kaelio_ktx-0.1.0-py3-none-any.whl',
sha256: 'a'.repeat(64),
bytes: 1,
},
})}\n`,
);
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(/Unsafe runtime wheel filename/);
});
});
describe('installManagedPythonRuntime', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-runtime-install-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('creates a venv, installs the core wheel, and writes a manifest', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const commands: Array<{ command: string; args: string[] }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
commands.push({ command, args });
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
});
const result = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
});
expect(result.status).toBe('installed');
expect(commands).toEqual([
{ command: 'uv', args: ['--version'] },
{ command: 'uv', args: ['venv', result.layout.venvDir] },
{
command: 'uv',
args: ['pip', 'install', '--python', result.layout.pythonPath, result.asset.wheelPath],
},
]);
const manifest = JSON.parse(await readFile(result.layout.manifestPath, 'utf8')) as {
cliVersion: string;
features: string[];
python: { executable: string; daemonExecutable: string };
};
expect(manifest.cliVersion).toBe('0.2.0');
expect(manifest.features).toEqual(['core']);
expect(manifest.python.executable).toBe(result.layout.pythonPath);
expect(manifest.python.daemonExecutable).toBe(result.layout.daemonPath);
});
it('installs the local-embeddings extra when requested', async () => {
const { assetDir } = await writeAsset(tempDir, 'embedding-wheel');
const commands: Array<{ command: string; args: string[] }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
commands.push({ command, args });
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
});
const result = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['local-embeddings'],
exec,
});
expect(commands.at(-1)).toEqual({
command: 'uv',
args: ['pip', 'install', '--python', result.layout.pythonPath, `${result.asset.wheelPath}[local-embeddings]`],
});
const manifest = JSON.parse(await readFile(result.layout.manifestPath, 'utf8')) as { features: string[] };
expect(manifest.features).toEqual(['core', 'local-embeddings']);
});
it('reuses an existing compatible runtime when force is false', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '',
}));
const first = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
});
await mkdir(join(first.layout.venvDir, 'bin'), { recursive: true });
await writeFile(first.layout.pythonPath, '#!/usr/bin/env python\n');
await writeFile(first.layout.daemonPath, '#!/usr/bin/env python\n');
const second = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
});
expect(second.status).toBe('ready');
expect(exec).toHaveBeenCalledTimes(3);
});
it('keeps failed install logs in the versioned runtime directory', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
if (command === 'uv' && args[0] === 'venv') {
throw Object.assign(new Error('uv venv failed'), { stdout: 'creating\n', stderr: 'bad python\n' });
}
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
});
await expect(
installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
}),
).rejects.toThrow(/Python runtime install failed/);
const log = await readFile(join(tempDir, 'runtime', '0.2.0', 'install.log'), 'utf8');
expect(log).toContain('$ uv venv');
expect(log).toContain('bad python');
});
});
describe('readManagedPythonRuntimeStatus', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-runtime-status-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('reports missing before install', async () => {
const status = await readManagedPythonRuntimeStatus({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir: join(tempDir, 'assets', 'python'),
});
expect(status.kind).toBe('missing');
expect(status.detail).toContain('No runtime manifest');
});
it('reports ready when manifest and executables exist', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '',
}));
const install = await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
});
await mkdir(join(install.layout.venvDir, 'bin'), { recursive: true });
await writeFile(install.layout.pythonPath, '#!/usr/bin/env python\n');
await writeFile(install.layout.daemonPath, '#!/usr/bin/env python\n');
const status = await readManagedPythonRuntimeStatus({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
});
expect(status.kind).toBe('ready');
expect(status.manifest?.features).toEqual(['core']);
});
it('reports broken when an executable is missing', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '',
}));
await installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
});
const status = await readManagedPythonRuntimeStatus({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
});
expect(status.kind).toBe('broken');
expect(status.detail).toContain('Missing Python executable');
});
});
describe('doctorManagedPythonRuntime', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-runtime-doctor-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('checks uv, bundled assets, and installed runtime status', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => ({
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
stderr: '',
}));
const checks = await doctorManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
exec,
});
expect(checks.map((check) => [check.id, check.status])).toEqual([
['uv', 'pass'],
['asset', 'pass'],
['runtime', 'fail'],
]);
expect(checks[2]?.fix).toBe('Run: ktx runtime install --yes');
});
});
describe('pruneManagedPythonRuntimes', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-runtime-prune-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('removes stale version directories and keeps the current version', async () => {
const runtimeRoot = join(tempDir, 'runtime');
await mkdir(join(runtimeRoot, '0.1.0'), { recursive: true });
await mkdir(join(runtimeRoot, '0.2.0'), { recursive: true });
await writeFile(join(runtimeRoot, 'README.txt'), 'not a runtime directory\n');
const result = await pruneManagedPythonRuntimes({ cliVersion: '0.2.0', runtimeRoot });
expect(result.removed).toEqual([join(runtimeRoot, '0.1.0')]);
expect(result.kept).toEqual([join(runtimeRoot, '0.2.0')]);
await expect(stat(join(runtimeRoot, '0.1.0'))).rejects.toThrow();
expect(await readdir(runtimeRoot)).toEqual(['0.2.0', 'README.txt']);
});
it('supports dry-run without deleting stale directories', async () => {
const runtimeRoot = join(tempDir, 'runtime');
await mkdir(join(runtimeRoot, '0.1.0'), { recursive: true });
await mkdir(join(runtimeRoot, '0.2.0'), { recursive: true });
const result = await pruneManagedPythonRuntimes({ cliVersion: '0.2.0', runtimeRoot, dryRun: true });
expect(result.removed).toEqual([]);
expect(result.stale).toEqual([join(runtimeRoot, '0.1.0')]);
expect(await readdir(runtimeRoot)).toEqual(['0.1.0', '0.2.0']);
});
});
- Step 2: Run the test to verify it fails
Run:
pnpm --filter @ktx/cli exec vitest run src/managed-python-runtime.test.ts
Expected: FAIL with an import error for ./managed-python-runtime.js.
- Step 3: Commit the failing tests
Run:
git add packages/cli/src/managed-python-runtime.test.ts
git commit -m "test: cover managed python runtime lifecycle"
Task 2: Implement the managed-runtime library
Files:
-
Create:
packages/cli/src/managed-python-runtime.ts -
Test:
packages/cli/src/managed-python-runtime.test.ts -
Step 1: Create the managed-runtime implementation
Create packages/cli/src/managed-python-runtime.ts with this content:
import { createHash } from 'node:crypto';
import { execFile } from 'node:child_process';
import { access, appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { z } from 'zod';
const execFileAsync = promisify(execFile);
export const runtimeFeatureSchema = z.enum(['core', 'local-embeddings']);
export type KtxRuntimeFeature = z.infer<typeof runtimeFeatureSchema>;
const runtimeAssetManifestSchema = z.object({
schemaVersion: z.literal(1),
distributionName: z.literal('kaelio-ktx'),
normalizedName: z.literal('kaelio_ktx'),
version: z.string().min(1),
wheel: z.object({
file: z.string().min(1),
sha256: z.string().regex(/^[a-f0-9]{64}$/),
bytes: z.number().int().nonnegative(),
}),
});
export type KtxRuntimeAssetManifest = z.infer<typeof runtimeAssetManifestSchema>;
const installedRuntimeManifestSchema = z.object({
schemaVersion: z.literal(1),
cliVersion: z.string().min(1),
installedAt: z.string().min(1),
asset: runtimeAssetManifestSchema,
features: z.array(runtimeFeatureSchema).min(1),
python: z.object({
executable: z.string().min(1),
daemonExecutable: z.string().min(1),
}),
installLog: z.string().min(1),
});
export type InstalledKtxRuntimeManifest = z.infer<typeof installedRuntimeManifestSchema>;
export interface ManagedPythonRuntimeLayoutOptions {
cliVersion: string;
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
homeDir?: string;
runtimeRoot?: string;
assetDir?: string;
}
export interface ManagedPythonRuntimeLayout {
cliVersion: string;
runtimeRoot: string;
versionDir: string;
venvDir: string;
manifestPath: string;
installLogPath: string;
assetDir: string;
assetManifestPath: string;
pythonPath: string;
daemonPath: string;
}
export interface ManagedRuntimeAsset {
manifest: KtxRuntimeAssetManifest;
wheelPath: string;
}
export type ManagedPythonRuntimeExec = (
command: string,
args: string[],
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
) => Promise<{ stdout: string; stderr: string }>;
export interface ManagedPythonRuntimeInstallOptions extends ManagedPythonRuntimeLayoutOptions {
features: KtxRuntimeFeature[];
force?: boolean;
exec?: ManagedPythonRuntimeExec;
}
export interface ManagedPythonRuntimeInstallResult {
status: 'ready' | 'installed';
layout: ManagedPythonRuntimeLayout;
asset: ManagedRuntimeAsset;
manifest: InstalledKtxRuntimeManifest;
}
export type ManagedPythonRuntimeStatusKind = 'missing' | 'ready' | 'mismatched' | 'broken';
export interface ManagedPythonRuntimeStatus {
kind: ManagedPythonRuntimeStatusKind;
detail: string;
layout: ManagedPythonRuntimeLayout;
manifest?: InstalledKtxRuntimeManifest;
}
export interface ManagedPythonRuntimeDoctorCheck {
id: 'uv' | 'asset' | 'runtime';
label: string;
status: 'pass' | 'fail';
detail: string;
fix?: string;
}
export interface ManagedPythonRuntimePruneResult {
runtimeRoot: string;
stale: string[];
kept: string[];
removed: string[];
}
function defaultAssetDir(): string {
return fileURLToPath(new URL('../assets/python/', import.meta.url));
}
function runtimeRootFor(input: Required<Pick<ManagedPythonRuntimeLayoutOptions, 'platform' | 'env' | 'homeDir'>>): string {
if (input.platform === 'darwin') {
return join(input.homeDir, 'Library', 'Application Support', 'kaelio', 'ktx', 'runtime');
}
if (input.platform === 'win32') {
return join(input.env.LOCALAPPDATA ?? join(input.homeDir, 'AppData', 'Local'), 'Kaelio', 'KTX', 'runtime');
}
return join(input.env.XDG_DATA_HOME ?? join(input.homeDir, '.local', 'share'), 'kaelio', 'ktx', 'runtime');
}
function executablePath(venvDir: string, platform: NodeJS.Platform, name: string): string {
if (platform === 'win32') {
return join(venvDir, 'Scripts', `${name}.exe`);
}
return join(venvDir, 'bin', name);
}
export function managedPythonRuntimeLayout(options: ManagedPythonRuntimeLayoutOptions): ManagedPythonRuntimeLayout {
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? homedir();
const runtimeRoot = options.runtimeRoot ?? runtimeRootFor({ platform, env, homeDir });
const versionDir = join(runtimeRoot, options.cliVersion);
const venvDir = join(versionDir, '.venv');
const assetDir = options.assetDir ?? defaultAssetDir();
return {
cliVersion: options.cliVersion,
runtimeRoot,
versionDir,
venvDir,
manifestPath: join(versionDir, 'manifest.json'),
installLogPath: join(versionDir, 'install.log'),
assetDir,
assetManifestPath: join(assetDir, 'manifest.json'),
pythonPath: executablePath(venvDir, platform, 'python'),
daemonPath: executablePath(venvDir, platform, 'ktx-daemon'),
};
}
async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
function assertSafeWheelFilename(file: string): void {
if (file !== basename(file) || file.includes('/') || file.includes('\\')) {
throw new Error(`Unsafe runtime wheel filename in bundled manifest: ${file}`);
}
}
async function readJsonFile(path: string): Promise<unknown> {
return JSON.parse(await readFile(path, 'utf8')) as unknown;
}
export async function verifyRuntimeAsset(input: { assetDir: string }): Promise<ManagedRuntimeAsset> {
const manifestPath = join(input.assetDir, 'manifest.json');
const manifest = runtimeAssetManifestSchema.parse(await readJsonFile(manifestPath));
assertSafeWheelFilename(manifest.wheel.file);
const wheelPath = join(input.assetDir, manifest.wheel.file);
const wheel = await readFile(wheelPath);
const sha256 = createHash('sha256').update(wheel).digest('hex');
if (sha256 !== manifest.wheel.sha256 || wheel.byteLength !== manifest.wheel.bytes) {
throw new Error(`Bundled Python runtime wheel checksum mismatch: ${wheelPath}`);
}
return { manifest, wheelPath };
}
function normalizeFeatures(features: KtxRuntimeFeature[]): KtxRuntimeFeature[] {
const requested = new Set<KtxRuntimeFeature>(['core', ...features]);
return runtimeFeatureSchema.options.filter((feature) => requested.has(feature));
}
async function readInstalledManifest(path: string): Promise<InstalledKtxRuntimeManifest | undefined> {
if (!(await pathExists(path))) {
return undefined;
}
return installedRuntimeManifestSchema.parse(await readJsonFile(path));
}
function hasFeatures(manifest: InstalledKtxRuntimeManifest, features: KtxRuntimeFeature[]): boolean {
return normalizeFeatures(features).every((feature) => manifest.features.includes(feature));
}
async function defaultExec(
command: string,
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
): Promise<{ stdout: string; stderr: string }> {
const result = await execFileAsync(command, args, {
cwd: options.cwd,
env: options.env,
encoding: 'utf8',
maxBuffer: 1024 * 1024 * 20,
});
return { stdout: result.stdout, stderr: result.stderr };
}
function errorOutput(error: unknown): { stdout: string; stderr: string } {
const value = error as { stdout?: unknown; stderr?: unknown };
return {
stdout: typeof value.stdout === 'string' ? value.stdout : '',
stderr: typeof value.stderr === 'string' ? value.stderr : '',
};
}
async function runLogged(input: {
exec: ManagedPythonRuntimeExec;
logPath: string;
command: string;
args: string[];
cwd?: string;
}): Promise<{ stdout: string; stderr: string }> {
await appendFile(input.logPath, `$ ${input.command} ${input.args.join(' ')}\n`);
try {
const result = await input.exec(input.command, input.args, { cwd: input.cwd });
if (result.stdout) {
await appendFile(input.logPath, result.stdout.endsWith('\n') ? result.stdout : `${result.stdout}\n`);
}
if (result.stderr) {
await appendFile(input.logPath, result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
}
return result;
} catch (error) {
const output = errorOutput(error);
if (output.stdout) {
await appendFile(input.logPath, output.stdout.endsWith('\n') ? output.stdout : `${output.stdout}\n`);
}
if (output.stderr) {
await appendFile(input.logPath, output.stderr.endsWith('\n') ? output.stderr : `${output.stderr}\n`);
}
throw new Error(`Python runtime install failed. Install log: ${input.logPath}`);
}
}
async function ensureUv(exec: ManagedPythonRuntimeExec): Promise<string> {
try {
const result = await exec('uv', ['--version']);
return result.stdout.trim() || 'uv available';
} catch {
throw new Error(
'uv is required to install the KTX Python runtime. Install uv and retry: ktx runtime install --yes',
);
}
}
export async function installManagedPythonRuntime(
options: ManagedPythonRuntimeInstallOptions,
): Promise<ManagedPythonRuntimeInstallResult> {
const layout = managedPythonRuntimeLayout(options);
const exec = options.exec ?? defaultExec;
const features = normalizeFeatures(options.features);
const asset = await verifyRuntimeAsset({ assetDir: layout.assetDir });
const existing = await readInstalledManifest(layout.manifestPath);
if (
options.force !== true &&
existing &&
existing.cliVersion === options.cliVersion &&
existing.asset.wheel.sha256 === asset.manifest.wheel.sha256 &&
hasFeatures(existing, features) &&
(await pathExists(existing.python.executable)) &&
(await pathExists(existing.python.daemonExecutable))
) {
return { status: 'ready', layout, asset, manifest: existing };
}
await rm(layout.versionDir, { recursive: true, force: true });
await mkdir(layout.versionDir, { recursive: true });
await writeFile(layout.installLogPath, '');
await ensureUv(exec);
await runLogged({ exec, logPath: layout.installLogPath, command: 'uv', args: ['venv', layout.venvDir] });
const wheelSpec = features.includes('local-embeddings') ? `${asset.wheelPath}[local-embeddings]` : asset.wheelPath;
await runLogged({
exec,
logPath: layout.installLogPath,
command: 'uv',
args: ['pip', 'install', '--python', layout.pythonPath, wheelSpec],
});
const manifest: InstalledKtxRuntimeManifest = {
schemaVersion: 1,
cliVersion: options.cliVersion,
installedAt: new Date().toISOString(),
asset: asset.manifest,
features,
python: {
executable: layout.pythonPath,
daemonExecutable: layout.daemonPath,
},
installLog: layout.installLogPath,
};
await writeFile(layout.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
return { status: 'installed', layout, asset, manifest };
}
export async function readManagedPythonRuntimeStatus(
options: ManagedPythonRuntimeLayoutOptions,
): Promise<ManagedPythonRuntimeStatus> {
const layout = managedPythonRuntimeLayout(options);
let manifest: InstalledKtxRuntimeManifest | undefined;
try {
manifest = await readInstalledManifest(layout.manifestPath);
} catch (error) {
return {
kind: 'broken',
detail: `Runtime manifest is invalid: ${error instanceof Error ? error.message : String(error)}`,
layout,
};
}
if (!manifest) {
return { kind: 'missing', detail: `No runtime manifest at ${layout.manifestPath}`, layout };
}
if (manifest.cliVersion !== options.cliVersion) {
return {
kind: 'mismatched',
detail: `Runtime is for CLI ${manifest.cliVersion}, current CLI is ${options.cliVersion}`,
layout,
manifest,
};
}
if (!(await pathExists(manifest.python.executable))) {
return { kind: 'broken', detail: `Missing Python executable: ${manifest.python.executable}`, layout, manifest };
}
if (!(await pathExists(manifest.python.daemonExecutable))) {
return { kind: 'broken', detail: `Missing ktx-daemon executable: ${manifest.python.daemonExecutable}`, layout, manifest };
}
return { kind: 'ready', detail: `Runtime ready at ${layout.versionDir}`, layout, manifest };
}
function check(status: ManagedPythonRuntimeDoctorCheck['status'], input: Omit<ManagedPythonRuntimeDoctorCheck, 'status'>) {
return { status, ...input };
}
export async function doctorManagedPythonRuntime(
options: ManagedPythonRuntimeLayoutOptions & { exec?: ManagedPythonRuntimeExec },
): Promise<ManagedPythonRuntimeDoctorCheck[]> {
const exec = options.exec ?? defaultExec;
const checks: ManagedPythonRuntimeDoctorCheck[] = [];
try {
const version = await ensureUv(exec);
checks.push(check('pass', { id: 'uv', label: 'uv', detail: version }));
} catch (error) {
checks.push(
check('fail', {
id: 'uv',
label: 'uv',
detail: error instanceof Error ? error.message : String(error),
fix: 'Install uv, then run: ktx runtime install --yes',
}),
);
}
try {
const asset = await verifyRuntimeAsset({ assetDir: managedPythonRuntimeLayout(options).assetDir });
checks.push(check('pass', { id: 'asset', label: 'Bundled Python wheel', detail: asset.wheelPath }));
} catch (error) {
checks.push(
check('fail', {
id: 'asset',
label: 'Bundled Python wheel',
detail: error instanceof Error ? error.message : String(error),
fix: 'Run: pnpm run artifacts:check',
}),
);
}
const status = await readManagedPythonRuntimeStatus(options);
checks.push(
check(status.kind === 'ready' ? 'pass' : 'fail', {
id: 'runtime',
label: 'Managed Python runtime',
detail: status.detail,
...(status.kind === 'ready' ? {} : { fix: 'Run: ktx runtime install --yes' }),
}),
);
return checks;
}
export async function pruneManagedPythonRuntimes(options: {
cliVersion: string;
runtimeRoot: string;
dryRun?: boolean;
}): Promise<ManagedPythonRuntimePruneResult> {
if (!(await pathExists(options.runtimeRoot))) {
return { runtimeRoot: options.runtimeRoot, stale: [], kept: [], removed: [] };
}
const entries = await readdir(options.runtimeRoot);
const stale: string[] = [];
const kept: string[] = [];
for (const entry of entries) {
const path = join(options.runtimeRoot, entry);
const info = await stat(path);
if (!info.isDirectory()) {
continue;
}
if (entry === options.cliVersion) {
kept.push(path);
} else {
stale.push(path);
}
}
const removed: string[] = [];
if (options.dryRun !== true) {
for (const path of stale) {
await rm(path, { recursive: true, force: true });
removed.push(path);
}
}
return { runtimeRoot: options.runtimeRoot, stale, kept, removed };
}
- Step 2: Run the managed-runtime tests
Run:
pnpm --filter @ktx/cli exec vitest run src/managed-python-runtime.test.ts
Expected: PASS.
- Step 3: Run the CLI type checker
Run:
pnpm --filter @ktx/cli run type-check
Expected: PASS.
- Step 4: Commit the implementation
Run:
git add packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts
git commit -m "feat: add managed python runtime installer"
Task 3: Add the runtime command runner
Files:
-
Create:
packages/cli/src/runtime.ts -
Create:
packages/cli/src/runtime.test.ts -
Test:
packages/cli/src/runtime.test.ts -
Step 1: Write the failing command-runner tests
Create packages/cli/src/runtime.test.ts with this content:
import { describe, expect, it, vi } from 'vitest';
import { runKtxRuntime, type KtxRuntimeDeps } from './runtime.js';
function makeIo() {
let stdout = '';
let stderr = '';
return {
io: {
stdout: {
write: (chunk: string) => {
stdout += chunk;
},
},
stderr: {
write: (chunk: string) => {
stderr += chunk;
},
},
},
stdout: () => stdout,
stderr: () => stderr,
};
}
describe('runKtxRuntime', () => {
it('installs the requested runtime feature and prints the manifest path', async () => {
const io = makeIo();
const deps: KtxRuntimeDeps = {
installRuntime: vi.fn(async () => ({
status: 'installed',
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',
},
asset: {
wheelPath: '/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl',
manifest: {
schemaVersion: 1,
distributionName: 'kaelio-ktx',
normalizedName: 'kaelio_ktx',
version: '0.1.0',
wheel: {
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
sha256: 'a'.repeat(64),
bytes: 10,
},
},
},
manifest: {
schemaVersion: 1,
cliVersion: '0.2.0',
installedAt: '2026-05-11T00:00:00.000Z',
asset: {
schemaVersion: 1,
distributionName: 'kaelio-ktx',
normalizedName: 'kaelio_ktx',
version: '0.1.0',
wheel: {
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
sha256: 'a'.repeat(64),
bytes: 10,
},
},
features: ['core', 'local-embeddings'],
python: {
executable: '/runtime/0.2.0/.venv/bin/python',
daemonExecutable: '/runtime/0.2.0/.venv/bin/ktx-daemon',
},
installLog: '/runtime/0.2.0/install.log',
},
})),
};
await expect(
runKtxRuntime(
{ command: 'install', cliVersion: '0.2.0', feature: 'local-embeddings', force: true },
io.io,
deps,
),
).resolves.toBe(0);
expect(deps.installRuntime).toHaveBeenCalledWith({
cliVersion: '0.2.0',
features: ['local-embeddings'],
force: true,
});
expect(io.stdout()).toContain('Installed KTX Python runtime');
expect(io.stdout()).toContain('features: core, local-embeddings');
expect(io.stdout()).toContain('manifest: /runtime/0.2.0/manifest.json');
expect(io.stderr()).toBe('');
});
it('prints runtime status as JSON', async () => {
const io = makeIo();
const deps: KtxRuntimeDeps = {
readStatus: vi.fn(async () => ({
kind: 'missing',
detail: 'No runtime manifest at /runtime/0.2.0/manifest.json',
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',
},
})),
};
await expect(runKtxRuntime({ command: 'status', cliVersion: '0.2.0', json: true }, io.io, deps)).resolves.toBe(0);
expect(JSON.parse(io.stdout())).toMatchObject({
kind: 'missing',
detail: 'No runtime manifest at /runtime/0.2.0/manifest.json',
layout: { runtimeRoot: '/runtime' },
});
});
it('returns failure for doctor when any check fails', async () => {
const io = makeIo();
const deps: KtxRuntimeDeps = {
doctorRuntime: vi.fn(async () => [
{ id: 'uv', label: 'uv', status: 'pass', detail: 'uv 0.9.5' },
{
id: 'runtime',
label: 'Managed Python runtime',
status: 'fail',
detail: 'No runtime manifest',
fix: 'Run: ktx runtime install --yes',
},
]),
};
await expect(runKtxRuntime({ command: 'doctor', cliVersion: '0.2.0', json: false }, io.io, deps)).resolves.toBe(1);
expect(io.stdout()).toContain('PASS uv: uv 0.9.5');
expect(io.stdout()).toContain('FAIL Managed Python runtime: No runtime manifest');
expect(io.stdout()).toContain('Fix: Run: ktx runtime install --yes');
});
it('requires --yes before pruning stale runtime directories', async () => {
const io = makeIo();
const deps: KtxRuntimeDeps = {
pruneRuntime: vi.fn(async () => {
throw new Error('should not prune without --yes');
}),
};
await expect(runKtxRuntime({ command: 'prune', cliVersion: '0.2.0', dryRun: false, yes: false }, io.io, deps))
.resolves.toBe(1);
expect(io.stderr()).toContain('Refusing to prune without --yes');
expect(deps.pruneRuntime).not.toHaveBeenCalled();
});
it('prints stale directories during prune dry-run', async () => {
const io = makeIo();
const deps: KtxRuntimeDeps = {
readStatus: vi.fn(async () => ({
kind: 'missing',
detail: 'No runtime manifest at /runtime/0.2.0/manifest.json',
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',
},
})),
pruneRuntime: vi.fn(async () => ({
runtimeRoot: '/runtime',
stale: ['/runtime/0.1.0'],
kept: ['/runtime/0.2.0'],
removed: [],
})),
};
await expect(runKtxRuntime({ command: 'prune', cliVersion: '0.2.0', dryRun: true, yes: false }, io.io, deps))
.resolves.toBe(0);
expect(io.stdout()).toContain('Stale KTX Python runtimes');
expect(io.stdout()).toContain('/runtime/0.1.0');
});
});
- Step 2: Run the test to verify it fails
Run:
pnpm --filter @ktx/cli exec vitest run src/runtime.test.ts
Expected: FAIL with an import error for ./runtime.js.
- Step 3: Create the command runner
Create packages/cli/src/runtime.ts with this content:
import {
doctorManagedPythonRuntime,
installManagedPythonRuntime,
pruneManagedPythonRuntimes,
readManagedPythonRuntimeStatus,
type KtxRuntimeFeature,
type ManagedPythonRuntimeDoctorCheck,
type ManagedPythonRuntimeInstallOptions,
type ManagedPythonRuntimeInstallResult,
type ManagedPythonRuntimeLayoutOptions,
type ManagedPythonRuntimePruneResult,
type ManagedPythonRuntimeStatus,
} from './managed-python-runtime.js';
import type { KtxCliIo } from './cli-runtime.js';
export type KtxRuntimeArgs =
| { command: 'install'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean }
| { 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>;
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
doctorRuntime?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeDoctorCheck[]>;
pruneRuntime?: (options: { cliVersion: string; runtimeRoot: string; dryRun?: boolean }) => Promise<ManagedPythonRuntimePruneResult>;
}
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 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 writeDoctor(io: KtxCliIo, checks: ManagedPythonRuntimeDoctorCheck[]): void {
io.stdout.write('KTX Python runtime doctor\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 writePrune(io: KtxCliIo, result: ManagedPythonRuntimePruneResult, dryRun: boolean): void {
if (result.stale.length === 0) {
io.stdout.write(`No stale KTX Python runtimes found under ${result.runtimeRoot}\n`);
return;
}
io.stdout.write(dryRun ? 'Stale KTX Python runtimes\n' : 'Removed stale KTX Python runtimes\n');
for (const path of dryRun ? result.stale : result.removed) {
io.stdout.write(`${path}\n`);
}
}
export async function runKtxRuntime(
args: KtxRuntimeArgs,
io: KtxCliIo = process,
deps: KtxRuntimeDeps = {},
): Promise<number> {
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 === 'status') {
const readStatus = deps.readStatus ?? readManagedPythonRuntimeStatus;
const status = await readStatus({ cliVersion: args.cliVersion });
if (args.json) {
writeJson(io, status);
} else {
writeStatus(io, status);
}
return 0;
}
if (args.command === 'doctor') {
const doctorRuntime = deps.doctorRuntime ?? doctorManagedPythonRuntime;
const checks = await doctorRuntime({ cliVersion: args.cliVersion });
if (args.json) {
writeJson(io, { checks });
} else {
writeDoctor(io, checks);
}
return checks.some((check) => check.status === 'fail') ? 1 : 0;
}
if (!args.dryRun && !args.yes) {
io.stderr.write('Refusing to prune without --yes. Preview with: ktx runtime prune --dry-run\n');
return 1;
}
const status = await (deps.readStatus ?? readManagedPythonRuntimeStatus)({ cliVersion: args.cliVersion });
const pruneRuntime = deps.pruneRuntime ?? pruneManagedPythonRuntimes;
const result = await pruneRuntime({
cliVersion: args.cliVersion,
runtimeRoot: status.layout.runtimeRoot,
dryRun: args.dryRun,
});
writePrune(io, result, args.dryRun);
return 0;
} catch (error) {
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
return 1;
}
}
- Step 4: Run the command-runner tests
Run:
pnpm --filter @ktx/cli exec vitest run src/runtime.test.ts
Expected: PASS.
- Step 5: Commit the command runner
Run:
git add packages/cli/src/runtime.ts packages/cli/src/runtime.test.ts
git commit -m "feat: add runtime command runner"
Task 4: Register ktx runtime commands
Files:
-
Create:
packages/cli/src/commands/runtime-commands.ts -
Modify:
packages/cli/src/cli-runtime.ts -
Modify:
packages/cli/src/cli-program.ts -
Modify:
packages/cli/src/index.ts -
Modify:
packages/cli/src/index.test.ts -
Test:
packages/cli/src/index.test.ts -
Step 1: Create the runtime command registration
Create packages/cli/src/commands/runtime-commands.ts with this content:
import { type Command, Option } from '@commander-js/extra-typings';
import type { KtxCliCommandContext } from '../cli-program.js';
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');
async function runRuntimeArgs(context: KtxCliCommandContext, args: KtxRuntimeArgs): Promise<void> {
const runner = context.deps.runtime ?? (await import('../runtime.js')).runKtxRuntime;
context.setExitCode(await runner(args, context.io));
}
export function registerRuntimeCommands(program: Command, context: KtxCliCommandContext): void {
const runtime = program
.command('runtime')
.description('Install, inspect, and prune the KTX-managed Python runtime')
.showHelpAfterError();
runtime
.command('install')
.description('Install the bundled Python runtime wheel into the managed runtime')
.addOption(runtimeFeatureOption)
.option('--force', 'Reinstall even when the runtime already looks ready', false)
.action(async (options: { feature: RuntimeFeature; force?: boolean }) => {
await runRuntimeArgs(context, {
command: 'install',
cliVersion: context.packageInfo.version,
feature: options.feature,
force: options.force === true,
});
});
runtime
.command('status')
.description('Show managed Python runtime status')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }) => {
await runRuntimeArgs(context, {
command: 'status',
cliVersion: context.packageInfo.version,
json: options.json === true,
});
});
runtime
.command('doctor')
.description('Check managed Python runtime prerequisites and installation')
.option('--json', 'Print JSON output', false)
.action(async (options: { json?: boolean }) => {
await runRuntimeArgs(context, {
command: 'doctor',
cliVersion: context.packageInfo.version,
json: options.json === true,
});
});
runtime
.command('prune')
.description('Remove stale managed Python runtimes for older CLI versions')
.option('--dry-run', 'List stale runtimes without deleting them', false)
.option('--yes', 'Confirm deletion of stale runtime directories', false)
.action(async (options: { dryRun?: boolean; yes?: boolean }) => {
await runRuntimeArgs(context, {
command: 'prune',
cliVersion: context.packageInfo.version,
dryRun: options.dryRun === true,
yes: options.yes === true,
});
});
}
- Step 2: Add runtime dependency injection to CLI runtime
In packages/cli/src/cli-runtime.ts, add this import after the existing
KtxPublicIngestArgs import:
import type { KtxRuntimeArgs } from './runtime.js';
Then add this property to KtxCliDeps after publicIngest:
runtime?: (args: KtxRuntimeArgs, io: KtxCliIo) => Promise<number>;
- Step 3: Add package info to command context and register the command
In packages/cli/src/cli-program.ts, add this import after the
registerPublicIngestCommands import:
import { registerRuntimeCommands } from './commands/runtime-commands.js';
Add this property to KtxCliCommandContext after deps:
packageInfo: KtxCliPackageInfo;
Add this property to the context object inside runCommanderKtxCli after
deps:
packageInfo: info,
Register the runtime commands after registerSlCommands(program, context);:
registerRuntimeCommands(program, context);
profileMark('commander:register-runtime');
- Step 4: Export runtime APIs from the CLI package
In packages/cli/src/index.ts, add this export after the setup exports:
export { runKtxRuntime, type KtxRuntimeArgs, type KtxRuntimeDeps } from './runtime.js';
- Step 5: Update root help and routing tests
In packages/cli/src/index.test.ts, update the root help command list in the
test named prints the May 6 public command surface in root help from:
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'serve', 'status']) {
to:
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'runtime', 'serve', 'status']) {
Then add this test after the root help test:
it('routes runtime management commands with the CLI package version', async () => {
const runtime = vi.fn(async () => 0);
const installIo = makeIo();
const statusIo = makeIo();
const doctorIo = makeIo();
const pruneIo = makeIo();
await expect(
runKtxCli(['runtime', 'install', '--feature', 'local-embeddings', '--force'], installIo.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);
expect(runtime).toHaveBeenNthCalledWith(
1,
{
command: 'install',
cliVersion: '0.0.0-private',
feature: 'local-embeddings',
force: true,
},
installIo.io,
);
expect(runtime).toHaveBeenNthCalledWith(
2,
{
command: 'status',
cliVersion: '0.0.0-private',
json: true,
},
statusIo.io,
);
expect(runtime).toHaveBeenNthCalledWith(
3,
{
command: 'doctor',
cliVersion: '0.0.0-private',
json: false,
},
doctorIo.io,
);
expect(runtime).toHaveBeenNthCalledWith(
4,
{
command: 'prune',
cliVersion: '0.0.0-private',
dryRun: true,
yes: false,
},
pruneIo.io,
);
});
- Step 6: Run the CLI routing tests
Run:
pnpm --filter @ktx/cli exec vitest run src/index.test.ts
Expected: PASS.
- Step 7: Commit the command registration
Run:
git add packages/cli/src/commands/runtime-commands.ts packages/cli/src/cli-runtime.ts packages/cli/src/cli-program.ts packages/cli/src/index.ts packages/cli/src/index.test.ts
git commit -m "feat: expose runtime management commands"
Task 5: Verify the managed runtime installer end to end
Files:
-
Verify:
packages/cli/src/managed-python-runtime.ts -
Verify:
packages/cli/src/runtime.ts -
Verify:
packages/cli/src/commands/runtime-commands.ts -
Verify:
packages/cli/src/index.test.ts -
Step 1: Run focused Vitest coverage
Run:
pnpm --filter @ktx/cli exec vitest run src/managed-python-runtime.test.ts src/runtime.test.ts src/index.test.ts
Expected: PASS.
- Step 2: Run the CLI type checker
Run:
pnpm --filter @ktx/cli run type-check
Expected: PASS.
- Step 3: Build CLI artifacts so bundled Python assets exist
Run:
pnpm run artifacts:check
Expected: PASS. The command must leave these generated files:
packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl
packages/cli/assets/python/manifest.json
- Step 4: Smoke the status command without installing
Run:
pnpm --filter @ktx/cli run build
node packages/cli/dist/bin.js runtime status --json
Expected: PASS with JSON containing "kind": "missing" or "kind": "ready".
Both are valid because a developer machine might already have a runtime for
the current CLI version.
- Step 5: Smoke the doctor command
Run:
node packages/cli/dist/bin.js runtime doctor
Expected: command exits 0 if the runtime is ready and exits 1 if the
runtime is missing. In both cases, stdout must include:
KTX Python runtime doctor
- Step 6: Run pre-commit for changed files
Run:
uv run pre-commit run --files packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts packages/cli/src/runtime.ts packages/cli/src/runtime.test.ts packages/cli/src/commands/runtime-commands.ts packages/cli/src/cli-runtime.ts packages/cli/src/cli-program.ts packages/cli/src/index.ts packages/cli/src/index.test.ts
Expected: PASS. If pre-commit cannot run because this checkout lacks a compatible pre-commit environment, record the exact failure and keep the Vitest, type-check, and build results.
- Step 7: Commit final verification fixes
If verification required edits, run:
git add packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts packages/cli/src/runtime.ts packages/cli/src/runtime.test.ts packages/cli/src/commands/runtime-commands.ts packages/cli/src/cli-runtime.ts packages/cli/src/cli-program.ts packages/cli/src/index.ts packages/cli/src/index.test.ts
git commit -m "test: verify managed python runtime commands"
If no verification edits were needed, do not create an empty commit.
Self-review
Spec coverage:
- Covers runtime root selection for macOS, Linux, and Windows.
- Covers versioned runtime directories based on the CLI package version.
- Covers locating
uv, creating a virtual environment, installing the bundled wheel, and writing a runtime manifest. - Covers feature levels by installing
coreby default andlocal-embeddingsthrough the wheel extra when requested. - Covers focused errors for missing
uv, failed install logs, status output, doctor output, and stale runtime pruning. - Leaves lazy install from normal commands, daemon start/stop/reuse, and public npm renaming for later plans.
Placeholder scan:
- The plan contains no placeholder markers and no unspecified implementation steps.
Type and name consistency:
- Runtime feature strings are consistently
coreandlocal-embeddings. - Runtime command args use
cliVersion,feature,force,json,dryRun, andyesconsistently across command registration, tests, and runner code. - Asset manifest names are consistently
kaelio-ktx,kaelio_ktx, andmanifest.json.