@michaelxwb/code-flow 0.6.5 → 1.0.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 (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +21 -9
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. package/src/migrate/spec-workflow.js +0 -238
package/lib/toml.js ADDED
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ function mask(text) {
4
+ let out = '', state = 'normal';
5
+ for (let index = 0; index < text.length; index++) {
6
+ const char = text[index], next = text.slice(index, index + 3);
7
+ if (char === '\n') {
8
+ out += '\n';
9
+ if (state === 'comment' || state === 'basic' || state === 'literal') state = 'normal';
10
+ continue;
11
+ }
12
+ if (state === 'comment') { out += ' '; continue; }
13
+ if (state === 'basic' || state === 'literal') {
14
+ out += char;
15
+ if (char === '\\' && state === 'basic' && text[index + 1] && text[index + 1] !== '\n') {
16
+ out += text[++index];
17
+ } else if (char === (state === 'basic' ? '"' : "'")) state = 'normal';
18
+ continue;
19
+ }
20
+ if (state === 'multibasic') {
21
+ if (next === '"""') { out += ' '; index += 2; state = 'normal'; continue; }
22
+ out += ' ';
23
+ if (char === '\\' && text[index + 1] && text[index + 1] !== '\n') { out += ' '; index++; }
24
+ continue;
25
+ }
26
+ if (state === 'multiliteral') {
27
+ if (next === "'''") { out += ' '; index += 2; state = 'normal'; continue; }
28
+ out += ' ';
29
+ continue;
30
+ }
31
+ if (char === '#') { out += ' '; state = 'comment'; continue; }
32
+ if (next === '"""') { out += ' '; index += 2; state = 'multibasic'; continue; }
33
+ if (next === "'''") { out += ' '; index += 2; state = 'multiliteral'; continue; }
34
+ if (char === '"') { out += char; state = 'basic'; continue; }
35
+ if (char === "'") { out += char; state = 'literal'; continue; }
36
+ out += char;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function bracketDelta(line) {
42
+ let depth = 0, quote = null;
43
+ for (let index = 0; index < line.length; index++) {
44
+ const char = line[index];
45
+ if (quote) {
46
+ if (char === '\\' && quote === '"') index++;
47
+ else if (char === quote) quote = null;
48
+ continue;
49
+ }
50
+ if (char === '"' || char === "'") { quote = char; continue; }
51
+ if (char === '[') depth++;
52
+ else if (char === ']') depth--;
53
+ }
54
+ return depth;
55
+ }
56
+
57
+ function tableHeader(line) {
58
+ const text = line.trim();
59
+ if (!text.startsWith('[') || !text.endsWith(']')) return null;
60
+ const array = text.startsWith('[[') && text.endsWith(']]');
61
+ const inner = text.slice(array ? 2 : 1, array ? -2 : -1).trim();
62
+ if (inner === 'features') return { kind: array ? 'array' : 'features', array };
63
+ if (/^["']/.test(inner) && (/features/i.test(inner) || /\\u/i.test(inner))) {
64
+ return { kind: 'unsupported', array, reason: 'quoted table' };
65
+ }
66
+ if (/^features\./.test(inner) || /\.features\./.test(inner)) {
67
+ return { kind: 'unsupported', array, reason: 'nested features table' };
68
+ }
69
+ return { kind: 'other', array };
70
+ }
71
+
72
+ function assignment(line) {
73
+ const text = line.trim();
74
+ if (!text || text.startsWith('[')) return null;
75
+ if (/^["'].*=/.test(text)) return { quoted: true, text };
76
+ const match = /^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/.exec(text);
77
+ return match ? { key: match[1], value: match[2].trim() } : null;
78
+ }
79
+
80
+ function multiline(value, line, rawLine) {
81
+ return bracketDelta(line) !== 0 || /"""|'''/.test(rawLine.split('#')[0]);
82
+ }
83
+
84
+ function featuresHooks(text) {
85
+ const rawLines = text.split('\n'), lines = mask(text).split('\n');
86
+ let table = null, depth = 0, header = -1, end = -1, key = -1, value = null, array = false, rootFeatures = -1, unsupported = null;
87
+ const fail = reason => { if (!unsupported) unsupported = reason; };
88
+ for (let index = 0; index < lines.length; index++) {
89
+ const line = lines[index];
90
+ if (depth === 0) {
91
+ const head = tableHeader(line);
92
+ if (head) {
93
+ if (header >= 0 && end < 0) end = index;
94
+ if (head.kind === 'features') {
95
+ if (header >= 0) fail('duplicate features table');
96
+ header = index; table = 'features';
97
+ } else if (head.kind === 'array') {
98
+ if (header >= 0) fail('duplicate features table');
99
+ array = true; header = index; table = 'features'; fail('array table');
100
+ } else if (head.kind === 'unsupported') {
101
+ fail(head.reason); table = 'other';
102
+ } else {
103
+ table = 'other';
104
+ }
105
+ depth = Math.max(0, depth + bracketDelta(line));
106
+ continue;
107
+ }
108
+ const item = assignment(line);
109
+ if (item && item.quoted) {
110
+ if (table === 'features' || /features|hooks/i.test(item.text) || /\\u/i.test(item.text)) {
111
+ fail('quoted key');
112
+ }
113
+ } else if (item && table === 'features') {
114
+ if (item.key === 'hooks') {
115
+ if (multiline(item.value, line, rawLines[index])) fail('Multi-line features value');
116
+ else if (item.value === 'true' || item.value === 'false') { key = index; value = item.value; }
117
+ else fail('features.hooks value');
118
+ } else if (item.key.startsWith('features.')) {
119
+ fail('dotted key inside features');
120
+ }
121
+ } else if (item && table === null) {
122
+ if (item.key === 'features.hooks') {
123
+ if (multiline(item.value, line, rawLines[index])) fail('Multi-line features value');
124
+ else if (item.value === 'true' || item.value === 'false') { key = index; value = item.value; }
125
+ else fail('features.hooks value');
126
+ } else if (item.key === 'features') {
127
+ fail('inline features value');
128
+ } else if (item.key.startsWith('features.')) {
129
+ rootFeatures = index;
130
+ if (multiline(item.value, line, rawLines[index])) fail('Multi-line features value');
131
+ }
132
+ }
133
+ }
134
+ depth = Math.max(0, depth + bracketDelta(line));
135
+ }
136
+ if (header >= 0 && end < 0) end = lines.length;
137
+ return { header, end, key, value, array, rootFeatures, unsupported };
138
+ }
139
+
140
+ module.exports = { featuresHooks };
package/lib/verify.js ADDED
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const { spawn, spawnSync } = require('node:child_process');
4
+ const { performance } = require('node:perf_hooks');
5
+ const f = require('./fs');
6
+
7
+ function canonicalEnv(...layers) {
8
+ return Object.fromEntries(Object.entries(Object.assign({}, ...layers))
9
+ .sort(([left], [right]) => left.localeCompare(right)));
10
+ }
11
+
12
+ function prepare(root, tests, settings, runnerEnv = {}) {
13
+ require('./task').validateTests(tests); f.object(runnerEnv, 'runner env');
14
+ for (const key of ['CF_WORKTREE_ID', 'CF_TASK_ID', 'CF_TASK_IDS', 'CF_RUN_ID', 'CF_PORT_OFFSET', 'CF_DB_SUFFIX']) {
15
+ f.demand(!Object.hasOwn(runnerEnv, key) || process.env[key] === undefined ||
16
+ process.env[key] === runnerEnv[key], 'env_conflict',
17
+ 'Existing ' + key + ' does not match the selected run', 1);
18
+ }
19
+ const unique = new Map(), entries = [];
20
+ const environment = f.hash(JSON.stringify(Object.keys(process.env).sort().map(key => [key, process.env[key]])));
21
+ for (const input of tests) {
22
+ const cwd = f.inside(root, input.cwd ?? '.');
23
+ f.demand(require('node:fs').statSync(cwd).isDirectory(), 'test_cwd', 'Test cwd is not a directory');
24
+ const env = canonicalEnv(runnerEnv, input.env ?? {});
25
+ const item = { ...input, root, cwd, env, timeout: input.timeout ?? settings.timeoutSeconds,
26
+ parallel: input.parallel ?? false, group: input.group ?? null };
27
+ const key = JSON.stringify([item.argv ?? null, item.run ?? null, cwd, item.timeout, item.parallel, item.group, environment, env]);
28
+ if (!unique.has(key)) unique.set(key, { ...item, key, queued: performance.now() });
29
+ entries.push({ id: input.id, key });
30
+ }
31
+ return { commands: [...unique.values()], entries };
32
+ }
33
+ function tail(buffer) {
34
+ let text = buffer.toString('utf8').split(/\r?\n/).slice(-50).join('\n');
35
+ while (Buffer.byteLength(text) > 65536) text = text.slice(1);
36
+ return text;
37
+ }
38
+ function capture() {
39
+ let buffer = Buffer.alloc(0);
40
+ return {
41
+ push(chunk) { buffer = Buffer.concat([buffer, chunk]).subarray(-65536); },
42
+ text() { return tail(buffer); }
43
+ };
44
+ }
45
+ function signalGroup(pid, signal) {
46
+ try { process.kill(-pid, signal); } catch (error) { if (error.code !== 'ESRCH') throw error; }
47
+ }
48
+ async function stopTree(child) {
49
+ if (!child.pid) return;
50
+ if (process.platform === 'win32') {
51
+ const result = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { timeout: 2000, windowsHide: true });
52
+ if (result.error) throw result.error;
53
+ if (child.exitCode === null) child.kill('SIGKILL');
54
+ return;
55
+ }
56
+ signalGroup(child.pid, 'SIGTERM');
57
+ await new Promise(resolve => setTimeout(resolve, 100));
58
+ signalGroup(child.pid, 'SIGKILL');
59
+ }
60
+ function spawnCommand(command) {
61
+ const options = { cwd: command.cwd, env: { ...process.env, ...(command.env ?? {}) },
62
+ detached: process.platform !== 'win32', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] };
63
+ return command.argv ? spawn(command.argv[0], command.argv.slice(1), options) :
64
+ spawn(command.run, { ...options, shell: true });
65
+ }
66
+ function spawnExecute(command, signal) {
67
+ return new Promise(resolve => {
68
+ const began = performance.now(), stdout = capture(), stderr = capture();
69
+ let child, status = null, stop = null, errorText = null, settled = false;
70
+ const end = async (code, childSignal) => {
71
+ if (settled) return; settled = true;
72
+ clearTimeout(timer); signal.removeEventListener('abort', cancel);
73
+ if (stop) { try { await stop; } catch (error) { status = 'cleanup-error'; errorText = error.message; } }
74
+ resolve({ id: command.id, status: status ?? (code === 0 ? 'pass' : childSignal ? 'signal' : 'fail'),
75
+ exit_code: code, signal: childSignal, stdout: stdout.text(), stderr: stderr.text(),
76
+ error: errorText, duration_ms: performance.now() - began, wait_ms: began - command.queued });
77
+ };
78
+ const halt = reason => {
79
+ if (status) return;
80
+ status = reason; stop = stopTree(child);
81
+ const reap = () => {
82
+ if (settled) return;
83
+ child.stdout.destroy(); child.stderr.destroy();
84
+ void end(child.exitCode, child.signalCode);
85
+ };
86
+ stop.then(reap, error => { errorText = error.message; reap(); });
87
+ };
88
+ const cancel = () => halt('cancelled');
89
+ const timer = setTimeout(() => halt('timeout'), command.timeout * 1000);
90
+ try {
91
+ child = spawnCommand(command);
92
+ child.stdout.on('data', chunk => stdout.push(chunk));
93
+ child.stderr.on('data', chunk => stderr.push(chunk));
94
+ child.once('error', error => { status = 'spawn-error'; errorText = error.message; });
95
+ child.once('close', (code, childSignal) => { void end(code, childSignal); });
96
+ signal.addEventListener('abort', cancel, { once: true });
97
+ if (signal.aborted) cancel();
98
+ } catch (error) { status = 'spawn-error'; errorText = error.message; void end(null, null); }
99
+ });
100
+ }
101
+
102
+ async function execute(command, signal) {
103
+ let lock = null;
104
+ if (command.group) {
105
+ const began = performance.now();
106
+ try {
107
+ lock = await require('./locks').acquireGroup(command.root, command.group, command.timeout, signal);
108
+ } catch (error) {
109
+ const status = error.code === 'cancelled' ? 'cancelled' :
110
+ error.code === 'lock_timeout' ? 'lock-timeout' : 'spawn-error';
111
+ return { id: command.id, status, exit_code: null, signal: null, stdout: '', stderr: '',
112
+ error: error.message, duration_ms: performance.now() - began, wait_ms: began - command.queued };
113
+ }
114
+ }
115
+ try { return await spawnExecute(command, signal); } finally {
116
+ if (lock) require('./locks').release(lock);
117
+ }
118
+ }
119
+ async function batch(commands, jobs, controller, results) {
120
+ const pending = [...commands], running = new Set(), groups = new Set();
121
+ while (pending.length || running.size) {
122
+ while (!controller.signal.aborted && running.size < jobs) {
123
+ const index = pending.findIndex(item => !item.group || !groups.has(item.group));
124
+ if (index < 0) break;
125
+ const item = pending.splice(index, 1)[0];
126
+ if (item.group) groups.add(item.group);
127
+ const job = execute(item, controller.signal).then(result => {
128
+ results.set(item.key, result);
129
+ if (result.status !== 'pass') controller.abort();
130
+ }).finally(() => { groups.delete(item.group); running.delete(job); });
131
+ running.add(job);
132
+ }
133
+ if (!running.size) break;
134
+ await Promise.race(running);
135
+ }
136
+ }
137
+ function report(plan, results, started, interrupted) {
138
+ const seen = new Set();
139
+ const rows = plan.entries.map(item => {
140
+ const duplicate = seen.has(item.key); seen.add(item.key);
141
+ return { ...(results.get(item.key) ?? { status: 'not-run' }), id: item.id, duplicate };
142
+ });
143
+ return { ok: rows.every(item => item.status === 'pass') && !interrupted, results: rows,
144
+ executed: results.size, deduped: plan.entries.length - plan.commands.length,
145
+ elapsed_ms: performance.now() - started, interrupted };
146
+ }
147
+ async function run(root, tests, settings, options = {}) {
148
+ const started = performance.now(), plan = prepare(root, tests, settings, options.env ?? {}), results = new Map();
149
+ const jobs = options.jobs ?? settings.jobs; f.positive(jobs, 'jobs', true);
150
+ const controller = new AbortController();
151
+ let interrupted = null;
152
+ const interrupt = signal => { interrupted = signal; controller.abort(); };
153
+ const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
154
+ process.once('SIGINT', onInt); process.once('SIGTERM', onTerm);
155
+ try {
156
+ for (let index = 0; index < plan.commands.length && !controller.signal.aborted;) {
157
+ const group = [plan.commands[index++]];
158
+ if (group[0].parallel) {
159
+ while (index < plan.commands.length && plan.commands[index].parallel) group.push(plan.commands[index++]);
160
+ }
161
+ await batch(group, group[0].parallel ? jobs : 1, controller, results);
162
+ }
163
+ return report(plan, results, started, interrupted);
164
+ } finally {
165
+ process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm);
166
+ }
167
+ }
168
+ module.exports = { prepare, run, execute, capture, stopTree };