@elench/testkit 0.1.96 → 0.1.98

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 (79) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +49 -12
  3. package/lib/cli/assistant/composer.mjs +19 -1
  4. package/lib/cli/assistant/context-pack.mjs +9 -8
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/model-discovery.mjs +243 -0
  7. package/lib/cli/assistant/prompt-builder.mjs +2 -5
  8. package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
  9. package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
  10. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  11. package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
  12. package/lib/cli/assistant/session.mjs +31 -6
  13. package/lib/cli/assistant/slash-commands.mjs +30 -3
  14. package/lib/cli/assistant/state.mjs +237 -71
  15. package/lib/cli/assistant/tool-registry.mjs +325 -39
  16. package/lib/cli/assistant/view-model.mjs +1 -1
  17. package/lib/cli/commands/assistant.mjs +4 -3
  18. package/lib/cli/commands/browser/serve.mjs +5 -23
  19. package/lib/cli/commands/cleanup.mjs +8 -2
  20. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  21. package/lib/cli/commands/destroy.mjs +8 -2
  22. package/lib/cli/commands/discover.mjs +5 -27
  23. package/lib/cli/commands/doctor.mjs +5 -5
  24. package/lib/cli/commands/flags.mjs +61 -0
  25. package/lib/cli/commands/run.mjs +10 -2
  26. package/lib/cli/commands/status.mjs +10 -2
  27. package/lib/cli/commands/typecheck.mjs +5 -5
  28. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  29. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  30. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  31. package/lib/cli/config.mjs +63 -0
  32. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  33. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  34. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  35. package/lib/cli/operations/destroy/operation.mjs +12 -0
  36. package/lib/cli/operations/discover/operation.mjs +32 -0
  37. package/lib/cli/operations/doctor/operation.mjs +5 -0
  38. package/lib/cli/operations/run/operation.mjs +129 -0
  39. package/lib/cli/operations/status/operation.mjs +7 -0
  40. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  41. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  42. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  43. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  44. package/lib/cli/renderers/destroy/text.mjs +3 -0
  45. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  46. package/lib/cli/renderers/discover/text.mjs +7 -0
  47. package/lib/cli/renderers/doctor/text.mjs +7 -0
  48. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  49. package/lib/cli/renderers/run/interactive.mjs +119 -0
  50. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  51. package/lib/cli/renderers/status/text.mjs +7 -0
  52. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  53. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  54. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  55. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  56. package/lib/cli/terminal/capabilities.mjs +33 -0
  57. package/lib/database/index.mjs +9 -21
  58. package/lib/database/template-steps.mjs +3 -3
  59. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  60. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  61. package/lib/runner/maintenance.mjs +25 -14
  62. package/lib/runner/readiness.mjs +5 -4
  63. package/lib/runner/runtime-preparation.mjs +36 -0
  64. package/lib/runner/state-io.mjs +10 -4
  65. package/lib/runner/template.mjs +24 -3
  66. package/node_modules/@elench/next-analysis/package.json +1 -1
  67. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  68. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  69. package/node_modules/@elench/ts-analysis/package.json +1 -1
  70. package/package.json +5 -5
  71. package/lib/cli/assistant/command-plan.mjs +0 -227
  72. package/lib/cli/command-helpers.mjs +0 -191
  73. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  74. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  75. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  76. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  77. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  78. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  79. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -1,11 +1,19 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { execaCommand } from "execa";
4
- import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
5
- import {
6
- readContextContent,
7
- } from "../context-resources.mjs";
8
- import { extractShellCommand, planShellCommand } from "./command-plan.mjs";
4
+ import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
5
+ import { readContextContent } from "../../results/context.mjs";
6
+ import { buildRunRequest, executeRunRequest } from "../operations/run/operation.mjs";
7
+ import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
8
+ import { executeDoctorOperation } from "../operations/doctor/operation.mjs";
9
+ import { executeTypecheckOperation } from "../operations/typecheck/operation.mjs";
10
+ import { executeStatusOperation } from "../operations/status/operation.mjs";
11
+ import { renderDiscoverResult } from "../renderers/discover/text.mjs";
12
+ import { renderDoctorResult } from "../renderers/doctor/text.mjs";
13
+ import { renderTypecheckResult } from "../renderers/typecheck/text.mjs";
14
+ import { renderStatusResult } from "../renderers/status/text.mjs";
15
+ import { createRunSession } from "../renderers/run/interactive.mjs";
16
+ import { loadCliConfig } from "../config.mjs";
9
17
 
