@pugi/cli 0.1.0-beta.31 → 0.1.0-beta.36

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 (45) hide show
  1. package/dist/commands/smoke.js +133 -0
  2. package/dist/core/auth/ensure-authenticated.js +129 -0
  3. package/dist/core/bash-classifier.js +108 -1
  4. package/dist/core/codegraph/decision-store.js +248 -0
  5. package/dist/core/codegraph/detect-repo.js +459 -0
  6. package/dist/core/codegraph/install.js +134 -0
  7. package/dist/core/codegraph/offer-hook.js +220 -0
  8. package/dist/core/diagnostics/probes/status-snapshot.js +50 -4
  9. package/dist/core/mcp/orchestrator-tools.js +595 -0
  10. package/dist/core/onboarding/ensure-initialized.js +133 -0
  11. package/dist/core/repl/session.js +370 -9
  12. package/dist/core/repl/slash-commands.js +68 -5
  13. package/dist/core/smoke/headless-driver.js +174 -0
  14. package/dist/core/smoke/orchestrator.js +194 -0
  15. package/dist/core/smoke/runner.js +238 -0
  16. package/dist/core/smoke/scenario-parser.js +316 -0
  17. package/dist/runtime/cli.js +453 -11
  18. package/dist/runtime/commands/cancel.js +231 -0
  19. package/dist/runtime/commands/codegraph-status.js +227 -0
  20. package/dist/runtime/commands/mcp.js +66 -11
  21. package/dist/runtime/commands/permissions.js +23 -0
  22. package/dist/runtime/commands/redo-blob-store.js +92 -0
  23. package/dist/runtime/commands/redo.js +361 -0
  24. package/dist/runtime/commands/status.js +11 -3
  25. package/dist/runtime/commands/undo.js +32 -0
  26. package/dist/runtime/headless-repl.js +195 -0
  27. package/dist/runtime/version.js +1 -1
  28. package/dist/tui/permissions-picker.js +78 -0
  29. package/dist/tui/render.js +35 -0
  30. package/dist/tui/status-bar.js +1 -1
  31. package/dist/tui/tool-stream-pane.js +45 -3
  32. package/package.json +7 -4
  33. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  34. package/test/scenarios/compact-force.scenario.txt +11 -0
  35. package/test/scenarios/identity.scenario.txt +11 -0
  36. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  37. package/test/scenarios/walkback.scenario.txt +12 -0
  38. package/dist/core/engine/compaction-hook.js +0 -154
  39. package/dist/core/init/scaffold.js +0 -195
  40. package/dist/core/memory/dual-write.spec.js +0 -297
  41. package/dist/core/memory-sync/queue.spec.js +0 -105
  42. package/dist/core/repl/codebase-survey.js +0 -308
  43. package/dist/core/repl/init-interview.js +0 -457
  44. package/dist/core/repl/onboarding-state.js +0 -297
  45. package/dist/runtime/commands/memory.spec.js +0 -174
