@oscharko-dev/keiko-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/context.d.ts +3 -0
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/context.js +103 -0
  5. package/dist/doctor.d.ts +24 -0
  6. package/dist/doctor.d.ts.map +1 -0
  7. package/dist/doctor.js +108 -0
  8. package/dist/evaluate.d.ts +8 -0
  9. package/dist/evaluate.d.ts.map +1 -0
  10. package/dist/evaluate.js +270 -0
  11. package/dist/evidence.d.ts +9 -0
  12. package/dist/evidence.d.ts.map +1 -0
  13. package/dist/evidence.js +129 -0
  14. package/dist/gateway-config.d.ts +12 -0
  15. package/dist/gateway-config.d.ts.map +1 -0
  16. package/dist/gateway-config.js +19 -0
  17. package/dist/gen-tests.d.ts +8 -0
  18. package/dist/gen-tests.d.ts.map +1 -0
  19. package/dist/gen-tests.js +216 -0
  20. package/dist/index.d.ts +18 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +26 -0
  23. package/dist/init.d.ts +9 -0
  24. package/dist/init.d.ts.map +1 -0
  25. package/dist/init.js +122 -0
  26. package/dist/install-layout.d.ts +19 -0
  27. package/dist/install-layout.d.ts.map +1 -0
  28. package/dist/install-layout.js +76 -0
  29. package/dist/investigate.d.ts +9 -0
  30. package/dist/investigate.d.ts.map +1 -0
  31. package/dist/investigate.js +249 -0
  32. package/dist/launcher-paths.d.ts +4 -0
  33. package/dist/launcher-paths.d.ts.map +1 -0
  34. package/dist/launcher-paths.js +69 -0
  35. package/dist/launcher-platforms.d.ts +25 -0
  36. package/dist/launcher-platforms.d.ts.map +1 -0
  37. package/dist/launcher-platforms.js +131 -0
  38. package/dist/launcher-state.d.ts +25 -0
  39. package/dist/launcher-state.d.ts.map +1 -0
  40. package/dist/launcher-state.js +228 -0
  41. package/dist/launcher.d.ts +21 -0
  42. package/dist/launcher.d.ts.map +1 -0
  43. package/dist/launcher.js +439 -0
  44. package/dist/lifecycle.d.ts +22 -0
  45. package/dist/lifecycle.d.ts.map +1 -0
  46. package/dist/lifecycle.js +425 -0
  47. package/dist/memory.d.ts +14 -0
  48. package/dist/memory.d.ts.map +1 -0
  49. package/dist/memory.js +290 -0
  50. package/dist/models.d.ts +4 -0
  51. package/dist/models.d.ts.map +1 -0
  52. package/dist/models.js +62 -0
  53. package/dist/prompt-enhancer.d.ts +13 -0
  54. package/dist/prompt-enhancer.d.ts.map +1 -0
  55. package/dist/prompt-enhancer.js +261 -0
  56. package/dist/repair.d.ts +10 -0
  57. package/dist/repair.d.ts.map +1 -0
  58. package/dist/repair.js +402 -0
  59. package/dist/run.d.ts +10 -0
  60. package/dist/run.d.ts.map +1 -0
  61. package/dist/run.js +269 -0
  62. package/dist/runner.d.ts +7 -0
  63. package/dist/runner.d.ts.map +1 -0
  64. package/dist/runner.js +108 -0
  65. package/dist/state-paths.d.ts +43 -0
  66. package/dist/state-paths.d.ts.map +1 -0
  67. package/dist/state-paths.js +396 -0
  68. package/dist/ui.d.ts +39 -0
  69. package/dist/ui.d.ts.map +1 -0
  70. package/dist/ui.js +450 -0
  71. package/dist/uninstall.d.ts +10 -0
  72. package/dist/uninstall.d.ts.map +1 -0
  73. package/dist/uninstall.js +345 -0
  74. package/dist/verify.d.ts +3 -0
  75. package/dist/verify.d.ts.map +1 -0
  76. package/dist/verify.js +108 -0
  77. package/package.json +42 -0
