moflo 4.8.86 → 4.8.87-rc.1
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.
- package/package.json +2 -2
- package/src/modules/cli/dist/src/epic/spells/auto-merge.yaml +26 -0
- package/src/modules/cli/dist/src/epic/spells/single-branch.yaml +26 -0
- package/src/modules/cli/dist/src/services/headless-worker-executor.js +39 -13
- package/src/modules/cli/dist/src/version.js +1 -1
- package/src/modules/cli/package.json +1 -1
- package/src/modules/spells/dist/commands/prompt-command.js +1 -25
- package/src/modules/spells/dist/core/interpolation.js +10 -2
- package/src/modules/spells/dist/core/prerequisite-checker.js +181 -12
- package/src/modules/spells/dist/core/runner.js +8 -5
- package/src/modules/spells/dist/core/stdin-reader.js +33 -0
- package/src/modules/spells/dist/index.js +1 -1
- package/src/modules/spells/dist/schema/validator.js +13 -393
- package/src/modules/spells/dist/schema/validators/jumps.js +67 -0
- package/src/modules/spells/dist/schema/validators/prerequisites.js +71 -0
- package/src/modules/spells/dist/schema/validators/references.js +124 -0
- package/src/modules/spells/dist/schema/validators/steps.js +145 -0
- package/src/modules/spells/dist/schema/validators/top-level.js +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.87-rc.1",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"@types/node": "^24.12.2",
|
|
118
118
|
"@xenova/transformers": "^2.17.0",
|
|
119
119
|
"eslint": "^8.0.0",
|
|
120
|
-
"moflo": "^4.8.
|
|
120
|
+
"moflo": "^4.8.86",
|
|
121
121
|
"tsx": "^4.21.0",
|
|
122
122
|
"typescript": "^5.9.3",
|
|
123
123
|
"vitest": "^4.0.0"
|
|
@@ -32,6 +32,32 @@ arguments:
|
|
|
32
32
|
|
|
33
33
|
mofloLevel: hooks
|
|
34
34
|
|
|
35
|
+
# Command-presence checks — fail fast with an actionable message if any
|
|
36
|
+
# of these aren't on PATH. Runtime-state checks (auth, dirty tree, remote)
|
|
37
|
+
# still live on the individual steps that need them via `preflight:`.
|
|
38
|
+
prerequisites:
|
|
39
|
+
- name: gh-cli
|
|
40
|
+
description: GitHub CLI is required to read issues and manage PRs
|
|
41
|
+
docsUrl: https://cli.github.com
|
|
42
|
+
detect:
|
|
43
|
+
type: command
|
|
44
|
+
command: gh
|
|
45
|
+
promptOnMissing: false
|
|
46
|
+
- name: git
|
|
47
|
+
description: Git is required for branch management
|
|
48
|
+
docsUrl: https://git-scm.com/downloads
|
|
49
|
+
detect:
|
|
50
|
+
type: command
|
|
51
|
+
command: git
|
|
52
|
+
promptOnMissing: false
|
|
53
|
+
- name: claude-cli
|
|
54
|
+
description: Claude Code CLI is required to run the story implementer subagent
|
|
55
|
+
docsUrl: https://docs.anthropic.com/en/docs/claude-code
|
|
56
|
+
detect:
|
|
57
|
+
type: command
|
|
58
|
+
command: claude
|
|
59
|
+
promptOnMissing: false
|
|
60
|
+
|
|
35
61
|
steps:
|
|
36
62
|
# Step 1: Initialize epic state in memory (enables resume)
|
|
37
63
|
- id: init-state
|
|
@@ -37,6 +37,32 @@ arguments:
|
|
|
37
37
|
|
|
38
38
|
mofloLevel: hooks
|
|
39
39
|
|
|
40
|
+
# Command-presence checks — fail fast with an actionable message if any
|
|
41
|
+
# of these aren't on PATH. Runtime-state checks (auth, dirty tree, remote)
|
|
42
|
+
# still live on the individual steps that need them via `preflight:`.
|
|
43
|
+
prerequisites:
|
|
44
|
+
- name: gh-cli
|
|
45
|
+
description: GitHub CLI is required to read issues and manage PRs
|
|
46
|
+
docsUrl: https://cli.github.com
|
|
47
|
+
detect:
|
|
48
|
+
type: command
|
|
49
|
+
command: gh
|
|
50
|
+
promptOnMissing: false
|
|
51
|
+
- name: git
|
|
52
|
+
description: Git is required for branch management
|
|
53
|
+
docsUrl: https://git-scm.com/downloads
|
|
54
|
+
detect:
|
|
55
|
+
type: command
|
|
56
|
+
command: git
|
|
57
|
+
promptOnMissing: false
|
|
58
|
+
- name: claude-cli
|
|
59
|
+
description: Claude Code CLI is required to run the story implementer subagent
|
|
60
|
+
docsUrl: https://docs.anthropic.com/en/docs/claude-code
|
|
61
|
+
detect:
|
|
62
|
+
type: command
|
|
63
|
+
command: claude
|
|
64
|
+
promptOnMissing: false
|
|
65
|
+
|
|
40
66
|
steps:
|
|
41
67
|
# Step 1: Initialize epic state in memory (enables resume)
|
|
42
68
|
- id: init-state
|
|
@@ -340,6 +340,23 @@ export function getWorkerConfig(type) {
|
|
|
340
340
|
// ============================================
|
|
341
341
|
// HeadlessWorkerExecutor Class
|
|
342
342
|
// ============================================
|
|
343
|
+
// Module-level registry + single process 'exit' listener.
|
|
344
|
+
// Attaching the listener per instance leaked handlers (MaxListenersExceededWarning
|
|
345
|
+
// after ~11 daemons), so we install one global listener that walks a live registry.
|
|
346
|
+
// Instances remove themselves on dispose() for long-running hosts / test suites.
|
|
347
|
+
const liveExecutors = new Set();
|
|
348
|
+
let exitListenerInstalled = false;
|
|
349
|
+
function ensureExitListener() {
|
|
350
|
+
if (exitListenerInstalled)
|
|
351
|
+
return;
|
|
352
|
+
exitListenerInstalled = true;
|
|
353
|
+
// 'exit' (not 'beforeExit') fires on explicit process.exit(); handler must be sync.
|
|
354
|
+
process.on('exit', () => {
|
|
355
|
+
for (const executor of liveExecutors) {
|
|
356
|
+
executor._killPoolOnExit();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
343
360
|
/**
|
|
344
361
|
* HeadlessWorkerExecutor - Executes workers using Claude Code in headless mode
|
|
345
362
|
*
|
|
@@ -374,20 +391,29 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
374
391
|
};
|
|
375
392
|
// Ensure log directory exists
|
|
376
393
|
this.ensureLogDir();
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
394
|
+
// Register for process-exit cleanup via the shared listener.
|
|
395
|
+
ensureExitListener();
|
|
396
|
+
liveExecutors.add(this);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Remove this executor from the exit-cleanup registry. Call when the host
|
|
400
|
+
* (daemon / test) is done with the instance — otherwise the registry
|
|
401
|
+
* retains a strong ref via the pool/cache Maps.
|
|
402
|
+
*/
|
|
403
|
+
dispose() {
|
|
404
|
+
liveExecutors.delete(this);
|
|
405
|
+
}
|
|
406
|
+
/** @internal — invoked by the shared process-exit listener. Must stay sync. */
|
|
407
|
+
_killPoolOnExit() {
|
|
408
|
+
for (const [, entry] of this.processPool) {
|
|
409
|
+
try {
|
|
410
|
+
clearTimeout(entry.timeout);
|
|
411
|
+
entry.process.kill('SIGTERM');
|
|
389
412
|
}
|
|
390
|
-
|
|
413
|
+
catch {
|
|
414
|
+
// Process already gone — ignore
|
|
415
|
+
}
|
|
416
|
+
}
|
|
391
417
|
}
|
|
392
418
|
// ============================================
|
|
393
419
|
// Public API
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* 2. ISO timestamp `fallbackDaysAgo` days in the past, if provided
|
|
11
11
|
* 3. empty string
|
|
12
12
|
*/
|
|
13
|
-
import * as readline from 'node:readline';
|
|
14
13
|
import { interpolateString } from '../core/interpolation.js';
|
|
15
14
|
import { acquireTTYLock } from '../core/tty-lock.js';
|
|
15
|
+
import { readLineFromStdin } from '../core/stdin-reader.js';
|
|
16
16
|
/**
|
|
17
17
|
* Resolve the effective default, walking the fallback chain.
|
|
18
18
|
* Exported for testing.
|
|
@@ -31,30 +31,6 @@ export function resolveDefault(raw, fallbackDaysAgo, now = Date.now()) {
|
|
|
31
31
|
}
|
|
32
32
|
return '';
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Read one line from stdin. Resolves with the trimmed line (empty string
|
|
36
|
-
* if the user just hit enter). Honors the provided abort signal.
|
|
37
|
-
*/
|
|
38
|
-
async function readLineFromStdin(prompt, abortSignal) {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
41
|
-
const onAbort = () => { rl.close(); reject(new Error('Prompt aborted')); };
|
|
42
|
-
if (abortSignal) {
|
|
43
|
-
if (abortSignal.aborted) {
|
|
44
|
-
rl.close();
|
|
45
|
-
reject(new Error('Prompt aborted'));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
49
|
-
}
|
|
50
|
-
rl.question(prompt, (answer) => {
|
|
51
|
-
if (abortSignal)
|
|
52
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
53
|
-
rl.close();
|
|
54
|
-
resolve(answer.trim());
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
34
|
export const promptCommand = {
|
|
59
35
|
type: 'prompt',
|
|
60
36
|
description: 'Ask the user a question and capture the response',
|
|
@@ -27,8 +27,10 @@ export const VAR_REF_PATTERN = /(?<!\$)\{([A-Za-z_][A-Za-z0-9_.-]*)\}/g;
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolution precedence:
|
|
29
29
|
* 1. {args.key} — explicit args prefix (depth-2 only)
|
|
30
|
-
* 2. {
|
|
31
|
-
*
|
|
30
|
+
* 2. {env.KEY} — environment variable (depth-2 only); typically populated
|
|
31
|
+
* by a spell-level `prerequisites:` entry with `promptOnMissing: true`
|
|
32
|
+
* 3. {stepId.outputKey} — walk context.variables
|
|
33
|
+
* 4. {key} — single-segment fallback to context.args
|
|
32
34
|
*/
|
|
33
35
|
function resolveVariable(path, context) {
|
|
34
36
|
const segments = path.split('.');
|
|
@@ -38,6 +40,12 @@ function resolveVariable(path, context) {
|
|
|
38
40
|
if (val !== undefined)
|
|
39
41
|
return val;
|
|
40
42
|
}
|
|
43
|
+
// Environment variable prefix: {env.KEY}
|
|
44
|
+
if (segments[0] === 'env' && segments.length === 2) {
|
|
45
|
+
const val = process.env[segments[1]];
|
|
46
|
+
if (val !== undefined && val.length > 0)
|
|
47
|
+
return val;
|
|
48
|
+
}
|
|
41
49
|
// Walk context.variables (step outputs): {stepId.outputKey}
|
|
42
50
|
let value = context.variables;
|
|
43
51
|
for (const segment of segments) {
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prerequisite Checker
|
|
3
3
|
*
|
|
4
|
-
* Collects
|
|
5
|
-
*
|
|
4
|
+
* Collects prerequisites from three sources (declarative YAML at spell and
|
|
5
|
+
* step level + imperative step-command-owned), dedupes by name, runs each
|
|
6
|
+
* detector once, and — when stdin is a TTY — prompts for unmet env-type
|
|
7
|
+
* prereqs and writes the answer into process.env so downstream steps
|
|
8
|
+
* (including nested loop bodies) inherit it.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
10
|
+
* Non-TTY failure path reports every unmet prereq with its docs URL so the
|
|
11
|
+
* caller sees the full picture rather than failing mid-cast.
|
|
12
|
+
*
|
|
13
|
+
* Story #193: initial step-command-owned prereqs.
|
|
14
|
+
* Issue #460: YAML-declared + interactive preflight walker.
|
|
8
15
|
*/
|
|
9
16
|
import { execFile } from 'node:child_process';
|
|
17
|
+
import { access } from 'node:fs/promises';
|
|
10
18
|
import { promisify } from 'node:util';
|
|
19
|
+
import { acquireTTYLock } from './tty-lock.js';
|
|
20
|
+
import { readLineFromStdin } from './stdin-reader.js';
|
|
11
21
|
const execFileAsync = promisify(execFile);
|
|
12
22
|
/** Check whether a CLI command is available on the system PATH. */
|
|
13
23
|
export async function commandExists(cmd) {
|
|
@@ -21,23 +31,92 @@ export async function commandExists(cmd) {
|
|
|
21
31
|
}
|
|
22
32
|
}
|
|
23
33
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
34
|
+
* Compile a declarative YAML `PrerequisiteSpec` into the imperative
|
|
35
|
+
* `Prerequisite` shape — synthesizes a `check()` that dispatches on the
|
|
36
|
+
* detector type and populates the prompt-resolution metadata.
|
|
37
|
+
*/
|
|
38
|
+
export function compilePrerequisiteSpec(spec) {
|
|
39
|
+
const detect = spec.detect;
|
|
40
|
+
const check = async () => {
|
|
41
|
+
switch (detect.type) {
|
|
42
|
+
case 'env': {
|
|
43
|
+
const val = process.env[detect.key];
|
|
44
|
+
return typeof val === 'string' && val.length > 0;
|
|
45
|
+
}
|
|
46
|
+
case 'command':
|
|
47
|
+
return commandExists(detect.command);
|
|
48
|
+
case 'file':
|
|
49
|
+
try {
|
|
50
|
+
await access(detect.path);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const installHint = spec.description ?? defaultHintForDetect(spec);
|
|
59
|
+
const envKey = detect.type === 'env' ? detect.key : undefined;
|
|
60
|
+
const promptOnMissing = spec.promptOnMissing ?? true;
|
|
61
|
+
return {
|
|
62
|
+
name: spec.name,
|
|
63
|
+
check,
|
|
64
|
+
installHint,
|
|
65
|
+
url: spec.docsUrl,
|
|
66
|
+
description: spec.description,
|
|
67
|
+
promptOnMissing,
|
|
68
|
+
envKey,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function defaultHintForDetect(spec) {
|
|
72
|
+
switch (spec.detect.type) {
|
|
73
|
+
case 'env': return `Set the ${spec.detect.key} environment variable`;
|
|
74
|
+
case 'command': return `Install "${spec.detect.command}" and add it to your PATH`;
|
|
75
|
+
case 'file': return `Ensure the file exists: ${spec.detect.path}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Collect unique prerequisites from a spell. Sources, in order:
|
|
80
|
+
* 1. spell-level YAML (`definition.prerequisites`)
|
|
81
|
+
* 2. step-level YAML (including nested loop/condition/parallel bodies)
|
|
82
|
+
* 3. step-command built-ins (imperative, from the registry)
|
|
83
|
+
*
|
|
84
|
+
* Deduplicates by name — first occurrence wins.
|
|
26
85
|
*/
|
|
27
86
|
export function collectPrerequisites(definition, registry) {
|
|
28
87
|
const seen = new Map();
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (const prereq of command.prerequisites) {
|
|
34
|
-
if (!seen.has(prereq.name)) {
|
|
35
|
-
seen.set(prereq.name, prereq);
|
|
88
|
+
if (definition.prerequisites) {
|
|
89
|
+
for (const spec of definition.prerequisites) {
|
|
90
|
+
if (!seen.has(spec.name)) {
|
|
91
|
+
seen.set(spec.name, compilePrerequisiteSpec(spec));
|
|
36
92
|
}
|
|
37
93
|
}
|
|
38
94
|
}
|
|
95
|
+
collectFromSteps(definition.steps, registry, seen);
|
|
39
96
|
return Array.from(seen.values());
|
|
40
97
|
}
|
|
98
|
+
function collectFromSteps(steps, registry, seen) {
|
|
99
|
+
for (const step of steps) {
|
|
100
|
+
if (step.prerequisites) {
|
|
101
|
+
for (const spec of step.prerequisites) {
|
|
102
|
+
if (!seen.has(spec.name)) {
|
|
103
|
+
seen.set(spec.name, compilePrerequisiteSpec(spec));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const command = registry.get(step.type);
|
|
108
|
+
if (command?.prerequisites) {
|
|
109
|
+
for (const prereq of command.prerequisites) {
|
|
110
|
+
if (!seen.has(prereq.name)) {
|
|
111
|
+
seen.set(prereq.name, prereq);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (step.steps && step.steps.length > 0) {
|
|
116
|
+
collectFromSteps(step.steps, registry, seen);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
41
120
|
/**
|
|
42
121
|
* Run all prerequisite checks concurrently. Errors are treated as unsatisfied.
|
|
43
122
|
*/
|
|
@@ -73,4 +152,94 @@ export function formatPrerequisiteErrors(results) {
|
|
|
73
152
|
}
|
|
74
153
|
return lines.join('\n');
|
|
75
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Evaluate all prereqs. On a TTY, prompts the user for unmet env-type prereqs
|
|
157
|
+
* whose spec opted into `promptOnMissing`, writes answers into process.env,
|
|
158
|
+
* then re-checks. Non-TTY calls and non-promptable unmet prereqs short-circuit
|
|
159
|
+
* to a single formatted failure report.
|
|
160
|
+
*/
|
|
161
|
+
export async function resolveUnmetPrerequisites(prerequisites, options = {}) {
|
|
162
|
+
if (prerequisites.length === 0) {
|
|
163
|
+
return { ok: true, resolvedNames: [] };
|
|
164
|
+
}
|
|
165
|
+
const interactive = options.interactive
|
|
166
|
+
?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
167
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
168
|
+
const initial = await checkPrerequisites(prerequisites);
|
|
169
|
+
const unmet = prerequisites.filter((_, i) => !initial[i].satisfied);
|
|
170
|
+
if (unmet.length === 0) {
|
|
171
|
+
return { ok: true, resolvedNames: [] };
|
|
172
|
+
}
|
|
173
|
+
const promptable = unmet.filter(p => interactive && p.promptOnMissing === true && typeof p.envKey === 'string');
|
|
174
|
+
if (!interactive || promptable.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
message: formatPrerequisiteErrors(initial),
|
|
178
|
+
resolvedNames: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
printPreflightBanner(log, unmet.length);
|
|
182
|
+
const promptLine = options.promptLine ?? readLineFromStdin;
|
|
183
|
+
const resolvedNames = [];
|
|
184
|
+
const lock = acquireTTYLock();
|
|
185
|
+
try {
|
|
186
|
+
for (const prereq of promptable) {
|
|
187
|
+
if (options.abortSignal?.aborted) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
message: 'Prerequisite resolution aborted',
|
|
191
|
+
resolvedNames,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (prereq.description)
|
|
195
|
+
log(prereq.description);
|
|
196
|
+
if (prereq.url)
|
|
197
|
+
log(` Docs: ${prereq.url}`);
|
|
198
|
+
const prompt = ` ${prereq.name} > `;
|
|
199
|
+
let answer;
|
|
200
|
+
try {
|
|
201
|
+
answer = await promptLine(prompt, options.abortSignal);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
message: `Prerequisite "${prereq.name}" prompt failed: ${err.message}`,
|
|
207
|
+
resolvedNames,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (!answer || answer.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
message: `Prerequisite "${prereq.name}" was not provided`,
|
|
214
|
+
resolvedNames,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (prereq.envKey) {
|
|
218
|
+
process.env[prereq.envKey] = answer;
|
|
219
|
+
}
|
|
220
|
+
resolvedNames.push(prereq.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
lock.release();
|
|
225
|
+
}
|
|
226
|
+
// Re-check everything — any still unmet (e.g. command/file prereqs that
|
|
227
|
+
// couldn't be resolved via prompt) fail now with the up-to-date report.
|
|
228
|
+
const rerun = await checkPrerequisites(prerequisites);
|
|
229
|
+
const stillUnmet = rerun.filter(r => !r.satisfied);
|
|
230
|
+
if (stillUnmet.length > 0) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
message: formatPrerequisiteErrors(rerun),
|
|
234
|
+
resolvedNames,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, resolvedNames };
|
|
238
|
+
}
|
|
239
|
+
function printPreflightBanner(log, unmetCount) {
|
|
240
|
+
log('');
|
|
241
|
+
log('\x1b[1;36m━━━ Preflight: missing prerequisites ━━━\x1b[0m');
|
|
242
|
+
log(`${unmetCount} prerequisite${unmetCount === 1 ? '' : 's'} need${unmetCount === 1 ? 's' : ''} a value before this spell can cast.`);
|
|
243
|
+
log('');
|
|
244
|
+
}
|
|
76
245
|
//# sourceMappingURL=prerequisite-checker.js.map
|
|
@@ -8,7 +8,7 @@ import { executeParallelSteps } from './parallel-executor.js';
|
|
|
8
8
|
import { rollbackSteps } from './rollback-orchestrator.js';
|
|
9
9
|
import { buildCredentialPatterns, addCredentialPattern, collectCredentialNames } from './credential-masker.js';
|
|
10
10
|
import { executeSingleStep } from './step-executor.js';
|
|
11
|
-
import { collectPrerequisites,
|
|
11
|
+
import { collectPrerequisites, resolveUnmetPrerequisites } from './prerequisite-checker.js';
|
|
12
12
|
import { collectPreflights, checkPreflights, formatPreflightErrors, partitionPreflightResults, runResolutionCommand, } from './preflight-checker.js';
|
|
13
13
|
import { DENY_ALL_GATEWAY } from './capability-gateway.js';
|
|
14
14
|
import { resolveEffectiveSandbox, formatSandboxLog, DEFAULT_SANDBOX_CONFIG, } from './platform-sandbox.js';
|
|
@@ -99,15 +99,18 @@ export class SpellCaster {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
// Pre-flight prerequisite checks
|
|
102
|
+
// Pre-flight prerequisite checks — walks YAML + step-command sources,
|
|
103
|
+
// prompts on a TTY for unmet env-type prereqs (issue #460).
|
|
103
104
|
if (!options.dryRun) {
|
|
104
105
|
const prerequisites = collectPrerequisites(definition, this.registry);
|
|
105
106
|
if (prerequisites.length > 0) {
|
|
106
|
-
const
|
|
107
|
-
|
|
107
|
+
const resolution = await resolveUnmetPrerequisites(prerequisites, {
|
|
108
|
+
abortSignal: options.signal,
|
|
109
|
+
});
|
|
110
|
+
if (!resolution.ok) {
|
|
108
111
|
return this.failureResult(spellId, startTime, [{
|
|
109
112
|
code: 'PREREQUISITES_FAILED',
|
|
110
|
-
message:
|
|
113
|
+
message: resolution.message ?? 'Prerequisites failed',
|
|
111
114
|
}], definition.name);
|
|
112
115
|
}
|
|
113
116
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdin reader — shared interactive-line-of-input helper.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the `prompt` step command and the issue-#460 prerequisite
|
|
5
|
+
* preflight walker; keeps the abort-signal + trimming semantics in one place.
|
|
6
|
+
*/
|
|
7
|
+
import * as readline from 'node:readline';
|
|
8
|
+
/**
|
|
9
|
+
* Read one line from stdin, emitting `promptText` first. Resolves with the
|
|
10
|
+
* user's trimmed answer (empty string if they just hit enter). Honors an
|
|
11
|
+
* optional abort signal — rejects with `Error('Prompt aborted')` if fired.
|
|
12
|
+
*/
|
|
13
|
+
export async function readLineFromStdin(promptText, abortSignal) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
const onAbort = () => { rl.close(); reject(new Error('Prompt aborted')); };
|
|
17
|
+
if (abortSignal) {
|
|
18
|
+
if (abortSignal.aborted) {
|
|
19
|
+
rl.close();
|
|
20
|
+
reject(new Error('Prompt aborted'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
24
|
+
}
|
|
25
|
+
rl.question(promptText, (answer) => {
|
|
26
|
+
if (abortSignal)
|
|
27
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(answer.trim());
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=stdin-reader.js.map
|
|
@@ -18,7 +18,7 @@ export { ConnectorAccessorImpl } from './core/connector-accessor.js';
|
|
|
18
18
|
export { GatedConnectorAccessor } from './core/gated-connector-accessor.js';
|
|
19
19
|
export { checkCapabilities, } from './core/capability-validator.js';
|
|
20
20
|
export { CapabilityGateway, CapabilityDeniedError, DenyAllGateway, DENY_ALL_GATEWAY, discloseStep, discloseSpell, formatStepDisclosure, formatSpellDisclosure, } from './core/capability-gateway.js';
|
|
21
|
-
export { collectPrerequisites, checkPrerequisites, formatPrerequisiteErrors, commandExists, } from './core/prerequisite-checker.js';
|
|
21
|
+
export { collectPrerequisites, checkPrerequisites, formatPrerequisiteErrors, commandExists, compilePrerequisiteSpec, resolveUnmetPrerequisites, } from './core/prerequisite-checker.js';
|
|
22
22
|
export { detectSandboxCapability, resetSandboxCache, resolveSandboxConfig, resolveEffectiveSandbox, formatSandboxLog, loadSandboxConfigFromProject, DEFAULT_SANDBOX_CONFIG, } from './core/platform-sandbox.js';
|
|
23
23
|
export { resolveScopePath, } from './core/sandbox-utils.js';
|
|
24
24
|
export { generateSandboxProfile, wrapWithSandboxExec, } from './core/sandbox-profile.js';
|