From eaaabb361e57913e745d3793d07468abd381dc39 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 12:28:24 +0200 Subject: [PATCH] fix(cli): clean up dev runtime commands (#59) --- README.md | 2 - .../content/docs/cli-reference/ktx-dev.mdx | 11 +-- examples/package-artifacts/README.md | 4 +- packages/cli/src/commands/runtime-commands.ts | 18 +--- packages/cli/src/dev.test.ts | 7 +- packages/cli/src/index.test.ts | 16 +--- .../cli/src/managed-python-runtime.test.ts | 41 +-------- packages/cli/src/managed-python-runtime.ts | 42 +-------- packages/cli/src/runtime.test.ts | 88 ++++++++++++------- packages/cli/src/runtime.ts | 54 +++++------- scripts/examples-docs.test.mjs | 6 +- scripts/local-embeddings-runtime-smoke.mjs | 14 ++- scripts/package-artifacts.mjs | 33 +++---- scripts/package-artifacts.test.mjs | 9 +- 14 files changed, 127 insertions(+), 218 deletions(-) diff --git a/README.md b/README.md index 1cd20080..b52a31f6 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,6 @@ ktx dev runtime install --yes ktx dev runtime status ktx dev runtime start ktx dev runtime stop -ktx dev runtime prune --dry-run -ktx dev runtime prune --yes ``` The release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx` diff --git a/docs-site/content/docs/cli-reference/ktx-dev.mdx b/docs-site/content/docs/cli-reference/ktx-dev.mdx index eea02562..e00a4585 100644 --- a/docs-site/content/docs/cli-reference/ktx-dev.mdx +++ b/docs-site/content/docs/cli-reference/ktx-dev.mdx @@ -16,7 +16,7 @@ ktx dev [options] | Subcommand | Description | |-----------|-------------| | `init [directory]` | Initialize a Git-backed KTX project directory | -| `runtime` | Install, start, stop, inspect, and prune the KTX-managed Python runtime | +| `runtime` | Install, start, stop, and inspect the KTX-managed Python runtime | ## `dev init` @@ -27,15 +27,14 @@ ktx dev [options] ## `dev runtime` -`ktx dev runtime` supports `install`, `start`, `stop`, `status`, and `prune`. +`ktx dev runtime` supports `install`, `start`, `stop`, and `status`. | Flag | Description | Default | |------|-------------|---------| -| `--feature ` | Runtime feature level for `install`, `start`, and `status` (`core` or `local-embeddings`) | `core` | +| `--feature ` | Runtime feature level for `install` and `start` (`core` or `local-embeddings`) | `core` | | `--json` | Print JSON output for `status` | `false` | -| `--yes` | Confirm runtime install or prune actions where supported | `false` | +| `--yes` | Confirm runtime install actions where supported | `false` | | `--force` | Reinstall or restart where supported | `false` | -| `--dry-run` | Preview runtime pruning without removing files | `false` | ## Examples @@ -48,8 +47,6 @@ ktx dev runtime install --yes ktx dev runtime status ktx dev runtime start ktx dev runtime stop -ktx dev runtime prune --dry-run -ktx dev runtime prune --yes ``` ## Common errors diff --git a/examples/package-artifacts/README.md b/examples/package-artifacts/README.md index 8c92f84e..22ecaf92 100644 --- a/examples/package-artifacts/README.md +++ b/examples/package-artifacts/README.md @@ -14,9 +14,7 @@ generated local project. The managed Python runtime smoke requires `uv` on `PATH`, isolates `KTX_RUNTIME_ROOT`, verifies `ktx dev runtime status`, runs `ktx sl query --yes` to install the core runtime from the bundled wheel, checks `ktx dev runtime status`, -starts and reuses the managed daemon, stops it, previews a stale runtime with -`ktx dev runtime prune --dry-run`, verifies confirmation is required, and removes -the stale runtime with `ktx dev runtime prune --yes`. +starts and reuses the managed daemon, and stops it. The artifact manifest contains the public `@kaelio/ktx` npm tarball and the bundled `kaelio-ktx` runtime wheel. The smoke does not install standalone diff --git a/packages/cli/src/commands/runtime-commands.ts b/packages/cli/src/commands/runtime-commands.ts index b57eae86..cf0abb42 100644 --- a/packages/cli/src/commands/runtime-commands.ts +++ b/packages/cli/src/commands/runtime-commands.ts @@ -18,7 +18,7 @@ async function runRuntimeArgs(context: KtxCliCommandContext, args: KtxRuntimeArg export function registerRuntimeCommands(program: Command, context: KtxCliCommandContext): void { const runtime = program .command('runtime') - .description('Install, inspect, and prune the KTX-managed Python runtime') + .description('Install, start, stop, and inspect the KTX-managed Python runtime') .showHelpAfterError(); runtime @@ -64,7 +64,7 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand runtime .command('status') - .description('Show managed Python runtime status') + .description('Show managed Python runtime status and readiness checks') .option('--json', 'Print JSON output', false) .action(async (options: { json?: boolean }) => { await runRuntimeArgs(context, { @@ -73,18 +73,4 @@ export function registerRuntimeCommands(program: Command, context: KtxCliCommand 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, - }); - }); } diff --git a/packages/cli/src/dev.test.ts b/packages/cli/src/dev.test.ts index 5aca4201..fe75d1af 100644 --- a/packages/cli/src/dev.test.ts +++ b/packages/cli/src/dev.test.ts @@ -106,6 +106,7 @@ describe('dev Commander tree', () => { for (const argv of [ ['dev', 'doctor', 'setup'], ['dev', 'runtime', 'doctor'], + ['dev', 'runtime', 'prune', '--dry-run'], ['dev', 'scan', 'warehouse'], ['dev', 'ingest', 'run'], ['dev', 'mapping', 'list'], @@ -126,7 +127,7 @@ describe('dev Commander tree', () => { it.each([ { argv: ['dev', 'runtime', '--help'], - expected: ['Usage: ktx dev runtime', 'install', 'start', 'stop', 'status', 'prune'], + expected: ['Usage: ktx dev runtime', 'install', 'start', 'stop', 'status'], }, { argv: ['scan', '--help'], @@ -147,6 +148,10 @@ describe('dev Commander tree', () => { for (const text of expected) { expect(io.stdout()).toContain(text); } + if (argv.join(' ') === 'dev runtime --help') { + expect(io.stdout()).not.toContain('prune'); + expect(io.stdout()).not.toContain('doctor'); + } expect(io.stderr()).toBe(''); expect(doctor).not.toHaveBeenCalled(); expect(ingest).not.toHaveBeenCalled(); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 9c08e58a..b79a0bb3 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -159,7 +159,7 @@ describe('runKtxCli', () => { await expect(runKtxCli(['dev', 'runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0); await expect(runKtxCli(['dev', 'runtime', 'stop', '--all'], stopAllIo.io, { runtime })).resolves.toBe(0); await expect(runKtxCli(['dev', 'runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0); - await expect(runKtxCli(['dev', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(0); + await expect(runKtxCli(['dev', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(1); expect(runtime).toHaveBeenNthCalledWith( 1, @@ -208,19 +208,11 @@ describe('runKtxCli', () => { }, statusIo.io, ); - expect(runtime).toHaveBeenNthCalledWith( - 6, - { - command: 'prune', - cliVersion: '0.0.0-private', - dryRun: true, - yes: false, - }, - pruneIo.io, - ); - for (const io of [installIo, startIo, stopIo, stopAllIo, statusIo, pruneIo]) { + expect(runtime).toHaveBeenCalledTimes(5); + for (const io of [installIo, startIo, stopIo, stopAllIo, statusIo]) { expect(io.stderr()).toBe(''); } + expect(pruneIo.stderr()).toMatch(/unknown command|error:/); }); it('prints the resolved project directory for ordinary project commands', async () => { diff --git a/packages/cli/src/managed-python-runtime.test.ts b/packages/cli/src/managed-python-runtime.test.ts index f2d9e784..63755ad1 100644 --- a/packages/cli/src/managed-python-runtime.test.ts +++ b/packages/cli/src/managed-python-runtime.test.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -8,7 +8,6 @@ import { doctorManagedPythonRuntime, installManagedPythonRuntime, managedPythonRuntimeLayout, - pruneManagedPythonRuntimes, readManagedPythonRuntimeStatus, verifyRuntimeAsset, type ManagedPythonRuntimeExec, @@ -471,41 +470,3 @@ describe('doctorManagedPythonRuntime', () => { }); }); }); - -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']); - }); -}); diff --git a/packages/cli/src/managed-python-runtime.ts b/packages/cli/src/managed-python-runtime.ts index 251ac7da..563b62f7 100644 --- a/packages/cli/src/managed-python-runtime.ts +++ b/packages/cli/src/managed-python-runtime.ts @@ -1,6 +1,6 @@ import { execFile } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { access, appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +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'; @@ -107,13 +107,6 @@ export interface ManagedPythonRuntimeDoctorCheck { fix?: string; } -export interface ManagedPythonRuntimePruneResult { - runtimeRoot: string; - stale: string[]; - kept: string[]; - removed: 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'; @@ -441,36 +434,3 @@ export async function doctorManagedPythonRuntime( ); return checks; } - -export async function pruneManagedPythonRuntimes(options: { - cliVersion: string; - runtimeRoot: string; - dryRun?: boolean; -}): Promise { - 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 }; -} diff --git a/packages/cli/src/runtime.test.ts b/packages/cli/src/runtime.test.ts index 16e879cc..8151a4b3 100644 --- a/packages/cli/src/runtime.test.ts +++ b/packages/cli/src/runtime.test.ts @@ -5,6 +5,7 @@ import type { ManagedPythonDaemonStopResult, } from './managed-python-daemon.js'; import type { + ManagedPythonRuntimeDoctorCheck, ManagedPythonRuntimeInstallResult, ManagedPythonRuntimeStatus, } from './managed-python-runtime.js'; @@ -256,7 +257,7 @@ describe('runKtxRuntime', () => { expect(io.stderr()).toContain('process scan: ps failed'); }); - it('prints runtime status as JSON', async () => { + it('prints runtime status and doctor checks as JSON with doctor-style exit status', async () => { const io = makeIo(); const deps: KtxRuntimeDeps = { readStatus: vi.fn(async (): Promise => ({ @@ -278,38 +279,41 @@ describe('runKtxRuntime', () => { daemonStderrPath: '/runtime/0.2.0/daemon.stderr.log', }, })), + doctorRuntime: vi.fn(async (): Promise => [ + { id: 'uv', label: 'uv', status: 'pass', detail: 'uv 0.9.5' }, + { id: 'asset', label: 'Bundled Python wheel', status: 'pass', detail: '/assets/python/runtime.whl' }, + { + id: 'runtime', + label: 'Managed Python runtime', + status: 'fail', + detail: 'No runtime manifest at /runtime/0.2.0/manifest.json', + fix: 'Run: ktx dev runtime install --yes', + }, + ]), }; - await expect(runKtxRuntime({ command: 'status', cliVersion: '0.2.0', json: true }, io.io, deps)).resolves.toBe(0); + await expect(runKtxRuntime({ command: 'status', cliVersion: '0.2.0', json: true }, io.io, deps)).resolves.toBe(1); expect(JSON.parse(io.stdout())).toMatchObject({ kind: 'missing', detail: 'No runtime manifest at /runtime/0.2.0/manifest.json', layout: { runtimeRoot: '/runtime' }, + checks: [ + { id: 'uv', status: 'pass' }, + { id: 'asset', status: 'pass' }, + { id: 'runtime', status: 'fail' }, + ], }); + expect(deps.readStatus).toHaveBeenCalledWith({ cliVersion: '0.2.0' }); + expect(deps.doctorRuntime).toHaveBeenCalledWith({ cliVersion: '0.2.0' }); }); - 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 () => { + it('prints runtime status and doctor checks in plain output', async () => { const io = makeIo(); const deps: KtxRuntimeDeps = { readStatus: vi.fn(async (): Promise => ({ - kind: 'missing', - detail: 'No runtime manifest at /runtime/0.2.0/manifest.json', + kind: 'ready', + detail: 'Runtime ready at /runtime/0.2.0', layout: { cliVersion: '0.2.0', runtimeRoot: '/runtime', @@ -325,19 +329,43 @@ describe('runKtxRuntime', () => { daemonStdoutPath: '/runtime/0.2.0/daemon.stdout.log', daemonStderrPath: '/runtime/0.2.0/daemon.stderr.log', }, + 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'], + 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', + }, })), - pruneRuntime: vi.fn(async () => ({ - runtimeRoot: '/runtime', - stale: ['/runtime/0.1.0'], - kept: ['/runtime/0.2.0'], - removed: [], - })), + doctorRuntime: vi.fn(async (): Promise => [ + { id: 'uv', label: 'uv', status: 'pass', detail: 'uv 0.9.5' }, + { id: 'asset', label: 'Bundled Python wheel', status: 'pass', detail: '/assets/python/runtime.whl' }, + { id: 'runtime', label: 'Managed Python runtime', status: 'pass', detail: 'Runtime ready at /runtime/0.2.0' }, + ]), }; - await expect(runKtxRuntime({ command: 'prune', cliVersion: '0.2.0', dryRun: true, yes: false }, io.io, deps)) - .resolves.toBe(0); + await expect(runKtxRuntime({ command: 'status', cliVersion: '0.2.0', json: false }, io.io, deps)).resolves.toBe(0); - expect(io.stdout()).toContain('Stale KTX Python runtimes'); - expect(io.stdout()).toContain('/runtime/0.1.0'); + expect(io.stdout()).toContain('KTX Python runtime'); + expect(io.stdout()).toContain('status: ready'); + expect(io.stdout()).toContain('KTX Python runtime checks'); + expect(io.stdout()).toContain('PASS uv: uv 0.9.5'); + expect(io.stdout()).toContain('PASS Managed Python runtime: Runtime ready at /runtime/0.2.0'); + expect(io.stderr()).toBe(''); }); }); diff --git a/packages/cli/src/runtime.ts b/packages/cli/src/runtime.ts index d5f4b5cb..8bb3fc7c 100644 --- a/packages/cli/src/runtime.ts +++ b/packages/cli/src/runtime.ts @@ -8,14 +8,14 @@ import { type ManagedPythonDaemonStopResult, } from './managed-python-daemon.js'; import { + doctorManagedPythonRuntime, installManagedPythonRuntime, - pruneManagedPythonRuntimes, readManagedPythonRuntimeStatus, type KtxRuntimeFeature, + type ManagedPythonRuntimeDoctorCheck, type ManagedPythonRuntimeInstallOptions, type ManagedPythonRuntimeInstallResult, type ManagedPythonRuntimeLayoutOptions, - type ManagedPythonRuntimePruneResult, type ManagedPythonRuntimeStatus, } from './managed-python-runtime.js'; @@ -23,8 +23,7 @@ export type KtxRuntimeArgs = | { command: 'install'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean } | { command: 'start'; cliVersion: string; feature: KtxRuntimeFeature; force: boolean } | { command: 'stop'; cliVersion: string; all: boolean } - | { command: 'status'; cliVersion: string; json: boolean } - | { command: 'prune'; cliVersion: string; dryRun: boolean; yes: boolean }; + | { command: 'status'; cliVersion: string; json: boolean }; export interface KtxRuntimeDeps { installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise; @@ -36,11 +35,7 @@ export interface KtxRuntimeDeps { stopDaemon?: (options: { cliVersion: string }) => Promise; stopAllDaemons?: (options: { cliVersion: string }) => Promise; readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise; - pruneRuntime?: (options: { - cliVersion: string; - runtimeRoot: string; - dryRun?: boolean; - }) => Promise; + doctorRuntime?: (options: ManagedPythonRuntimeLayoutOptions) => Promise; } function writeJson(io: KtxCliIo, value: unknown): void { @@ -145,17 +140,20 @@ function writeStatus(io: KtxCliIo, status: ManagedPythonRuntimeStatus): void { } } -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`); +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 hasRuntimeCheckFailures(checks: ManagedPythonRuntimeDoctorCheck[]): boolean { + return checks.some((check) => check.status === 'fail'); +} + export async function runKtxRuntime( args: KtxRuntimeArgs, io: KtxCliIo = process, @@ -196,27 +194,19 @@ export async function runKtxRuntime( } 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); + writeJson(io, { ...status, checks }); } else { writeStatus(io, status); + writeRuntimeChecks(io, checks); } - return 0; + return hasRuntimeCheckFailures(checks) ? 1 : 0; } - if (!args.dryRun && !args.yes) { - io.stderr.write('Refusing to prune without --yes. Preview with: ktx dev 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; + const _exhaustive: never = args; + return _exhaustive; } catch (error) { io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); return 1; diff --git a/scripts/examples-docs.test.mjs b/scripts/examples-docs.test.mjs index 5378b8ce..504e0d36 100644 --- a/scripts/examples-docs.test.mjs +++ b/scripts/examples-docs.test.mjs @@ -192,8 +192,7 @@ describe('standalone example docs', () => { assert.match(packageArtifacts, /requires `uv` on `PATH`/); assert.match(packageArtifacts, /ktx dev runtime status/); assert.match(packageArtifacts, /ktx dev runtime status/); - assert.match(packageArtifacts, /ktx dev runtime prune --dry-run/); - assert.match(packageArtifacts, /ktx dev runtime prune --yes/); + assert.doesNotMatch(packageArtifacts, /ktx dev runtime prune/); assert.match( packageArtifacts, new RegExp( @@ -226,8 +225,7 @@ describe('standalone example docs', () => { assert.match(readme, /requires `uv` on `PATH`/); assert.match(readme, /ktx dev runtime status/); assert.match(readme, /ktx dev runtime status/); - assert.match(readme, /ktx dev runtime prune --dry-run/); - assert.match(readme, /ktx dev runtime prune --yes/); + assert.doesNotMatch(readme, /ktx dev runtime prune/); assert.doesNotMatch(readme, /@ktx\/context/); assert.doesNotMatch(readme, /@ktx\/cli/); assert.doesNotMatch(readme, /python -m ktx_daemon semantic-validate/); diff --git a/scripts/local-embeddings-runtime-smoke.mjs b/scripts/local-embeddings-runtime-smoke.mjs index 064cd070..8b0243a0 100644 --- a/scripts/local-embeddings-runtime-smoke.mjs +++ b/scripts/local-embeddings-runtime-smoke.mjs @@ -205,6 +205,17 @@ function parseJsonStdout(label, result) { } } +function parseJsonStdoutWithExitCode(label, result, expectedCode) { + if (result.code !== expectedCode) { + throw new Error(`${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`); + } + try { + return JSON.parse(result.stdout); + } catch (error) { + throw new Error(`${label} did not write JSON stdout: ${error.message}\nstdout:\n${result.stdout}`); + } +} + function requireOutput(label, result, pattern) { if (!pattern.test(result.stdout)) { throw new Error(`${label} stdout did not match ${pattern}\nstdout:\n${result.stdout}`); @@ -283,13 +294,14 @@ export async function runLocalEmbeddingsRuntimeSmoke(options = {}) { requireSuccess(commands[0].label, version); requireOutput(commands[0].label, version, expectedPublicKtxVersionPattern()); - const missingStatus = parseJsonStdout( + const missingStatus = parseJsonStdoutWithExitCode( commands[1].label, await run(commands[1].command, commands[1].args, { cwd: installDir, env: smokeEnv, timeoutMs: commands[1].timeoutMs, }), + 1, ); if (missingStatus.kind !== 'missing') { throw new Error(`Expected missing runtime before install, got ${JSON.stringify(missingStatus)}`); diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index d99509d9..5f080068 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -548,6 +548,15 @@ function parseJsonResult(label, result) { return JSON.parse(result.stdout); } +function parseJsonResultWithExitCode(label, result, expectedCode) { + assert.equal( + result.code, + expectedCode, + label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr, + ); + return JSON.parse(result.stdout); +} + function parseJsonFailure(label, result) { assert.equal(result.code, 1, label + ' should fail with exit code 1'); assert.equal(result.stdout, '', label + ' should not write stdout when failing'); @@ -594,9 +603,10 @@ try { requireSuccess('ktx public package version', version); requireOutput('ktx public package version', version, /@kaelio\\/ktx 0\\.1\\.0/); - const runtimeStatusBefore = parseJsonResult( + const runtimeStatusBefore = parseJsonResultWithExitCode( 'ktx dev runtime status missing', await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'status', '--json']), + 1, ); assert.equal(runtimeStatusBefore.kind, 'missing'); assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT); @@ -889,27 +899,6 @@ try { requireOutput('ktx dev runtime stop', runtimeStop, /Stopped KTX Python daemon/); process.stdout.write('ktx dev runtime daemon lifecycle verified\\n'); - const staleRuntimeDir = join(process.env.KTX_RUNTIME_ROOT, '0.0.0'); - await mkdir(staleRuntimeDir, { recursive: true }); - - const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--dry-run']); - requireSuccess('ktx dev runtime prune dry run', runtimePruneDryRun); - requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/); - requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /0\\.0\\.0/); - await access(staleRuntimeDir); - - const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune']); - assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx dev runtime prune needs confirmation'); - assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx dev runtime prune needs confirmation wrote stdout'); - assert.match(runtimePruneNeedsConfirmation.stderr, /Refusing to prune without --yes/); - - const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--yes']); - requireSuccess('ktx dev runtime prune confirmed', runtimePruneConfirmed); - requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/); - requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /0\\.0\\.0/); - await assert.rejects(() => access(staleRuntimeDir)); - process.stdout.write('ktx dev runtime prune verified\\n'); - const structuralScan = await run('pnpm', ['exec', 'ktx', 'scan', 'warehouse', '--project-dir', projectDir, diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index a8527c98..b4176353 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -490,13 +490,8 @@ describe('verification snippets', () => { assert.match(source, /ktx dev runtime start reuse/); assert.match(source, /Using existing KTX Python daemon/); assert.match(source, /ktx dev runtime stop/); - assert.match(source, /ktx dev runtime prune dry run/); - assert.match(source, /0\.0\.0/); - assert.match(source, /ktx dev runtime prune needs confirmation/); - assert.match(source, /Refusing to prune without --yes/); - assert.match(source, /ktx dev runtime prune confirmed/); - assert.match(source, /Removed stale KTX Python runtimes/); - assert.match(source, /assert\.rejects\(\(\) => access\(staleRuntimeDir\)\)/); + assert.doesNotMatch(source, /ktx dev runtime prune/); + assert.doesNotMatch(source, /staleRuntimeDir/); assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'scan',\s*'warehouse'/); assert.match(source, /'--mode',\s*'enriched'/); assert.doesNotMatch(source, /'--enrich'/);