@@ -0,0 +1,345 @@
1
+ // `keiko uninstall` — reverses the runtime artifacts Keiko creates on a machine so a
2
+ // user can clean their device and reinstall a clean version. CLI-only surface; no UI
3
+ // route, no server change, no new runtime dependency.
4
+ //
5
+ // SCOPE BOUNDARY (deliberate, see PR rationale): Keiko never removes its own installed
6
+ // npm package. A running Node process cannot reliably delete the package files it is
7
+ // executing from, and the deterministic-first architecture avoids shelling into a
8
+ // package manager. `uninstall` therefore reverses only Keiko-OWNED runtime artifacts
9
+ // and prints the exact package-manager command to remove the package itself — the same
10
+ // guidance `keiko doctor` already gives. The artifacts reversed are:
11
+ //
12
+ // 1. Launcher OS shortcuts — delegated to `removeLauncherShortcuts`, which
13
+ // content-hash-verifies each recorded shortcut and refuses any foreign/modified
14
+ // file (home-contained; never deletes a file Keiko did not generate).
15
+ // 2. `keiko:start` / `keiko:stop` scripts in the project `package.json` — removed
16
+ // ONLY when their value exactly matches what `keiko init` writes, so a
17
+ // user-customized script is never clobbered.
18
+ // 3. The `.keiko/` state directory — every artifact the allowlisted runtime-state
19
+ // manifest (`state-paths.ts`, Issue #1321) recognizes is removed: lifecycle/launcher
20
+ // files, the UI / Memory / Local-Knowledge databases and their WAL/SHM sidecars,
21
+ // Evidence and Quality-Intelligence records, and the sealed credential vaults. An
22
+ // unrecognized customer file and any symlink are left in place; a directory (and the
23
+ // state root) is removed only once no such entry survives beneath it. Nothing
24
+ // recursively deletes an arbitrary directory or follows a symlink out of `.keiko`.
25
+ //
26
+ // With no scope flag every step runs; `--state` / `--launchers` / `--scripts` narrow
27
+ // it. `--dry-run` reports `would-...` without changing anything.
28
+ import { existsSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
29
+ import { homedir as defaultHomedir } from "node:os";
30
+ import { join, resolve } from "node:path";
31
+ import { LauncherError } from "./launcher-platforms.js";
32
+ import { removeLauncherShortcuts } from "./launcher.js";
33
+ import { KEIKO_START_SCRIPT, KEIKO_STOP_SCRIPT } from "./init.js";
34
+ import { localPackageRoot } from "./install-layout.js";
35
+ import { classifyPid, defaultIsProcessAlive, inspectStateRoot, isInsidePath, resolveStateDir, scanRuntimeState, } from "./state-paths.js";
36
+ const USAGE = `Usage:
37
+ keiko uninstall [--state] [--launchers] [--scripts] [--state-dir PATH]
38
+ [--package PATH] [--force] [--dry-run]
39
+
40
+ Reverses the runtime artifacts Keiko creates on this machine:
41
+ --launchers remove the user-local OS shortcut(s) (\`keiko launcher install\`)
42
+ --scripts remove the keiko:start / keiko:stop scripts from package.json
43
+ --state remove Keiko-owned runtime state under .keiko: lifecycle/launcher files,
44
+ UI / Memory / Local-Knowledge databases and their WAL/SHM sidecars,
45
+ Evidence and Quality-Intelligence records, and the sealed credential vaults
46
+
47
+ With no scope flag, all three are removed. An unknown (non-Keiko) file under .keiko and any
48
+ symlink are always left in place, and the state directory is removed only once nothing of
49
+ yours remains. The installed npm package itself is left in place; the command prints the
50
+ package-manager step to remove it. \`--force\` stops a running UI before removing state;
51
+ \`--dry-run\` shows what would be removed. Filesystem unlinking does not guarantee secure
52
+ erasure of SSD-backed data.
53
+ `;
54
+ const KEIKO_SCRIPTS = {
55
+ "keiko:start": KEIKO_START_SCRIPT,
56
+ "keiko:stop": KEIKO_STOP_SCRIPT,
57
+ };
58
+ function readFlagValue(args, index) {
59
+ const value = args[index + 1];
60
+ return value === undefined || value.startsWith("--") ? null : value;
61
+ }
62
+ function applyValuedFlag(raw, arg, value) {
63
+ if (arg === "--state-dir")
64
+ raw.stateDirArg = value;
65
+ else
66
+ raw.packageArg = value;
67
+ }
68
+ function applyBooleanFlag(raw, arg) {
69
+ switch (arg) {
70
+ case "--state":
71
+ raw.state = true;
72
+ return true;
73
+ case "--launchers":
74
+ raw.launchers = true;
75
+ return true;
76
+ case "--scripts":
77
+ raw.scripts = true;
78
+ return true;
79
+ case "--dry-run":
80
+ raw.dryRun = true;
81
+ return true;
82
+ case "--force":
83
+ raw.force = true;
84
+ return true;
85
+ default:
86
+ return false;
87
+ }
88
+ }
89
+ function parseUninstallArgs(args, cwd) {
90
+ const raw = {
91
+ state: false,
92
+ launchers: false,
93
+ scripts: false,
94
+ stateDirArg: undefined,
95
+ packageArg: undefined,
96
+ dryRun: false,
97
+ force: false,
98
+ };
99
+ for (let i = 0; i < args.length; i += 1) {
100
+ const arg = args[i];
101
+ if (arg === undefined)
102
+ return null;
103
+ if (arg === "--help" || arg === "-h")
104
+ return "help";
105
+ if (applyBooleanFlag(raw, arg))
106
+ continue;
107
+ if (arg === "--state-dir" || arg === "--package") {
108
+ const value = readFlagValue(args, i);
109
+ if (value === null)
110
+ return null;
111
+ applyValuedFlag(raw, arg, value);
112
+ i += 1;
113
+ continue;
114
+ }
115
+ return null;
116
+ }
117
+ return finalizeOptions(raw, cwd);
118
+ }
119
+ function finalizeOptions(raw, cwd) {
120
+ const anyScope = raw.state || raw.launchers || raw.scripts;
121
+ return {
122
+ scopes: anyScope
123
+ ? { state: raw.state, launchers: raw.launchers, scripts: raw.scripts }
124
+ : { state: true, launchers: true, scripts: true },
125
+ packagePath: raw.packageArg === undefined ? resolve(cwd, "package.json") : resolve(cwd, raw.packageArg),
126
+ stateDirArg: raw.stateDirArg,
127
+ dryRun: raw.dryRun,
128
+ force: raw.force,
129
+ };
130
+ }
131
+ function resolveDeps(deps) {
132
+ return {
133
+ cwd: deps.cwd ?? process.cwd(),
134
+ homedir: deps.homedir ?? defaultHomedir,
135
+ isProcessAlive: deps.isProcessAlive ?? defaultIsProcessAlive,
136
+ killProcess: deps.killProcess ?? process.kill.bind(process),
137
+ };
138
+ }
139
+ // Returns "ok" to proceed, or "refused" when state removal is requested while the UI is
140
+ // running and `--force` was not given (removing live state would orphan the process).
141
+ function ensureServerStoppable(opts, io, deps, stateDir) {
142
+ if (!opts.scopes.state)
143
+ return "ok";
144
+ const probe = classifyPid(join(stateDir, "ui.pid"), deps.isProcessAlive);
145
+ if (probe.state !== "running" || probe.pid === undefined)
146
+ return "ok";
147
+ if (!opts.force) {
148
+ io.err(`keiko uninstall: the Keiko UI is running (pid ${String(probe.pid)}). Run \`keiko stop\` first, or re-run with --force to stop it.\n`);
149
+ return "refused";
150
+ }
151
+ if (opts.dryRun) {
152
+ io.out(`would-stop: Keiko UI (pid ${String(probe.pid)})\n`);
153
+ return "ok";
154
+ }
155
+ io.out(`Stopping Keiko UI (pid ${String(probe.pid)}) ...\n`);
156
+ try {
157
+ deps.killProcess(probe.pid, "SIGTERM");
158
+ }
159
+ catch {
160
+ // Process already exited between the probe and the signal — nothing to stop.
161
+ }
162
+ return "ok";
163
+ }
164
+ function removeLaunchersStep(opts, io, deps, stateDir) {
165
+ if (!opts.scopes.launchers)
166
+ return 0;
167
+ const result = removeLauncherShortcuts(io, {
168
+ stateDir,
169
+ homedir: deps.homedir(),
170
+ dryRun: opts.dryRun,
171
+ });
172
+ return result.refused;
173
+ }
174
+ function isRecord(value) {
175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
176
+ }
177
+ function pruneKeikoScripts(scripts, io, dryRun) {
178
+ const removeNames = new Set();
179
+ for (const [name, expected] of Object.entries(KEIKO_SCRIPTS)) {
180
+ if (!(name in scripts))
181
+ continue;
182
+ if (scripts[name] !== expected) {
183
+ io.out(`kept: ${name} (customized — not the script keiko init writes)\n`);
184
+ continue;
185
+ }
186
+ io.out(`${dryRun ? "would-remove" : "removed"}: package.json script ${name}\n`);
187
+ removeNames.add(name);
188
+ }
189
+ const next = Object.fromEntries(Object.entries(scripts).filter(([key]) => !removeNames.has(key)));
190
+ return { next, removed: removeNames.size };
191
+ }
192
+ function removeScriptsStep(opts, io) {
193
+ if (!opts.scopes.scripts)
194
+ return;
195
+ if (!existsSync(opts.packagePath)) {
196
+ io.out(`scripts: package.json not found at ${opts.packagePath} (nothing to remove)\n`);
197
+ return;
198
+ }
199
+ let pkg;
200
+ try {
201
+ pkg = JSON.parse(readFileSync(opts.packagePath, "utf8"));
202
+ }
203
+ catch {
204
+ io.err(`keiko uninstall: package.json at ${opts.packagePath} is not valid JSON; skipping scripts.\n`);
205
+ return;
206
+ }
207
+ const scripts = isRecord(pkg) ? pkg.scripts : undefined;
208
+ if (!isRecord(pkg) || !isRecord(scripts)) {
209
+ io.out("scripts: no keiko:start / keiko:stop scripts found.\n");
210
+ return;
211
+ }
212
+ const { next, removed } = pruneKeikoScripts(scripts, io, opts.dryRun);
213
+ if (removed > 0 && !opts.dryRun) {
214
+ writeFileSync(opts.packagePath, `${JSON.stringify({ ...pkg, scripts: next }, null, 2)}\n`, "utf8");
215
+ }
216
+ }
217
+ // Issue #1321: remove every Keiko-owned sensitive runtime artifact under the state dir,
218
+ // not only the three lifecycle/launcher files. The set is the allowlisted manifest in
219
+ // `state-paths.ts` (UI/Memory/Knowledge DBs + sidecars, Evidence/QI records, the sealed
220
+ // credential vaults, config, logs). Anything the manifest does not recognize — an unknown
221
+ // customer file or a symlink — is reported and left in place, and a directory is removed
222
+ // only when no such retained entry survives beneath it. Content-free: paths only.
223
+ function removeOwnedFiles(scan, io, dryRun) {
224
+ for (const file of scan.files) {
225
+ if (dryRun) {
226
+ io.out(`would-remove: ${file.absPath}\n`);
227
+ continue;
228
+ }
229
+ unlinkSync(file.absPath);
230
+ io.out(`removed: ${file.absPath}\n`);
231
+ }
232
+ }
233
+ function reportRetained(scan, io) {
234
+ for (const entry of scan.retained) {
235
+ const why = entry.reason === "symlink"
236
+ ? "symlink — not followed"
237
+ : entry.reason === "hardlink"
238
+ ? "hardlink — not modified or removed"
239
+ : "not a recognized Keiko artifact";
240
+ io.out(`kept: ${entry.absPath} (${why})\n`);
241
+ }
242
+ }
243
+ // Owned directories are visited deepest-first (the scan lists them shallowest-first). A
244
+ // directory is removed only when no retained entry lives beneath it, so an unknown file or
245
+ // a refused symlink anywhere inside keeps the whole chain up to the state root in place.
246
+ function removeOwnedDirectories(scan, io, dryRun) {
247
+ for (const dir of [...scan.directories].reverse()) {
248
+ if (scan.retained.some((entry) => isInsidePath(dir.absPath, entry.absPath)))
249
+ continue;
250
+ if (dryRun) {
251
+ io.out(`would-remove: ${dir.absPath}\n`);
252
+ continue;
253
+ }
254
+ rmdirSync(dir.absPath);
255
+ io.out(`removed: ${dir.absPath}\n`);
256
+ }
257
+ }
258
+ function finalizeStateDir(scan, stateDir, io, dryRun) {
259
+ if (scan.retained.length === 0) {
260
+ if (dryRun)
261
+ io.out(`would-remove: ${stateDir} (empty after removals)\n`);
262
+ else {
263
+ rmdirSync(stateDir);
264
+ io.out(`removed: ${stateDir}\n`);
265
+ }
266
+ return;
267
+ }
268
+ const topLevel = new Set(scan.retained.map((entry) => entry.relPath.split("/")[0] ?? entry.relPath));
269
+ const count = topLevel.size;
270
+ io.out(`kept: ${stateDir} (still contains ${String(count)} non-Keiko entr${count === 1 ? "y" : "ies"})\n`);
271
+ }
272
+ function removeStateStep(opts, io, stateDir) {
273
+ if (!opts.scopes.state)
274
+ return;
275
+ if (!existsSync(stateDir)) {
276
+ io.out(`state: ${stateDir} not found (nothing to remove)\n`);
277
+ return;
278
+ }
279
+ const scan = scanRuntimeState(stateDir);
280
+ removeOwnedFiles(scan, io, opts.dryRun);
281
+ reportRetained(scan, io);
282
+ removeOwnedDirectories(scan, io, opts.dryRun);
283
+ finalizeStateDir(scan, stateDir, io, opts.dryRun);
284
+ }
285
+ function refuseUnsafeStateRoot(opts, io, root) {
286
+ if (!opts.scopes.state && !opts.scopes.launchers)
287
+ return false;
288
+ if (root.status === "symlink") {
289
+ io.err(`keiko uninstall: refusing to use symlinked state directory: ${root.absPath}\n`);
290
+ return true;
291
+ }
292
+ if (root.status === "not-directory") {
293
+ io.err(`keiko uninstall: refusing to use non-directory state path: ${root.absPath}\n`);
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ function printPackageGuidance(io, deps) {
299
+ const localInstalled = existsSync(localPackageRoot(deps.cwd));
300
+ io.out("\nKeiko runtime artifacts processed. To remove the package itself, run:\n");
301
+ if (localInstalled) {
302
+ io.out(" npm uninstall @oscharko-dev/keiko (local install in this project)\n");
303
+ io.out(" npm uninstall -g @oscharko-dev/keiko (if also installed globally)\n");
304
+ }
305
+ else {
306
+ io.out(" npm uninstall -g @oscharko-dev/keiko (global install)\n");
307
+ io.out(" npm uninstall @oscharko-dev/keiko (if installed locally in a project)\n");
308
+ }
309
+ io.out(" yarn remove @oscharko-dev/keiko • pnpm remove @oscharko-dev/keiko (yarn / pnpm)\n");
310
+ }
311
+ export function runUninstallCli(args, io, env, deps = {}) {
312
+ const resolved = resolveDeps(deps);
313
+ const opts = parseUninstallArgs(args, resolved.cwd);
314
+ if (opts === "help") {
315
+ io.out(USAGE);
316
+ return 0;
317
+ }
318
+ if (opts === null) {
319
+ io.err(USAGE);
320
+ return 2;
321
+ }
322
+ const stateDir = resolveStateDir(resolved.cwd, env, opts.stateDirArg);
323
+ const stateRoot = inspectStateRoot(stateDir);
324
+ if (refuseUnsafeStateRoot(opts, io, stateRoot))
325
+ return 1;
326
+ try {
327
+ if (ensureServerStoppable(opts, io, resolved, stateDir) === "refused")
328
+ return 1;
329
+ const launcherRefused = removeLaunchersStep(opts, io, resolved, stateDir);
330
+ removeScriptsStep(opts, io);
331
+ removeStateStep(opts, io, stateDir);
332
+ printPackageGuidance(io, resolved);
333
+ return launcherRefused > 0 ? 1 : 0;
334
+ }
335
+ catch (e) {
336
+ if (e instanceof LauncherError) {
337
+ io.err(`${e.message}\n`);
338
+ return 1;
339
+ }
340
+ // Filesystem errors (e.g. a read-only package.json or state file) are reported as a
341
+ // clean non-zero exit rather than crashing the CLI with an unhandled exception.
342
+ io.err(`keiko uninstall: ${e instanceof Error ? e.message : String(e)}\n`);
343
+ return 1;
344
+ }
345
+ }
@@ -0,0 +1,3 @@
1
+ import type { CliIo } from "./runner.js";
2
+ export declare function runVerifyCli(args: readonly string[], io: CliIo): Promise<number>;
3
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAoFzC,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BtF"}
package/dist/verify.js ADDED
@@ -0,0 +1,108 @@
1
+ // `keiko verify` — the real verification gate (NOT a dry-run). It detects the workspace, builds a
2
+ // plan from the detected npm scripts, runs the allowlisted commands through the #6 safe tool layer
3
+ // under per-command resource limits, and prints a redacted human table (or --json of the report).
4
+ // Exit 0 when overall status is `passed`; 1 when any step did not pass or a workspace/runtime error
5
+ // occurred; 2 on a usage error. Mirrors runContextCli's structure (flag parsing, --json, typed
6
+ // error catch at the boundary).
7
+ import { detectWorkspace, WorkspaceError } from "@oscharko-dev/keiko-workspace";
8
+ import { buildVerificationPlan, buildVerificationSummary, detectScripts, EmptyPlanError, runVerification, VerificationError, } from "@oscharko-dev/keiko-verification";
9
+ const VALID_KINDS = new Set([
10
+ "test",
11
+ "targeted-test",
12
+ "typecheck",
13
+ "lint",
14
+ "build",
15
+ ]);
16
+ const USAGE = `Usage:
17
+ keiko verify [--dir PATH] [--only KIND[,KIND]] [--changed FILE[,FILE]] [--json]
18
+
19
+ Runs the project's own gates (typecheck, lint, test, build) through the safe tool
20
+ layer under per-command resource limits and prints a redacted evidence summary.
21
+ KIND is one of: test, targeted-test, typecheck, lint, build.
22
+ `;
23
+ // Returns the value of a `--flag value` pair, undefined if absent, or null if present without a
24
+ // value (a usage error) — identical contract to runContextCli's flagValue.
25
+ function flagValue(args, name) {
26
+ const i = args.indexOf(name);
27
+ if (i === -1) {
28
+ return undefined;
29
+ }
30
+ const value = args[i + 1];
31
+ return value === undefined || value.startsWith("--") ? null : value;
32
+ }
33
+ function parseKinds(raw) {
34
+ const parts = raw.split(",").map((p) => p.trim());
35
+ if (parts.some((p) => !VALID_KINDS.has(p))) {
36
+ return null;
37
+ }
38
+ return parts;
39
+ }
40
+ function parseArgs(args) {
41
+ const dirRaw = flagValue(args, "--dir");
42
+ const onlyRaw = flagValue(args, "--only");
43
+ const changedRaw = flagValue(args, "--changed");
44
+ if (dirRaw === null || onlyRaw === null || changedRaw === null) {
45
+ return null;
46
+ }
47
+ const only = onlyRaw === undefined ? undefined : parseKinds(onlyRaw);
48
+ if (only === null) {
49
+ return null;
50
+ }
51
+ const changed = changedRaw === undefined ? undefined : changedRaw.split(",").map((p) => p.trim());
52
+ return { dir: dirRaw ?? ".", only, changed, json: args.includes("--json") };
53
+ }
54
+ async function runPlan(parsed) {
55
+ const workspace = detectWorkspace(parsed.dir);
56
+ const catalog = detectScripts(workspace);
57
+ const plan = buildVerificationPlan(workspace, catalog, {
58
+ only: parsed.only,
59
+ changedFiles: parsed.changed,
60
+ });
61
+ if (plan.steps.length === 0) {
62
+ throw new EmptyPlanError("verification plan contains no runnable or skipped steps");
63
+ }
64
+ return runVerification(plan, { workspace });
65
+ }
66
+ function renderText(report, io) {
67
+ const summary = buildVerificationSummary(report);
68
+ io.out(`Verification: ${summary.overallStatus} (${String(summary.durationMs)}ms)\n`);
69
+ io.out("KIND\tSTATUS\tEXIT\tMS\tCOMMAND\tDETAIL\n");
70
+ for (const r of summary.results) {
71
+ const exit = r.exitCode === null ? "-" : String(r.exitCode);
72
+ io.out(`${r.kind}\t${r.status}\t${exit}\t${String(r.durationMs)}\t${r.command}\t${r.detail ?? ""}\n`);
73
+ }
74
+ }
75
+ export async function runVerifyCli(args, io) {
76
+ // Issue #640: handle --help / -h before workflow-arg validation so help discovery exits 0
77
+ // with usage on stdout, not 2 with a validation error on stderr.
78
+ if (args.includes("--help") || args.includes("-h")) {
79
+ io.out(USAGE);
80
+ return 0;
81
+ }
82
+ const parsed = parseArgs(args);
83
+ if (parsed === null) {
84
+ io.err(USAGE);
85
+ return 2;
86
+ }
87
+ try {
88
+ const report = await runPlan(parsed);
89
+ if (parsed.json) {
90
+ io.out(`${JSON.stringify(buildVerificationSummary(report), null, 2)}\n`);
91
+ }
92
+ else {
93
+ renderText(report, io);
94
+ }
95
+ return report.overallStatus === "passed" ? 0 : 1;
96
+ }
97
+ catch (error) {
98
+ if (error instanceof WorkspaceError) {
99
+ io.err(`Error [${error.code}]: ${error.message}\n`);
100
+ return 1;
101
+ }
102
+ if (error instanceof VerificationError) {
103
+ io.err(`Error [${error.code}]: ${error.message}\n`);
104
+ return 1;
105
+ }
106
+ throw error;
107
+ }
108
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@oscharko-dev/keiko-cli",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Internal CLI package: command modules for the `keiko` binary shipped via the root product package (ADR-0019). Not published independently.",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -b tsconfig.json",
17
+ "typecheck": "tsc -b tsconfig.json",
18
+ "test": "vitest run"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "sideEffects": false,
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "dependencies": {
28
+ "@oscharko-dev/keiko-contracts": "0.2.0",
29
+ "@oscharko-dev/keiko-security": "0.2.0",
30
+ "@oscharko-dev/keiko-model-gateway": "0.2.0",
31
+ "@oscharko-dev/keiko-workspace": "0.2.0",
32
+ "@oscharko-dev/keiko-tools": "0.2.0",
33
+ "@oscharko-dev/keiko-evaluations": "0.2.0",
34
+ "@oscharko-dev/keiko-evidence": "0.2.0",
35
+ "@oscharko-dev/keiko-verification": "0.2.0",
36
+ "@oscharko-dev/keiko-harness": "0.2.0",
37
+ "@oscharko-dev/keiko-sdk": "0.2.0",
38
+ "@oscharko-dev/keiko-workflows": "0.2.0",
39
+ "@oscharko-dev/keiko-server": "0.2.0",
40
+ "@oscharko-dev/keiko-memory-vault": "0.2.0"
41
+ }
42
+ }