import { execFile } from 'node:child_process'; import { createHash } from 'node:crypto'; import { access, appendFile, mkdir, readFile, rm, 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; 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; 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; 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; daemonStatePath: string; daemonStdoutPath: string; daemonStderrPath: 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 const MISSING_UV_RUNTIME_INSTALL_MESSAGE = 'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx dev runtime install --yes'; function defaultAssetDir(): string { return fileURLToPath(new URL('../assets/python/', import.meta.url)); } function runtimeRootFor(input: Required>): string { if (input.env.KTX_RUNTIME_ROOT) { return input.env.KTX_RUNTIME_ROOT; } 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'), daemonStatePath: join(versionDir, 'daemon.json'), daemonStdoutPath: join(versionDir, 'daemon.stdout.log'), daemonStderrPath: join(versionDir, 'daemon.stderr.log'), }; } async function pathExists(path: string): Promise { 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 { return JSON.parse(await readFile(path, 'utf8')) as unknown; } function isErrnoException(error: unknown, code: string): boolean { return typeof error === 'object' && error !== null && 'code' in error && error.code === code; } export async function verifyRuntimeAsset(input: { assetDir: string }): Promise { const manifestPath = join(input.assetDir, 'manifest.json'); let manifestData: unknown; try { manifestData = await readJsonFile(manifestPath); } catch (error) { if (isErrnoException(error, 'ENOENT')) { throw new Error( [ `Missing bundled Python runtime manifest: ${manifestPath}`, 'In a source checkout, build the local runtime assets with: pnpm run artifacts:build', 'Then retry the runtime-backed KTX command.', ].join('\n'), ); } throw error; } const manifest = runtimeAssetManifestSchema.parse(manifestData); 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(['core', ...features]); return runtimeFeatureSchema.options.filter((feature) => requested.has(feature)); } async function readInstalledManifest(path: string): Promise { 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; env?: NodeJS.ProcessEnv; }): 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, env: input.env }); 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}`); } } function managedRuntimeUvEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return { ...baseEnv, UV_NO_CONFIG: '1' }; } async function ensureUv(exec: ManagedPythonRuntimeExec, env?: NodeJS.ProcessEnv): Promise { try { const result = await exec('uv', ['--version'], { env }); return result.stdout.trim() || 'uv available'; } catch { throw new Error(MISSING_UV_RUNTIME_INSTALL_MESSAGE); } } export async function installManagedPythonRuntime( options: ManagedPythonRuntimeInstallOptions, ): Promise { const layout = managedPythonRuntimeLayout(options); const exec = options.exec ?? defaultExec; const features = normalizeFeatures(options.features); const asset = await verifyRuntimeAsset({ assetDir: layout.assetDir }); const uvEnv = managedRuntimeUvEnv(options.env ?? process.env); 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, uvEnv); await runLogged({ exec, logPath: layout.installLogPath, command: 'uv', args: ['venv', layout.venvDir], env: uvEnv, }); 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], env: uvEnv, }); 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 { 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 { return { status, ...input }; } export async function doctorManagedPythonRuntime( options: ManagedPythonRuntimeLayoutOptions & { exec?: ManagedPythonRuntimeExec }, ): Promise { const exec = options.exec ?? defaultExec; const checks: ManagedPythonRuntimeDoctorCheck[] = []; try { const version = await ensureUv(exec, managedRuntimeUvEnv(options.env ?? process.env)); 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, make sure it is on PATH, and run: ktx dev 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 dev runtime install --yes' }), }), ); return checks; }