@elench/testkit 0.1.97 → 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 (74) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +25 -1
  3. package/lib/cli/assistant/composer.mjs +1 -1
  4. package/lib/cli/assistant/context-pack.mjs +4 -4
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/prompt-builder.mjs +2 -2
  7. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  8. package/lib/cli/assistant/session.mjs +5 -5
  9. package/lib/cli/assistant/slash-commands.mjs +22 -1
  10. package/lib/cli/assistant/state.mjs +148 -75
  11. package/lib/cli/assistant/tool-registry.mjs +305 -39
  12. package/lib/cli/assistant/view-model.mjs +1 -1
  13. package/lib/cli/commands/assistant.mjs +4 -3
  14. package/lib/cli/commands/browser/serve.mjs +5 -23
  15. package/lib/cli/commands/cleanup.mjs +8 -2
  16. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  17. package/lib/cli/commands/destroy.mjs +8 -2
  18. package/lib/cli/commands/discover.mjs +5 -27
  19. package/lib/cli/commands/doctor.mjs +5 -5
  20. package/lib/cli/commands/flags.mjs +61 -0
  21. package/lib/cli/commands/run.mjs +10 -2
  22. package/lib/cli/commands/status.mjs +10 -2
  23. package/lib/cli/commands/typecheck.mjs +5 -5
  24. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  25. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  26. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  27. package/lib/cli/config.mjs +63 -0
  28. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  29. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  30. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  31. package/lib/cli/operations/destroy/operation.mjs +12 -0
  32. package/lib/cli/operations/discover/operation.mjs +32 -0
  33. package/lib/cli/operations/doctor/operation.mjs +5 -0
  34. package/lib/cli/operations/run/operation.mjs +129 -0
  35. package/lib/cli/operations/status/operation.mjs +7 -0
  36. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  37. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  38. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  39. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  40. package/lib/cli/renderers/destroy/text.mjs +3 -0
  41. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  42. package/lib/cli/renderers/discover/text.mjs +7 -0
  43. package/lib/cli/renderers/doctor/text.mjs +7 -0
  44. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  45. package/lib/cli/renderers/run/interactive.mjs +119 -0
  46. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  47. package/lib/cli/renderers/status/text.mjs +7 -0
  48. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  49. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  50. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  51. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  52. package/lib/cli/terminal/capabilities.mjs +33 -0
  53. package/lib/database/index.mjs +9 -21
  54. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  55. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  56. package/lib/runner/maintenance.mjs +25 -14
  57. package/lib/runner/readiness.mjs +5 -4
  58. package/lib/runner/state-io.mjs +10 -4
  59. package/node_modules/@elench/next-analysis/package.json +1 -1
  60. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  61. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  62. package/node_modules/@elench/ts-analysis/package.json +1 -1
  63. package/package.json +6 -7
  64. package/lib/cli/command-helpers.mjs +0 -191
  65. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  66. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  67. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  68. /package/lib/cli/{agents → assistant}/providers/claude.mjs +0 -0
  69. /package/lib/cli/{agents → assistant}/providers/codex.mjs +0 -0
  70. /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
  71. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  72. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  73. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  74. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -1,10 +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";
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";
8
17
 
9
18
  const COMMAND_OUTPUT_LIMIT = 14_000;
10
19
  const COMMAND_LINE_LIMIT = 220;
@@ -12,9 +21,29 @@ const FILE_LINE_LIMIT = 160;
12
21
 
13
22
  export function listAssistantTools() {
14
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
+ },
15
44
  {
16
45
  name: "shell_exec",
17
- description: "Execute a shell command inside the repository. Prefer real repo commands such as npm, npx, and testkit.",
46
+ description: "Execute a shell command inside the repository for arbitrary repo work outside first-class testkit actions.",
18
47
  },