@@ -63,7 +63,8 @@ export const SLASH_COMMAND_HELP = Object.freeze([
63
63
  { name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
64
64
  { name: 'delegate', args: '<slug> <brief>', gloss: 'Dispatch a brief to one Tier 1 specialist (α7.5)', group: 'Workforce dispatch' },
65
65
  { name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
66
- { name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
66
+ { name: 'jobs', args: '[--watch]', gloss: 'List background jobs + agent-progress; --watch mounts the live Ink TUI', group: 'Workforce dispatch' },
67
+ { name: 'cancel', args: '[<id> | all]', gloss: 'Halt active dispatch by id (Wave 6)', group: 'Workforce dispatch' },
67
68
  { name: 'ask', args: '<question>', gloss: 'Surface a yes/no modal locally (α6.3 forcing question)', group: 'Workforce dispatch' },
68
69
  // Session
69
70
  { name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
@@ -81,6 +82,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
81
82
  { name: 'status', args: '', gloss: 'Session snapshot — id · cwd · mode · tokens · dispatches · auth', group: 'Pugi tools' },
82
83
  { name: 'consensus', args: '[ref]', gloss: '3-model consensus review (codex · claude · deepseek)', group: 'Pugi tools' },
83
84
  { name: 'repo-map', args: '[refresh]', gloss: 'AST-light symbol summary of the workspace (leak L28)', group: 'Pugi tools' },
85
+ { name: 'codegraph-status', args: '[--install|--reindex|--offer]', gloss: 'Codegraph MCP — install state, index age, symbol count, refresh CTA (Wave 6 BT 9 P2)', group: 'Pugi tools' },
84
86
  // Settings
85
87
  { name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
86
88
  { name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
@@ -94,6 +96,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
94
96
  { name: 'onboarding', args: '[--reset|--non-interactive]', gloss: 'First-run wizard — auth / mode / style / MCP / telemetry (leak L25)', group: 'Settings' },
95
97
  { name: 'vim', args: '[on|off|status]', gloss: 'Toggle vim-style modal editing in the input buffer (leak L26)', group: 'Settings' },
96
98
  { name: 'undo', args: '', gloss: 'Revert the last successful write / edit / multi_edit (Aider walk-back, Wave 6)', group: 'Settings' },
99
+ { name: 'redo', args: '', gloss: 'Reapply the most recent /undo (LIFO stack, Wave 6 cleanup)', group: 'Settings' },
97
100
  // Meta
98
101
  { name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
99
102
  { name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
@@ -231,7 +234,48 @@ export function parseSlashCommand(input) {
231
234
  return { kind: 'version' };
232
235
  }
233
236
  case 'jobs': {
234
- return { kind: 'jobs' };
237
+ // Wave 6 cleanup (2026-05-27): tokenise the tail so the slash
238
+ // can route `--watch` к the live Ink TUI (same renderer as
239
+ // `pugi jobs --watch`). Unknown tokens fall through silently —
240
+ // the slash surface is intentionally minimal vs. the shell
241
+ // command (which supports list/status/tail/kill subcommands
242
+ // through `runJobsCommand`).
243
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
244
+ const watch = tokens.includes('--watch') || tokens.includes('-w') || tokens[0] === 'watch';
245
+ return { kind: 'jobs', watch };
246
+ }
247
+ case 'cancel':
248
+ case 'halt': {
249
+ // Wave 6 small-CC-parity batch (2026-05-27):
250
+ //
251
+ // /cancel -> list active dispatches
252
+ // /cancel all -> halt every running dispatch
253
+ // /cancel <id> -> halt one (id may be a prefix; runner
254
+ // does startsWith lookup)
255
+ //
256
+ // The `halt` alias matches operator muscle memory from systemd /
257
+ // brand voice (`stop` is already taken by /stop <persona>; cancel
258
+ // is dispatch-id-keyed, stop is persona-keyed). Unknown extra
259
+ // tokens are tolerated — the runner reads only the first.
260
+ const trimmedTail = tail.trim();
261
+ if (trimmedTail.length === 0) {
262
+ return { kind: 'cancel', mode: 'list', dispatchId: '' };
263
+ }
264
+ const firstToken = trimmedTail.split(/\s+/)[0].toLowerCase();
265
+ if (firstToken === 'all' || firstToken === '*') {
266
+ return { kind: 'cancel', mode: 'all', dispatchId: 'all' };
267
+ }
268
+ // Defensive: dispatch ids are filename-safe per the
269
+ // `validateAgentProgress` agentId regex (`[a-zA-Z0-9_-]+`).
270
+ // Reject anything outside that range with a usage tip so the
271
+ // operator sees the typo before the round-trip.
272
+ if (!/^[A-Za-z0-9_-]+$/.test(firstToken)) {
273
+ return {
274
+ kind: 'error',
275
+ message: `/cancel: invalid dispatch id '${firstToken}'. Use letters / digits / '-' / '_' only.`,
276
+ };
277
+ }
278
+ return { kind: 'cancel', mode: 'one', dispatchId: firstToken };
235
279
  }
236
280
  case 'ask': {
237
281
  if (tail.length === 0) {
@@ -477,6 +521,17 @@ export function parseSlashCommand(input) {
477
521
  const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
478
522
  return { kind: 'chain', args: tokens };
479
523
  }
524
+ case 'codegraph-status':
525
+ case 'codegraph': {
526
+ // Wave 6 BT 9 Phase 2 (2026-05-27): forward the tokenized argv
527
+ // to `runCodegraphStatusCommand`. Flags handled by the runner:
528
+ // --install — merge codegraph into .pugi/mcp.json (accept)
529
+ // --reindex — stamp lastIndexedAt + hint runtime to refresh
530
+ // --offer — surface the install prompt even after a decline
531
+ // `/codegraph` is the short alias; same handler.
532
+ const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
533
+ return { kind: 'codegraph-status', args: tokens };
534
+ }
480
535
  case 'compact': {
481
536
  // Leak L8 (2026-05-27): graduated from stub. The session module
482
537
  // owns the summariser round-trip. Wave 6 BT 8: `--force` overrides
@@ -561,11 +616,19 @@ export function parseSlashCommand(input) {
561
616
  // Wave 6 final (2026-05-27): graduated from stub. Tail args are
562
617
  // ignored — `runUndoCommand` is parameterless (single-step revert
563
618
  // of the most recent successful mutating tool result). Multiple
564
- // undos = stack of single-step undos. Re-do is not yet
565
- // implemented; the runner reports that in the operator-facing
566
- // message after each successful undo.
619
+ // undos = stack of single-step undos. `/redo` is the counterpart
620
+ // (Wave 6 cleanup) operators ping-pong через undo/redo on the
621
+ // event-log stack without re-running the underlying tool.
567
622
  return { kind: 'undo' };
568
623
  }
624
+ case 'redo': {
625
+ // Wave 6 cleanup (2026-05-27): counterpart к /undo. Tail args
626
+ // are ignored — `runRedoCommand` is parameterless. Each /redo
627
+ // pops one entry from the LIFO undo stack (the runner tracks
628
+ // which undos have already been consumed by previous redos so
629
+ // double-/redo is a noop, not a double-write).
630
+ return { kind: 'redo' };
631
+ }
569
632
  case 'memory':
570
633
  case 'config':
571
634
  case 'budget': {
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Headless subprocess driver for the smoke harness (Phase 1, 2026-05-27).
3
+ *
4
+ * Spawns `pugi --headless`, feeds the scenario's user-input lines one
5
+ * at a time, and collects every JSON envelope written to stdout. The
6
+ * driver is the bridge between the in-process scenario parser and the
7
+ * real CLI binary — running scenarios via subprocess is what lets
8
+ * `pugi smoke` validate the AS-PUBLISHED CLI, not just the in-process
9
+ * code path.
10
+ *
11
+ * Phase 1 invariants:
12
+ *
13
+ * - One scenario = one subprocess. We do NOT reuse processes across
14
+ * scenarios; each run starts in a fresh temp workspace so file
15
+ * assertions are deterministic.
16
+ *
17
+ * - Stdin is fed line-by-line on a timer (default 200ms gap). We
18
+ * could fire all lines at once, but spacing them out gives the
19
+ * engine a chance to emit `persona-turn` envelopes between user
20
+ * turns. Mirrors real-operator typing cadence — feeds a more
21
+ * realistic test signal than a burst write.
22
+ *
23
+ * - Hard timeout per scenario (default 30s). The driver kills the
24
+ * subprocess on timeout and surfaces a clean executor-error so the
25
+ * report still shows the row instead of hanging CI.
26
+ *
27
+ * - Stderr is captured but NOT parsed. We forward it to the optional
28
+ * `onStderr` sink so the operator can see what the headless CLI
29
+ * wrote to stderr (banners, MCP load notices) — handy for
30
+ * debugging a failing scenario locally.
31
+ */
32
+ import { spawn } from 'node:child_process';
33
+ import { mkdtempSync, rmSync } from 'node:fs';
34
+ import { tmpdir } from 'node:os';
35
+ import { resolve } from 'node:path';
36
+ /**
37
+ * Run a parsed scenario through the headless CLI. Returns the captured
38
+ * envelope stream + the workspace root the process ran in so the
39
+ * runner can resolve `EXPECT_FILE` paths.
40
+ */
41
+ export async function runHeadlessScenario(scenario, opts = {}) {
42
+ const pugiBin = opts.pugiBin ?? 'pugi';
43
+ const extraArgs = opts.extraArgs ?? [];
44
+ const timeoutMs = opts.timeoutMs ?? 30_000;
45
+ const lineGapMs = opts.lineGapMs ?? 200;
46
+ const onStderr = opts.onStderr ?? noopChunk;
47
+ const spawner = opts.spawner ?? defaultSpawner;
48
+ const workspaceRoot = mkdtempSync(resolve(tmpdir(), 'pugi-smoke-'));
49
+ let cleanedUp = false;
50
+ const args = ['--headless', ...extraArgs];
51
+ const child = spawner(pugiBin, args, {
52
+ cwd: workspaceRoot,
53
+ env: { ...process.env, PUGI_HEADLESS: '1' },
54
+ });
55
+ const envelopes = [];
56
+ let stdoutBuffer = '';
57
+ child.stdout.setEncoding('utf8');
58
+ child.stdout.on('data', (chunk) => {
59
+ stdoutBuffer += chunk;
60
+ let newlineIndex = stdoutBuffer.indexOf('\n');
61
+ while (newlineIndex !== -1) {
62
+ const line = stdoutBuffer.slice(0, newlineIndex);
63
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
64
+ const parsed = parseEnvelopeLine(line);
65
+ if (parsed)
66
+ envelopes.push(parsed);
67
+ newlineIndex = stdoutBuffer.indexOf('\n');
68
+ }
69
+ });
70
+ child.stderr.setEncoding('utf8');
71
+ child.stderr.on('data', (chunk) => onStderr(chunk));
72
+ const userInputs = scenario.steps
73
+ .filter((s) => s.kind === 'user-input')
74
+ .map((s) => s.body);
75
+ // Feed inputs serially with a small gap. We don't await the engine's
76
+ // response — Phase 1 evaluates the WHOLE envelope stream after the
77
+ // process closes, so the gap is purely to give the engine room to
78
+ // emit between turns. A future phase can swap this for a barrier
79
+ // ("wait for `persona-turn` before sending the next `user-turn`")
80
+ // once envelope ordering is sealed.
81
+ const feedPromise = (async () => {
82
+ for (const line of userInputs) {
83
+ child.stdin.write(`${line}\n`);
84
+ await delay(lineGapMs);
85
+ }
86
+ child.stdin.end();
87
+ })();
88
+ const timeoutHandle = setTimeout(() => {
89
+ child.kill('SIGTERM');
90
+ }, timeoutMs);
91
+ // Don't keep node alive purely for the timeout — the spawned child
92
+ // already holds the loop via its IPC pipe.
93
+ timeoutHandle.unref?.();
94
+ await new Promise((resolvePromise) => {
95
+ child.on('close', () => resolvePromise());
96
+ });
97
+ clearTimeout(timeoutHandle);
98
+ // Flush any tail bytes that did not end with a newline.
99
+ if (stdoutBuffer.length > 0) {
100
+ const parsed = parseEnvelopeLine(stdoutBuffer);
101
+ if (parsed)
102
+ envelopes.push(parsed);
103
+ stdoutBuffer = '';
104
+ }
105
+ await feedPromise.catch(() => {
106
+ /* feed promise rejects when the child closes early — ignore */
107
+ });
108
+ // Cleanup the temp workspace lazily — the orchestrator may want to
109
+ // inspect it after the run. Callers that need a fresh dir per scenario
110
+ // should pass a custom workspaceRoot in a future phase; for now the
111
+ // runner resolves EXPECT_FILE paths against this directory and the
112
+ // OS reaps the tmp tree on reboot.
113
+ // (Explicit cleanup helper exposed for tests that want determinism.)
114
+ void cleanedUp;
115
+ return { envelopes, workspaceRoot };
116
+ }
117
+ /**
118
+ * Tear down a workspace created by `runHeadlessScenario`. Exposed
119
+ * separately because the orchestrator wants the directory to survive
120
+ * the run for EXPECT_FILE evaluation.
121
+ */
122
+ export function cleanupWorkspace(workspaceRoot) {
123
+ try {
124
+ rmSync(workspaceRoot, { recursive: true, force: true });
125
+ }
126
+ catch {
127
+ /* best-effort */
128
+ }
129
+ }
130
+ /**
131
+ * Parse one line of headless stdout into an envelope. Returns null
132
+ * when the line is empty or unparseable — non-fatal so the runner can
133
+ * tolerate stray banner output.
134
+ */
135
+ export function parseEnvelopeLine(raw) {
136
+ const line = raw.trim();
137
+ if (line.length === 0)
138
+ return null;
139
+ if (!line.startsWith('{'))
140
+ return null;
141
+ try {
142
+ const parsed = JSON.parse(line);
143
+ if (typeof parsed['kind'] !== 'string')
144
+ return null;
145
+ if (typeof parsed['body'] !== 'string')
146
+ return null;
147
+ const ts = typeof parsed['ts'] === 'number'
148
+ ? parsed['ts']
149
+ : Number(parsed['ts'] ?? Date.now());
150
+ return {
151
+ kind: parsed['kind'],
152
+ body: parsed['body'],
153
+ ts: Number.isFinite(ts) ? ts : Date.now(),
154
+ };
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ function defaultSpawner(bin, args, options) {
161
+ return spawn(bin, args, {
162
+ cwd: options.cwd,
163
+ env: options.env,
164
+ stdio: ['pipe', 'pipe', 'pipe'],
165
+ });
166
+ }
167
+ function delay(ms) {
168
+ return new Promise((resolve) => {
169
+ const handle = setTimeout(resolve, ms);
170
+ handle.unref?.();
171
+ });
172
+ }
173
+ function noopChunk(_chunk) { }
174
+ //# sourceMappingURL=headless-driver.js.map
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Smoke orchestrator — glues the scenario parser, the headless
3
+ * subprocess driver, and the runner into a single "load → run → report"
4
+ * pipeline. The CLI surface (`pugi smoke`) and the standalone script
5
+ * (`scripts/run-scenarios.ts`) both call into this module so the two
6
+ * entry points share one code path.
7
+ *
8
+ * Phase 1 boundary — `runSmoke` is responsible for:
9
+ *
10
+ * 1. Discovering scenario files under `scenariosDir` (glob match on
11
+ * `*.scenario.txt`).
12
+ * 2. Parsing each file via `parseScenario`. Parse errors are surfaced
13
+ * via the report but do not stop the run — every scenario gets a
14
+ * chance to fail with a clean diagnostic.
15
+ * 3. Driving each scenario through the headless executor (the
16
+ * executor is injected so tests can swap it for a deterministic
17
+ * stub; production wires `runHeadlessScenario` from
18
+ * `headless-driver.ts`).
19
+ * 4. Filtering by `--filter <pattern>` (compiles to fnmatch-lite).
20
+ * 5. Computing pass/fail/summary numbers.
21
+ *
22
+ * The orchestrator is intentionally synchronous (apart from the
23
+ * per-scenario `await`) — running scenarios in parallel is a Phase 2
24
+ * concern. The corpus is small and sequential output is easier to read.
25
+ */
26
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
27
+ import { resolve } from 'node:path';
28
+ import { parseScenario, } from './scenario-parser.js';
29
+ import { runScenario, } from './runner.js';
30
+ /**
31
+ * Top-level smoke entry. Returns the report so the CLI can pretty-print
32
+ * it AND set `process.exitCode` deterministically.
33
+ */
34
+ export async function runSmoke(opts) {
35
+ const log = opts.log ?? noopLog;
36
+ const now = opts.now ?? Date.now;
37
+ const allScenarios = loadScenariosFromDir(opts.scenariosDir);
38
+ const visible = opts.filter && opts.filter.length > 0
39
+ ? filterByPattern(allScenarios, opts.filter)
40
+ : allScenarios;
41
+ const results = [];
42
+ let passed = 0;
43
+ let failed = 0;
44
+ for (const item of visible) {
45
+ log(`pugi smoke: running ${item.scenario.id}`);
46
+ if (item.parseErrors.length > 0) {
47
+ results.push({
48
+ id: item.scenario.id,
49
+ filePath: item.scenario.filePath,
50
+ status: 'parse-error',
51
+ durationMs: 0,
52
+ assertionCount: 0,
53
+ failures: [],
54
+ parseErrors: item.parseErrors,
55
+ });
56
+ failed += 1;
57
+ continue;
58
+ }
59
+ let envelopes = [];
60
+ let workspaceRoot = '.';
61
+ try {
62
+ const out = await opts.executor(item.scenario);
63
+ envelopes = out.envelopes;
64
+ workspaceRoot = out.workspaceRoot;
65
+ }
66
+ catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ results.push({
69
+ id: item.scenario.id,
70
+ filePath: item.scenario.filePath,
71
+ status: 'executor-error',
72
+ durationMs: 0,
73
+ assertionCount: 0,
74
+ failures: [],
75
+ executorError: message,
76
+ });
77
+ failed += 1;
78
+ continue;
79
+ }
80
+ const result = runScenario({
81
+ scenario: item.scenario,
82
+ envelopes,
83
+ workspaceRoot,
84
+ now,
85
+ });
86
+ results.push({
87
+ id: result.id,
88
+ filePath: item.scenario.filePath,
89
+ status: result.passed ? 'passed' : 'failed',
90
+ durationMs: result.durationMs,
91
+ assertionCount: result.assertionCount,
92
+ failures: result.failures,
93
+ });
94
+ if (result.passed)
95
+ passed += 1;
96
+ else
97
+ failed += 1;
98
+ }
99
+ const total = visible.length;
100
+ const skipped = allScenarios.length - visible.length;
101
+ const exitCode = failed === 0 ? 0 : 1;
102
+ return { total, passed, failed, skipped, results, exitCode };
103
+ }
104
+ /**
105
+ * Walk `dir` for `*.scenario.txt` files (non-recursive). Returns each
106
+ * file's parsed scenario + collected parse errors so the orchestrator
107
+ * can surface malformed files as failed runs rather than skipping them.
108
+ */
109
+ export function loadScenariosFromDir(dir) {
110
+ let names = [];
111
+ try {
112
+ names = readdirSync(dir);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ const out = [];
118
+ for (const name of names) {
119
+ if (!name.endsWith('.scenario.txt'))
120
+ continue;
121
+ const filePath = resolve(dir, name);
122
+ let stat;
123
+ try {
124
+ stat = statSync(filePath);
125
+ }
126
+ catch {
127
+ continue;
128
+ }
129
+ if (!stat.isFile())
130
+ continue;
131
+ const body = readFileSync(filePath, 'utf8');
132
+ const parsed = parseScenario(filePath, body);
133
+ if (parsed.scenario) {
134
+ out.push({ scenario: parsed.scenario, parseErrors: parsed.errors });
135
+ }
136
+ }
137
+ // Stable sort by id so report output is deterministic across
138
+ // filesystems with different readdir order.
139
+ out.sort((a, b) => a.scenario.id.localeCompare(b.scenario.id));
140
+ return out;
141
+ }
142
+ /**
143
+ * Render a SmokeReport into a multi-line human-readable string. Kept
144
+ * separate from `runSmoke` so the CLI can pick its own format (text vs
145
+ * JSON). The default text format mirrors `node:test`'s tap-lite output:
146
+ *
147
+ * ok 1 - identity (12ms)
148
+ * not ok 2 - codegen-create-file (8ms)
149
+ * line 5: EXPECT failed — no envelope matched ...
150
+ *
151
+ * pugi smoke: 1 passed, 1 failed
152
+ */
153
+ export function renderReportText(report) {
154
+ const lines = [];
155
+ for (let i = 0; i < report.results.length; i += 1) {
156
+ const r = report.results[i];
157
+ if (!r)
158
+ continue;
159
+ const ordinal = i + 1;
160
+ if (r.status === 'passed') {
161
+ lines.push(`ok ${ordinal} - ${r.id} (${r.durationMs}ms)`);
162
+ }
163
+ else if (r.status === 'failed') {
164
+ lines.push(`not ok ${ordinal} - ${r.id} (${r.durationMs}ms)`);
165
+ for (const f of r.failures) {
166
+ lines.push(` line ${f.line}: ${f.message}`);
167
+ }
168
+ }
169
+ else if (r.status === 'parse-error') {
170
+ lines.push(`not ok ${ordinal} - ${r.id} (parse error)`);
171
+ for (const e of r.parseErrors ?? []) {
172
+ lines.push(` line ${e.line}: ${e.message}`);
173
+ }
174
+ }
175
+ else if (r.status === 'executor-error') {
176
+ lines.push(`not ok ${ordinal} - ${r.id} (executor error)`);
177
+ lines.push(` ${r.executorError ?? 'unknown executor failure'}`);
178
+ }
179
+ }
180
+ lines.push('');
181
+ const skippedSuffix = report.skipped > 0 ? `, ${report.skipped} skipped` : '';
182
+ lines.push(`pugi smoke: ${report.passed} passed, ${report.failed} failed${skippedSuffix}`);
183
+ return lines.join('\n');
184
+ }
185
+ function filterByPattern(scenarios, pattern) {
186
+ if (!pattern.includes('*')) {
187
+ return scenarios.filter((s) => s.scenario.id.includes(pattern));
188
+ }
189
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
190
+ const re = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
191
+ return scenarios.filter((s) => re.test(s.scenario.id));
192
+ }
193
+ function noopLog(_line) { }
194
+ //# sourceMappingURL=orchestrator.js.map