10
18
  const COMMAND_OUTPUT_LIMIT = 14_000;
11
19
  const COMMAND_LINE_LIMIT = 220;
@@ -13,9 +21,29 @@ const FILE_LINE_LIMIT = 160;
13
21
 
14
22
  export function listAssistantTools() {
15
23
  return [
24
+ {
25
+ name: "run_tests",
26
+ description: "Run testkit suites in-process with the shared live run session and tree state.",
27
+ },
28
+ {
29
+ name: "discover_tests",
30
+ description: "Discover testkit suites and files in the current repository.",
31
+ },
32
+ {
33
+ name: "show_status",
34
+ description: "Show local testkit runtime and database state for the current repository.",
35
+ },
36
+ {
37
+ name: "run_doctor",
38
+ description: "Run built-in testkit doctor checks for the current repository.",
39
+ },
40
+ {
41
+ name: "run_typecheck",
42
+ description: "Run testkit typechecking for config, helpers, and suites.",
43
+ },
16
44
  {
17
45
  name: "shell_exec",
18
- description: "Execute a shell command inside the repository. Use local testkit commands for testkit work.",
46
+ description: "Execute a shell command inside the repository for arbitrary repo work outside first-class testkit actions.",
19
47
  },
20
48
  {
21
49
  name: "read_context",
@@ -35,6 +63,16 @@ export function listAssistantTools() {
35
63
  export async function executeAssistantTool(name, argumentsObject, context) {
36
64
  const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
37
65
  switch (name) {
66
+ case "run_tests":
67
+ return runTestsTool(args, context);
68
+ case "discover_tests":
69
+ return discoverTestsTool(args, context);
70
+ case "show_status":
71
+ return showStatusTool(args, context);
72
+ case "run_doctor":
73
+ return doctorTool(args, context);
74
+ case "run_typecheck":
75
+ return typecheckTool(args, context);
38
76
  case "shell_exec":
39
77
  return shellExecTool(args, context);
40
78
  case "read_context":
@@ -49,10 +87,13 @@ export async function executeAssistantTool(name, argumentsObject, context) {
49
87
  }
50
88
 
51
89
  async function shellExecTool(args, context) {
52
- const command = extractShellCommand(args).trim();
90
+ const command = String(args.command || "").trim();
53
91
  if (!command) throw new Error("shell_exec requires a command string");
92
+ if (isTestkitCommand(command)) {
93
+ throw new Error("Use the dedicated testkit tools for run/discover/status/doctor/typecheck instead of shell_exec.");
94
+ }
54
95
 
55
- const shellCommand = planShellCommand(command);
96
+ const shellCommand = classifyShellCommand(command);
56
97
  const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
57
98
  context.commandLog?.appendCommandLog({
58
99
  type: "command_start",
@@ -60,22 +101,17 @@ async function shellExecTool(args, context) {
60
101
  commandId,
61
102
  cwd: context.productDir,
62
103
  raw: command,
63
- executable: shellCommand.executableCommand,
64
- normalized: shellCommand.normalized,
65
104
  });
66
105
  context.onEvent?.({
67
106
  type: "tool-start",
68
107
  tool: "shell_exec",
69
- command: shellCommand.executableCommand,
70
- rawCommand: command,
108
+ command,
71
109
  title: shellCommand.title,
72
110
  testkitRelated: shellCommand.testkitRelated,
73
- message: shellCommand.normalized
74
- ? `Running ${shellCommand.displayCommand} (${shellCommand.normalizationReason})`
75
- : `Running ${shellCommand.displayCommand}`,
111
+ message: `Running ${shellCommand.display}`,
76
112
  });
77
113
 
78
- const result = await execaCommand(shellCommand.executableCommand, {
114
+ const result = await execaCommand(command, {
79
115
  cwd: context.productDir,
80
116
  reject: false,
81
117
  shell: true,
@@ -93,21 +129,18 @@ async function shellExecTool(args, context) {
93
129
  commandId,
94
130
  cwd: context.productDir,
95
131
  raw: command,
96
- executable: shellCommand.executableCommand,
97
- normalized: shellCommand.normalized,
98
132
  code: result.exitCode ?? 0,
99
133
  signal: result.signal ?? null,
100
134
  });
101
135
  context.onEvent?.({
102
136
  type: "tool-exit",
103
137
  tool: "shell_exec",
104
- command: shellCommand.executableCommand,
105
- rawCommand: command,
138
+ command,
106
139
  title: shellCommand.title,
107
140
  testkitRelated: shellCommand.testkitRelated,
108
141
  code: result.exitCode ?? 0,
109
142
  signal: result.signal ?? null,
110
- message: `${shellCommand.displayCommand} exited ${result.exitCode ?? 0}`,
143
+ message: `${shellCommand.display} exited ${result.exitCode ?? 0}`,
111
144
  });
112
145
 
113
146
  if (shellCommand.testkitRelated) {
@@ -115,15 +148,13 @@ async function shellExecTool(args, context) {
115
148
  }
116
149
  context.commandLog?.refresh?.();
117
150
 
118
- const lines = formatCommandResult(result, shellCommand);
151
+ const lines = formatCommandResult(command, result, shellCommand);
119
152
  return {
120
153
  ok: (result.exitCode ?? 0) === 0,
121
154
  title: shellCommand.title,
122
155
  text: lines.join("\n"),
123
156
  data: {
124
157
  command,
125
- executableCommand: shellCommand.executableCommand,
126
- normalizedCommand: shellCommand.normalized,
127
158
  stdout: result.stdout || "",
128
159
  stderr: result.stderr || "",
129
160
  exitCode: result.exitCode ?? 0,
@@ -133,22 +164,179 @@ async function shellExecTool(args, context) {
133
164
  };
134
165
  }
135
166
 
167
+ async function runTestsTool(args, context) {
168
+ const invocationId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
169
+ const normalizedArgs = normalizeRunToolArgs(args, context.productDir);
170
+ const rawCommand = buildRunCommandLine(normalizedArgs);
171
+ const request = await buildRunRequest(normalizedArgs, null, context.productDir, context.productDir);
172
+ const runSession = createRunSession({
173
+ productDir: request.productDir,
174
+ stderr: process.stderr,
175
+ config: loadCliConfig(request.productDir),
176
+ });
177
+ context.commandLog?.appendCommandLog({
178
+ type: "command_start",
179
+ command: "testkit",
180
+ commandId: invocationId,
181
+ cwd: context.productDir,
182
+ raw: rawCommand,
183
+ });
184
+ context.onEvent?.({
185
+ type: "tool-start",
186
+ tool: "run_tests",
187
+ invocationId,
188
+ title: buildRunToolTitle(normalizedArgs),
189
+ testkitRelated: true,
190
+ message: `Running ${buildRunDisplay(normalizedArgs)}`,
191
+ });
192
+ context.onEvent?.({
193
+ type: "run-session-start",
194
+ tool: "run_tests",
195
+ invocationId,
196
+ session: runSession,
197
+ });
198
+
199
+ const previousExitCode = process.exitCode;
200
+ try {
201
+ const result = await executeRunRequest(request, {
202
+ outputMode: "compact",
203
+ terminal: {
204
+ stdout: process.stdout,
205
+ stderr: process.stderr,
206
+ stdin: process.stdin,
207
+ env: context.env,
208
+ },
209
+ attachedRunSession: runSession,
210
+ });
211
+ process.exitCode = previousExitCode;
212
+ refreshArtifactSelection(context);
213
+ context.commandLog?.refresh?.();
214
+ context.commandLog?.appendCommandLog({
215
+ type: "command_exit",
216
+ command: "testkit",
217
+ commandId: invocationId,
218
+ cwd: context.productDir,
219
+ raw: rawCommand,
220
+ code: result.exitCode ?? 0,
221
+ signal: null,
222
+ });
223
+ const text = formatRunSessionText(runSession);
224
+ context.onEvent?.({
225
+ type: "tool-exit",
226
+ tool: "run_tests",
227
+ invocationId,
228
+ title: buildRunToolTitle(normalizedArgs),
229
+ testkitRelated: true,
230
+ code: result.exitCode ?? 0,
231
+ message: `${buildRunDisplay(normalizedArgs)} exited ${result.exitCode ?? 0}`,
232
+ });
233
+ context.onEvent?.({
234
+ type: "run-session-end",
235
+ tool: "run_tests",
236
+ invocationId,
237
+ session: runSession,
238
+ });
239
+ return {
240
+ ok: (result.exitCode ?? 0) === 0,
241
+ title: buildRunToolTitle(normalizedArgs),
242
+ text,
243
+ data: {
244
+ exitCode: result.exitCode ?? 0,
245
+ testkitRelated: true,
246
+ summaryRows: runSession.getSnapshot().summaryData?.rows || [],
247
+ },
248
+ };
249
+ } finally {
250
+ process.exitCode = previousExitCode;
251
+ }
252
+ }
253
+
254
+ async function discoverTestsTool(args, context) {
255
+ const flags = {
256
+ dir: context.productDir,
257
+ service: normalizeOptionalString(args.service),
258
+ type: normalizeStringArray(args.type),
259
+ suite: normalizeStringArray(args.suite),
260
+ file: normalizeStringArray(args.file || args.path),
261
+ "runnable-only": Boolean(args.runnableOnly || args["runnable-only"]),
262
+ strict: Boolean(args.strict),
263
+ output: normalizeOptionalString(args.output),
264
+ };
265
+ const result = await executeDiscoverOperation(flags, context.productDir);
266
+ return {
267
+ ok: true,
268
+ title: "testkit discover",
269
+ text: renderDiscoverResult(result, { outputMode: args.outputMode || "compact" }).join("\n"),
270
+ data: {
271
+ testkitRelated: true,
272
+ services: result.services?.length || 0,
273
+ files: result.files?.length || 0,
274
+ },
275
+ };
276
+ }
277
+
278
+ async function showStatusTool(args, context) {
279
+ const flags = {
280
+ dir: context.productDir,
281
+ service: normalizeOptionalString(args.service),
282
+ };
283
+ const results = await executeStatusOperation(flags);
284
+ return {
285
+ ok: true,
286
+ title: "testkit status",
287
+ text: results.flatMap(renderStatusResult).join("\n"),
288
+ data: {
289
+ testkitRelated: true,
290
+ services: results.map((result) => result.name),
291
+ },
292
+ };
293
+ }
294
+
295
+ async function doctorTool(args, context) {
296
+ const result = await executeDoctorOperation({
297
+ dir: context.productDir,
298
+ typecheck: args.typecheck == null ? true : Boolean(args.typecheck),
299
+ });
300
+ return {
301
+ ok: result.ok,
302
+ title: "testkit doctor",
303
+ text: renderDoctorResult(result).join("\n"),
304
+ data: {
305
+ testkitRelated: true,
306
+ ok: result.ok,
307
+ },
308
+ };
309
+ }
310
+
311
+ async function typecheckTool(_args, context) {
312
+ const result = await executeTypecheckOperation({ dir: context.productDir });
313
+ return {
314
+ ok: true,
315
+ title: "testkit typecheck",
316
+ text: renderTypecheckResult(result).join("\n"),
317
+ data: {
318
+ testkitRelated: true,
319
+ count: result.results.length,
320
+ },
321
+ };
322
+ }
323
+
136
324
  function readContextTool(args, context) {
137
325
  if (args.file || args.path) {
138
326
  ensureArtifactLoaded(context);
139
- const artifact = context.inspectState.getSnapshot().runArtifact;
327
+ const artifact = context.runState.getSnapshot().runArtifact;
140
328
  const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
141
- context.inspectState.revealFile(subject.service.name, subject.file.path);
329
+ context.runState.revealFile(subject.service.name, subject.file.path);
142
330
  } else if (args.service) {
143
331
  ensureArtifactLoaded(context);
144
- if (!context.inspectState.revealService(args.service)) {
332
+ if (!context.runState.revealService(args.service)) {
145
333
  throw new Error(`Unknown service "${args.service}"`);
146
334
  }
147
335
  }
148
336
 
149
337
  const content = readContextContent({
150
338
  productDir: context.productDir,
151
- snapshot: context.inspectState.getSnapshot(),
339
+ snapshot: context.runState.getSnapshot(),
152
340
  mode: normalizeContextMode(args.mode),
153
341
  logTail: args.logTail == null ? 12 : Number(args.logTail),
154
342
  });
@@ -227,11 +415,23 @@ async function searchRepoTool(args, context) {
227
415
  };
228
416
  }
229
417
 
230
- function formatCommandResult(result, shellCommand) {
231
- const lines = [`$ ${shellCommand.displayCommand}`];
232
- if (shellCommand.normalized) {
233
- lines.push(`normalized from: ${shellCommand.rawCommand}`);
234
- }
418
+ function classifyShellCommand(command) {
419
+ const normalized = command.trim();
420
+ return {
421
+ command: normalized.split(/\s+/)[0] || "command",
422
+ display: normalized,
423
+ title: "Shell command",
424
+ testkitRelated: false,
425
+ };
426
+ }
427
+
428
+ function isTestkitCommand(command) {
429
+ const normalized = command.trim();
430
+ return /^(testkit)\b/.test(normalized) || /^(npx)\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit:/.test(normalized);
431
+ }
432
+
433
+ function formatCommandResult(command, result, shellCommand) {
434
+ const lines = [`$ ${command}`];
235
435
  const stdout = (result.stdout || "").trim();
236
436
  const stderr = (result.stderr || "").trim();
237
437
  const merged = [];
@@ -265,6 +465,92 @@ function resolveRepoPath(productDir, file) {
265
465
  return path.resolve(productDir, file);
266
466
  }
267
467
 
468
+ function normalizeOptionalString(value) {
469
+ if (typeof value !== "string") return null;
470
+ const normalized = value.trim();
471
+ return normalized.length > 0 ? normalized : null;
472
+ }
473
+
474
+ function normalizeStringArray(value) {
475
+ if (Array.isArray(value)) return value.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
476
+ if (value == null) return [];
477
+ return String(value).split(",").map((entry) => entry.trim()).filter(Boolean);
478
+ }
479
+
480
+ function normalizeRunToolArgs(args, productDir) {
481
+ return {
482
+ dir: productDir,
483
+ service: normalizeOptionalString(args.service),
484
+ type: normalizeStringArray(args.type),
485
+ suite: normalizeStringArray(args.suite),
486
+ file: normalizeStringArray(args.file || args.path),
487
+ workers: normalizeOptionalString(args.workers),
488
+ "file-timeout-seconds": normalizeOptionalString(args.fileTimeoutSeconds || args["file-timeout-seconds"]),
489
+ shard: normalizeOptionalString(args.shard),
490
+ seed: normalizeOptionalString(args.seed),
491
+ "write-status": Boolean(args.writeStatus || args["write-status"]),
492
+ "allow-partial-status": Boolean(args.allowPartialStatus || args["allow-partial-status"]),
493
+ "ignore-skip-rules": Boolean(args.ignoreSkipRules || args["ignore-skip-rules"]),
494
+ "output-mode": "compact",
495
+ debug: false,
496
+ };
497
+ }
498
+
499
+ function buildRunDisplay(args) {
500
+ return buildRunCommandLine(args);
501
+ }
502
+
503
+ function buildRunToolTitle(args) {
504
+ const types = args.type?.length ? args.type.join(",") : "all";
505
+ return `testkit run ${types}`;
506
+ }
507
+
508
+ function formatRunSessionText(runSession) {
509
+ const snapshot = runSession.getSnapshot();
510
+ const fileLines = collectRunTreeFiles(snapshot.services || [])
511
+ .map((entry) => `${formatRunStatus(entry.status)} ${entry.serviceName} ${entry.type} ${entry.filePath || entry.path}`);
512
+ const rows = snapshot.summaryData?.rows || [];
513
+ const summaryLines = rows.map(([label, value]) => `${label}: ${value}`);
514
+ const lines = [...fileLines, ...summaryLines];
515
+ return lines.length > 0 ? lines.join("\n") : "Run complete.";
516
+ }
517
+
518
+ function collectRunTreeFiles(services) {
519
+ const files = [];
520
+ for (const service of services || []) {
521
+ for (const typeNode of service.types || []) {
522
+ for (const suite of typeNode.suites || []) {
523
+ for (const file of suite.files || []) {
524
+ files.push(file);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ return files;
530
+ }
531
+
532
+ function formatRunStatus(status) {
533
+ if (status === "passed") return "PASS";
534
+ if (status === "failed") return "FAIL";
535
+ if (status === "skipped") return "SKIP";
536
+ if (status === "running") return "RUN";
537
+ if (status === "not_run") return "NOT_RUN";
538
+ return "PENDING";
539
+ }
540
+
541
+ function buildRunCommandLine(args) {
542
+ const parts = ["testkit", "run"];
543
+ if (args.type?.length === 1) parts.push(args.type[0]);
544
+ parts.push("--dir", ".");
545
+ if (args.type?.length !== 1) {
546
+ for (const type of args.type || []) parts.push("--type", type);
547
+ }
548
+ for (const suite of args.suite || []) parts.push("--suite", suite);
549
+ for (const file of args.file || []) parts.push("--file", file);
550
+ if (args.service) parts.push("--service", args.service);
551
+ return parts.join(" ");
552
+ }
553
+
268
554
  function shellQuote(value) {
269
555
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
270
556
  }
@@ -275,22 +561,22 @@ function normalizeContextMode(mode) {
275
561
  }
276
562
 
277
563
  function ensureArtifactLoaded(context) {
278
- const snapshot = context.inspectState.getSnapshot();
564
+ const snapshot = context.runState.getSnapshot();
279
565
  if (snapshot.runArtifact) return snapshot.runArtifact;
280
566
  try {
281
- context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
567
+ context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
282
568
  } catch {
283
- context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
569
+ context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
284
570
  }
285
- return context.inspectState.getSnapshot().runArtifact;
571
+ return context.runState.getSnapshot().runArtifact;
286
572
  }
287
573
 
288
574
  function refreshArtifactSelection(context) {
289
575
  try {
290
- context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
576
+ context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
291
577
  } catch {
292
578
  try {
293
- context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
579
+ context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
294
580
  } catch {
295
581
  // Ignore missing artifacts.
296
582
  }
@@ -23,7 +23,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
23
23
  }
24
24
 
25
25
  export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
26
- const summaryRows = snapshot?.inspect?.summaryData?.rows || snapshot?.summaryData?.rows || [];
26
+ const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
27
27
  const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
28
28
  const contextSelection = snapshot?.context?.selection || {};
29
29
  const latestResult = rowValue("Result");
@@ -1,5 +1,7 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
- import { sharedFlags, resolveConfigsForCommand } from "../command-helpers.mjs";
2
+ import { loadManagedConfigs } from "../../app/configs.mjs";
3
+ import { loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
4
+ import { sharedFlags } from "./flags.mjs";
3
5
  import { createAssistantState } from "../assistant/state.mjs";
4
6
  import { runInteractiveAssistant } from "../assistant/interactive.mjs";
5
7
 
@@ -44,7 +46,7 @@ export default class AssistantCommand extends Command {
44
46
  if (flags.message && flags.prompt) {
45
47
  this.error("Use either --message or --prompt, not both.");
46
48
  }
47
- const { allConfigs } = await resolveConfigsForCommand(flags);
49
+ const { allConfigs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
48
50
  const productDir = allConfigs[0]?.productDir || process.cwd();
49
51
  const interactive =
50
52
  (process.stdout.isTTY || process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1") &&
@@ -70,7 +72,6 @@ export default class AssistantCommand extends Command {
70
72
  await assistantState.loadLatestArtifact();
71
73
  if (flags.file) {
72
74
  try {
73
- const { loadLatestRunArtifact, resolveFileSubject } = await import("../viewer.mjs");
74
75
  const artifact = loadLatestRunArtifact(productDir);
75
76
  const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
76
77
  assistantState.revealFile(subject.service.name, subject.file.path);
@@ -1,6 +1,6 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
- import { startBrowserBridgeServer } from "@elench/testkit-bridge";
3
- import { loadBrowserBridgeContext } from "../../../app/browser-bridge.mjs";
2
+ import { executeBrowserServeOperation } from "../../operations/browser/serve/operation.mjs";
3
+ import { renderBrowserServeResult } from "../../renderers/browser-serve/text.mjs";
4
4
 
5
5
  export default class BrowserServeCommand extends Command {
6
6
  static summary = "Serve the local browser bridge for the current testkit product";
@@ -23,31 +23,13 @@ export default class BrowserServeCommand extends Command {
23
23
 
24
24
  async run() {
25
25
  const { flags } = await this.parse(BrowserServeCommand);
26
- const { productDir, context } = await loadBrowserBridgeContext({ dir: flags.dir });
27
-
28
- const adapter = {
29
- loadProductContext: async () => context,
30
- };
31
-
32
- const serverRef = await startBrowserBridgeServer(adapter, {
33
- host: flags.host,
34
- port: flags.port,
35
- });
36
-
37
- const payload = {
38
- ok: true,
39
- productDir,
40
- host: serverRef.host,
41
- port: serverRef.port,
42
- url: serverRef.url,
43
- };
26
+ const result = await executeBrowserServeOperation(flags);
44
27
 
45
28
  if (!this.jsonEnabled()) {
46
- this.log(`testkit browser bridge serving ${productDir}`);
47
- this.log(`Listening on ${serverRef.url}`);
29
+ for (const line of renderBrowserServeResult(result)) this.log(line);
48
30
  }
49
31
 
50
32
  await new Promise(() => {});
51
- return payload;
33
+ return result;
52
34
  }
53
35
  }
@@ -1,5 +1,7 @@
1
1
  import { Command } from "@oclif/core";
2
- import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
2
+ import { sharedFlags } from "./flags.mjs";
3
+ import { executeCleanupOperation } from "../operations/cleanup/operation.mjs";
4
+ import { renderCleanupResult } from "../renderers/cleanup/text.mjs";
3
5
 
4
6
  export default class CleanupCommand extends Command {
5
7
  static summary = "Clean stale local testkit state";
@@ -10,6 +12,10 @@ export default class CleanupCommand extends Command {
10
12
 
11
13
  async run() {
12
14
  const { flags } = await this.parse(CleanupCommand);
13
- return runStatusLike("cleanup", flags);
15
+ const result = await executeCleanupOperation(flags);
16
+ if (!this.jsonEnabled()) {
17
+ for (const line of renderCleanupResult(result)) this.log(line);
18
+ }
19
+ return { ok: true, ...result };
14
20
  }
15
21
  }
@@ -1,6 +1,7 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
- import { sharedFlags } from "../../../command-helpers.mjs";
3
- import { runDatabaseSnapshotCaptureCommand } from "../../../db.mjs";
2
+ import { sharedFlags } from "../../flags.mjs";
3
+ import { executeDatabaseSnapshotCaptureOperation } from "../../../operations/db/snapshot/capture/operation.mjs";
4
+ import { renderDatabaseSnapshotCaptureResult } from "../../../renderers/db-snapshot-capture/text.mjs";
4
5
 
5
6
  export default class DbSnapshotCaptureCommand extends Command {
6
7
  static summary = "Capture a database schema snapshot";
@@ -16,7 +17,10 @@ export default class DbSnapshotCaptureCommand extends Command {
16
17
 
17
18
  async run() {
18
19
  const { flags } = await this.parse(DbSnapshotCaptureCommand);
19
- await runDatabaseSnapshotCaptureCommand(flags);
20
- return { ok: true };
20
+ const result = await executeDatabaseSnapshotCaptureOperation(flags);
21
+ if (!this.jsonEnabled()) {
22
+ for (const line of renderDatabaseSnapshotCaptureResult(result)) this.log(line);
23
+ }
24
+ return result;
21
25
  }
22
26
  }
@@ -1,5 +1,7 @@
1
1
  import { Command } from "@oclif/core";
2
- import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
2
+ import { sharedFlags } from "./flags.mjs";
3
+ import { executeDestroyOperation } from "../operations/destroy/operation.mjs";
4
+ import { renderDestroyResult } from "../renderers/destroy/text.mjs";
3
5
 
4
6
  export default class DestroyCommand extends Command {
5
7
  static summary = "Destroy local testkit state";
@@ -10,6 +12,10 @@ export default class DestroyCommand extends Command {
10
12
 
11
13
  async run() {
12
14
  const { flags } = await this.parse(DestroyCommand);
13
- return runStatusLike("destroy", flags);
15
+ const results = await executeDestroyOperation(flags);
16
+ if (!this.jsonEnabled()) {
17
+ for (const line of renderDestroyResult(results)) this.log(line);
18
+ }
19
+ return { ok: true, results };
14
20
  }
15
21
  }
@@ -1,11 +1,7 @@
1
- import fs from "fs";
2
- import path from "path";
3
1
  import { Command, Flags } from "@oclif/core";
4
- import { discoverTests } from "../../discovery/index.mjs";
5
- import { resolveProductDir } from "../../config/index.mjs";
6
- import { buildDiscoveryReportLines } from "../presentation/discovery-reporter.mjs";
7
- import { resolveRequestedFiles } from "../args.mjs";
8
- import { sharedFlags } from "../command-helpers.mjs";
2
+ import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
3
+ import { renderDiscoverResult } from "../renderers/discover/text.mjs";
4
+ import { sharedFlags } from "./flags.mjs";
9
5
 
10
6
  export default class DiscoverCommand extends Command {
11
7
  static summary = "Discover managed tests and report their metadata";
@@ -49,30 +45,12 @@ export default class DiscoverCommand extends Command {
49
45
 
50
46
  async run() {
51
47
  const { flags } = await this.parse(DiscoverCommand);
52
- const productDir = resolveProductDir(process.cwd(), flags.dir);
53
- const fileNames = resolveRequestedFiles(flags.file || [], productDir, process.cwd());
54
- const result = await discoverTests({
55
- dir: productDir,
56
- service: flags.service || null,
57
- type: flags.type || [],
58
- suite: flags.suite || [],
59
- file: fileNames,
60
- runnableOnly: flags["runnable-only"],
61
- diagnostics: flags.strict ? "error" : "report",
62
- });
63
- let outputLabel = null;
64
- if (flags.output) {
65
- const outputPath = path.resolve(productDir, flags.output);
66
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
67
- fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
68
- outputLabel = path.relative(productDir, outputPath) || path.basename(outputPath);
69
- }
48
+ const result = await executeDiscoverOperation(flags, process.cwd());
70
49
 
71
50
  if (!this.jsonEnabled()) {
72
- for (const line of buildDiscoveryReportLines(result, { outputMode: flags["output-mode"] })) {
51
+ for (const line of renderDiscoverResult(result, { outputMode: flags["output-mode"] })) {
73
52
  this.log(line);
74
53
  }
75
- if (outputLabel) this.log(`Wrote ${outputLabel}`);
76
54
  }
77
55
 
78
56
  return result;