19
48
  {
20
49
  name: "read_context",
@@ -34,6 +63,16 @@ export function listAssistantTools() {
34
63
  export async function executeAssistantTool(name, argumentsObject, context) {
35
64
  const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
36
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);
37
76
  case "shell_exec":
38
77
  return shellExecTool(args, context);
39
78
  case "read_context":
@@ -50,6 +89,9 @@ export async function executeAssistantTool(name, argumentsObject, context) {
50
89
  async function shellExecTool(args, context) {
51
90
  const command = String(args.command || "").trim();
52
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
+ }
53
95
 
54
96
  const shellCommand = classifyShellCommand(command);
55
97
  const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
@@ -122,22 +164,179 @@ async function shellExecTool(args, context) {
122
164
  };
123
165
  }
124
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
+
125
324
  function readContextTool(args, context) {
126
325
  if (args.file || args.path) {
127
326
  ensureArtifactLoaded(context);
128
- const artifact = context.inspectState.getSnapshot().runArtifact;
327
+ const artifact = context.runState.getSnapshot().runArtifact;
129
328
  const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
130
- context.inspectState.revealFile(subject.service.name, subject.file.path);
329
+ context.runState.revealFile(subject.service.name, subject.file.path);
131
330
  } else if (args.service) {
132
331
  ensureArtifactLoaded(context);
133
- if (!context.inspectState.revealService(args.service)) {
332
+ if (!context.runState.revealService(args.service)) {
134
333
  throw new Error(`Unknown service "${args.service}"`);
135
334
  }
136
335
  }
137
336
 
138
337
  const content = readContextContent({
139
338
  productDir: context.productDir,
140
- snapshot: context.inspectState.getSnapshot(),
339
+ snapshot: context.runState.getSnapshot(),
141
340
  mode: normalizeContextMode(args.mode),
142
341
  logTail: args.logTail == null ? 12 : Number(args.logTail),
143
342
  });
@@ -218,30 +417,6 @@ async function searchRepoTool(args, context) {
218
417
 
219
418
  function classifyShellCommand(command) {
220
419
  const normalized = command.trim();
221
- if (/^(testkit)\b/.test(normalized)) {
222
- return {
223
- command: "testkit",
224
- display: normalized,
225
- title: "testkit command",
226
- testkitRelated: true,
227
- };
228
- }
229
- if (/^(npx)\s+testkit\b/.test(normalized)) {
230
- return {
231
- command: "npx testkit",
232
- display: normalized,
233
- title: "npx testkit",
234
- testkitRelated: true,
235
- };
236
- }
237
- if (/^(npm)\s+run\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit:/.test(normalized)) {
238
- return {
239
- command: "npm run testkit",
240
- display: normalized,
241
- title: "npm testkit script",
242
- testkitRelated: true,
243
- };
244
- }
245
420
  return {
246
421
  command: normalized.split(/\s+/)[0] || "command",
247
422
  display: normalized,
@@ -250,6 +425,11 @@ function classifyShellCommand(command) {
250
425
  };
251
426
  }
252
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
+
253
433
  function formatCommandResult(command, result, shellCommand) {
254
434
  const lines = [`$ ${command}`];
255
435
  const stdout = (result.stdout || "").trim();
@@ -285,6 +465,92 @@ function resolveRepoPath(productDir, file) {
285
465
  return path.resolve(productDir, file);
286
466
  }
287
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
+
288
554
  function shellQuote(value) {
289
555
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
290
556
  }
@@ -295,22 +561,22 @@ function normalizeContextMode(mode) {
295
561
  }
296
562
 
297
563
  function ensureArtifactLoaded(context) {
298
- const snapshot = context.inspectState.getSnapshot();
564
+ const snapshot = context.runState.getSnapshot();
299
565
  if (snapshot.runArtifact) return snapshot.runArtifact;
300
566
  try {
301
- context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
567
+ context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
302
568
  } catch {
303
- context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
569
+ context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
304
570
  }
305
- return context.inspectState.getSnapshot().runArtifact;
571
+ return context.runState.getSnapshot().runArtifact;
306
572
  }
307
573
 
308
574
  function refreshArtifactSelection(context) {
309
575
  try {
310
- context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
576
+ context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
311
577
  } catch {
312
578
  try {
313
- context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
579
+ context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
314
580
  } catch {
315
581
  // Ignore missing artifacts.
316
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;
@@ -1,5 +1,6 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
- import { runDoctor } from "../../app/doctor.mjs";
2
+ import { executeDoctorOperation } from "../operations/doctor/operation.mjs";
3
+ import { renderDoctorResult } from "../renderers/doctor/text.mjs";
3
4
 
4
5
  export default class DoctorCommand extends Command {
5
6
  static summary = "Run built-in config, discovery, and hygiene checks";
@@ -19,12 +20,11 @@ export default class DoctorCommand extends Command {
19
20
 
20
21
  async run() {
21
22
  const { flags } = await this.parse(DoctorCommand);
22
- const result = await runDoctor({ dir: flags.dir, typecheck: flags.typecheck });
23
+ const result = await executeDoctorOperation(flags);
23
24
 
24
25
  if (!this.jsonEnabled()) {
25
- this.log(`testkit doctor ${result.ok ? "passed" : "failed"} for ${result.productDir}`);
26
- for (const check of result.checks) {
27
- this.log(`${check.level.toUpperCase()} ${check.code} ${check.message}`);
26
+ for (const line of renderDoctorResult(result)) {
27
+ this.log(line);
28
28
  }
29
29
  }
30
30