mustflow 2.18.2 → 2.18.3
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/dist/cli/commands/run/builtin-dispatch.js +92 -0
- package/dist/cli/commands/run/executor.js +112 -0
- package/dist/cli/commands/run/output.js +59 -0
- package/dist/cli/commands/run/process-tree.js +91 -0
- package/dist/cli/commands/run/receipt.js +42 -0
- package/dist/cli/commands/run.js +11 -380
- package/dist/cli/commands/verify/args.js +262 -0
- package/dist/cli/commands/verify.js +1 -262
- package/dist/cli/index.js +6 -72
- package/dist/cli/lib/command-registry.js +27 -0
- package/package.json +1 -1
- package/templates/default/i18n.toml +7 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
- package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
- package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
- package/templates/default/manifest.toml +8 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../../core/command-classification.js';
|
|
2
|
+
import { getPackageVersion } from '../../lib/package-info.js';
|
|
3
|
+
import { createBufferedReporter } from './output.js';
|
|
4
|
+
/**
|
|
5
|
+
* mf:anchor cli.run.builtin-inprocess
|
|
6
|
+
* purpose: Dispatch selected mustflow built-in commands without spawning a nested CLI process.
|
|
7
|
+
* search: builtin intent, in-process command, nested mf run, run receipt
|
|
8
|
+
* invariant: Only commands classified by command-classification can use this path.
|
|
9
|
+
* risk: config, state
|
|
10
|
+
*/
|
|
11
|
+
async function runKnownBuiltinCommand(args, reporter, lang) {
|
|
12
|
+
const [command, ...commandArgs] = args;
|
|
13
|
+
if ((command === '--version' || command === '-v' || command === 'version') && commandArgs.length === 0) {
|
|
14
|
+
reporter.stdout(getPackageVersion());
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (!canRunMustflowBuiltinInProcess(command)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (command === 'check') {
|
|
21
|
+
return (await import('../check.js')).runCheck(commandArgs, reporter, lang);
|
|
22
|
+
}
|
|
23
|
+
if (command === 'classify') {
|
|
24
|
+
return (await import('../classify.js')).runClassify(commandArgs, reporter, lang);
|
|
25
|
+
}
|
|
26
|
+
if (command === 'context') {
|
|
27
|
+
return (await import('../context.js')).runContext(commandArgs, reporter, lang);
|
|
28
|
+
}
|
|
29
|
+
if (command === 'doctor') {
|
|
30
|
+
return (await import('../doctor.js')).runDoctor(commandArgs, reporter, lang);
|
|
31
|
+
}
|
|
32
|
+
if (command === 'help') {
|
|
33
|
+
return (await import('../help.js')).runHelp(commandArgs, reporter, lang);
|
|
34
|
+
}
|
|
35
|
+
if (command === 'impact') {
|
|
36
|
+
return (await import('../impact.js')).runImpact(commandArgs, reporter, lang);
|
|
37
|
+
}
|
|
38
|
+
if (command === 'line-endings') {
|
|
39
|
+
return (await import('../line-endings.js')).runLineEndings(commandArgs, reporter, lang);
|
|
40
|
+
}
|
|
41
|
+
if (command === 'map') {
|
|
42
|
+
return (await import('../map.js')).runMap(commandArgs, reporter, lang);
|
|
43
|
+
}
|
|
44
|
+
if (command === 'status') {
|
|
45
|
+
return (await import('../status.js')).runStatus(commandArgs, reporter, lang);
|
|
46
|
+
}
|
|
47
|
+
if (command === 'update') {
|
|
48
|
+
return (await import('../update.js')).runUpdate(commandArgs, reporter, lang);
|
|
49
|
+
}
|
|
50
|
+
if (command === 'version-sources') {
|
|
51
|
+
return (await import('../version-sources.js')).runVersionSources(commandArgs, reporter, lang);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
async function withWorkingDirectory(cwd, callback) {
|
|
56
|
+
const previousCwd = process.cwd();
|
|
57
|
+
process.chdir(cwd);
|
|
58
|
+
try {
|
|
59
|
+
return await callback();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
process.chdir(previousCwd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
|
|
66
|
+
const [command = '', ...builtinArgs] = commandArgv;
|
|
67
|
+
if (!isMustflowBinName(command)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const output = createBufferedReporter();
|
|
71
|
+
try {
|
|
72
|
+
const status = await withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
|
|
73
|
+
if (status === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
signal: null,
|
|
79
|
+
stdout: output.stdout(),
|
|
80
|
+
stderr: output.stderr(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
return {
|
|
86
|
+
status: 1,
|
|
87
|
+
signal: null,
|
|
88
|
+
stdout: output.stdout(),
|
|
89
|
+
stderr: `${output.stderr()}${message}\n`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
|
|
3
|
+
import { createPendingTimeoutTermination, forceTerminateProcessTreeNonBlocking, getKillMethod, terminateProcessTreeNonBlocking, } from './process-tree.js';
|
|
4
|
+
import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
|
|
5
|
+
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
8
|
+
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
9
|
+
let settled = false;
|
|
10
|
+
let timedOut = false;
|
|
11
|
+
let childError;
|
|
12
|
+
let childPid;
|
|
13
|
+
let stdoutBytes = 0;
|
|
14
|
+
let stderrBytes = 0;
|
|
15
|
+
let timeout;
|
|
16
|
+
let termination = null;
|
|
17
|
+
const child = spawn(command.executable, command.args ?? [], {
|
|
18
|
+
cwd,
|
|
19
|
+
env,
|
|
20
|
+
shell: command.shell,
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
detached: process.platform !== 'win32',
|
|
24
|
+
});
|
|
25
|
+
childPid = child.pid;
|
|
26
|
+
const finish = (status, signal) => {
|
|
27
|
+
if (settled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
settled = true;
|
|
31
|
+
if (timeout) {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
}
|
|
34
|
+
resolve({
|
|
35
|
+
status: timedOut ? null : status,
|
|
36
|
+
signal: timedOut ? null : signal,
|
|
37
|
+
error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
|
|
38
|
+
stdout: stdout.toSnapshot(),
|
|
39
|
+
stderr: stderr.toSnapshot(),
|
|
40
|
+
pid: childPid,
|
|
41
|
+
termination,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const stopForOutputLimit = (stream) => {
|
|
45
|
+
if (settled || childError) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
childError = createOutputLimitError(stream, maxOutputBytes);
|
|
49
|
+
child.stdout?.destroy();
|
|
50
|
+
child.stderr?.destroy();
|
|
51
|
+
child.unref();
|
|
52
|
+
terminateProcessTreeNonBlocking(childPid);
|
|
53
|
+
forceTerminateProcessTreeNonBlocking(childPid);
|
|
54
|
+
finish(null, null);
|
|
55
|
+
};
|
|
56
|
+
child.stdout?.on('data', (chunk) => {
|
|
57
|
+
stdout.append(chunk);
|
|
58
|
+
stdoutBytes += chunk.byteLength;
|
|
59
|
+
if (streamOutput) {
|
|
60
|
+
writeStreamChunk(reporter, 'stdout', chunk);
|
|
61
|
+
}
|
|
62
|
+
if (enforceOutputLimit && stdoutBytes > maxOutputBytes) {
|
|
63
|
+
stopForOutputLimit('stdout');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
child.stderr?.on('data', (chunk) => {
|
|
67
|
+
stderr.append(chunk);
|
|
68
|
+
stderrBytes += chunk.byteLength;
|
|
69
|
+
if (streamOutput) {
|
|
70
|
+
writeStreamChunk(reporter, 'stderr', chunk);
|
|
71
|
+
}
|
|
72
|
+
if (enforceOutputLimit && stderrBytes > maxOutputBytes) {
|
|
73
|
+
stopForOutputLimit('stderr');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
child.once('error', (error) => {
|
|
77
|
+
childError = error;
|
|
78
|
+
});
|
|
79
|
+
child.once('close', (status, signal) => {
|
|
80
|
+
finish(status, signal);
|
|
81
|
+
});
|
|
82
|
+
timeout = setTimeout(() => {
|
|
83
|
+
timedOut = true;
|
|
84
|
+
child.stdout?.destroy();
|
|
85
|
+
child.stderr?.destroy();
|
|
86
|
+
child.unref();
|
|
87
|
+
termination = createPendingTimeoutTermination(getKillMethod());
|
|
88
|
+
terminateProcessTreeNonBlocking(childPid);
|
|
89
|
+
forceTerminateProcessTreeNonBlocking(childPid);
|
|
90
|
+
finish(null, null);
|
|
91
|
+
}, timeoutSeconds * 1000);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
95
|
+
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
96
|
+
}
|
|
97
|
+
export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
98
|
+
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
99
|
+
}
|
|
100
|
+
export function getRunStatus(error, exitCode, successExitCodes) {
|
|
101
|
+
const errorWithCode = error;
|
|
102
|
+
if (errorWithCode?.code === 'ETIMEDOUT') {
|
|
103
|
+
return 'timed_out';
|
|
104
|
+
}
|
|
105
|
+
if (isOutputLimitExceededError(error)) {
|
|
106
|
+
return 'output_limit_exceeded';
|
|
107
|
+
}
|
|
108
|
+
if (error) {
|
|
109
|
+
return 'start_failed';
|
|
110
|
+
}
|
|
111
|
+
return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
|
|
112
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
|
|
2
|
+
const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
|
|
3
|
+
export function emitOutput(reporter, output, stream) {
|
|
4
|
+
if (!output) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
|
|
8
|
+
if (text.length === 0) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
reporter[stream](text);
|
|
12
|
+
}
|
|
13
|
+
export function createBufferedReporter() {
|
|
14
|
+
const stdout = [];
|
|
15
|
+
const stderr = [];
|
|
16
|
+
return {
|
|
17
|
+
reporter: {
|
|
18
|
+
stdout(message) {
|
|
19
|
+
stdout.push(`${message}\n`);
|
|
20
|
+
},
|
|
21
|
+
stderr(message) {
|
|
22
|
+
stderr.push(`${message}\n`);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
stdout() {
|
|
26
|
+
return stdout.join('');
|
|
27
|
+
},
|
|
28
|
+
stderr() {
|
|
29
|
+
return stderr.join('');
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function writeStreamChunk(reporter, stream, chunk) {
|
|
34
|
+
if (stream === 'stdout') {
|
|
35
|
+
if (reporter.writeStdout) {
|
|
36
|
+
reporter.writeStdout(chunk);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
reporter.stdout(chunk.toString());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (reporter.writeStderr) {
|
|
43
|
+
reporter.writeStderr(chunk);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
reporter.stderr(chunk.toString());
|
|
47
|
+
}
|
|
48
|
+
export function createOutputLimitError(stream, maxOutputBytes) {
|
|
49
|
+
return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
|
|
50
|
+
code: OUTPUT_LIMIT_ERROR_CODE,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function isOutputLimitExceededError(error) {
|
|
54
|
+
if (!error) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const errorWithCode = error;
|
|
58
|
+
return errorWithCode.code === OUTPUT_LIMIT_ERROR_CODE || OUTPUT_LIMIT_ERROR_MESSAGE.test(error.message);
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
function signalProcessTree(pid, signal) {
|
|
3
|
+
if (!pid || pid <= 0) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
8
|
+
stdio: 'ignore',
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
});
|
|
11
|
+
if (signal === 'SIGKILL') {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, signal);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// taskkill may already have terminated the direct child.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
process.kill(-pid, signal);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, signal);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// The child may already be gone after Node's spawn timeout handling.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function signalProcessTreeNonBlocking(pid, signal) {
|
|
34
|
+
if (!pid || pid <= 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
39
|
+
stdio: 'ignore',
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
detached: true,
|
|
42
|
+
});
|
|
43
|
+
killer.unref();
|
|
44
|
+
if (signal === 'SIGKILL') {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, signal);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// taskkill may already have terminated the direct child.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
process.kill(-pid, signal);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, signal);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// The child may already be gone after the timeout fired.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function terminateProcessTree(pid) {
|
|
67
|
+
signalProcessTree(pid, 'SIGTERM');
|
|
68
|
+
}
|
|
69
|
+
export function forceTerminateProcessTree(pid) {
|
|
70
|
+
signalProcessTree(pid, 'SIGKILL');
|
|
71
|
+
}
|
|
72
|
+
export function terminateProcessTreeNonBlocking(pid) {
|
|
73
|
+
signalProcessTreeNonBlocking(pid, 'SIGTERM');
|
|
74
|
+
}
|
|
75
|
+
export function forceTerminateProcessTreeNonBlocking(pid) {
|
|
76
|
+
signalProcessTreeNonBlocking(pid, 'SIGKILL');
|
|
77
|
+
}
|
|
78
|
+
export function getKillMethod() {
|
|
79
|
+
return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
|
|
80
|
+
}
|
|
81
|
+
export function createPendingTimeoutTermination(method) {
|
|
82
|
+
return {
|
|
83
|
+
reason: 'timeout',
|
|
84
|
+
method,
|
|
85
|
+
graceful_signal: 'SIGTERM',
|
|
86
|
+
forced_signal: 'SIGKILL',
|
|
87
|
+
forced_kill_attempted: true,
|
|
88
|
+
confirmed: false,
|
|
89
|
+
cleanup_pending: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createRunReceipt, createRunReceiptRelativePath, } from '../../../core/run-receipt.js';
|
|
2
|
+
export function assembleRunReceipt(input) {
|
|
3
|
+
return createRunReceipt({
|
|
4
|
+
intent: input.intentName,
|
|
5
|
+
status: input.runStatus,
|
|
6
|
+
timedOut: input.runStatus === 'timed_out',
|
|
7
|
+
startedAt: input.startedAt,
|
|
8
|
+
finishedAt: input.finishedAt,
|
|
9
|
+
projectRoot: input.projectRoot,
|
|
10
|
+
cwd: input.plan.cwd,
|
|
11
|
+
lifecycle: input.plan.lifecycle ?? 'oneshot',
|
|
12
|
+
runPolicy: input.plan.runPolicy ?? 'agent_allowed',
|
|
13
|
+
mode: input.plan.mode,
|
|
14
|
+
argv: input.plan.commandArgv,
|
|
15
|
+
cmd: input.plan.shellCommand,
|
|
16
|
+
envPolicy: input.plan.envPolicy,
|
|
17
|
+
envAllowlist: input.plan.envAllowlist,
|
|
18
|
+
timeoutSeconds: input.plan.timeoutSeconds,
|
|
19
|
+
maxOutputBytes: input.plan.maxOutputBytes,
|
|
20
|
+
successExitCodes: input.plan.successExitCodes,
|
|
21
|
+
exitCode: input.exitCode,
|
|
22
|
+
signal: input.result.signal,
|
|
23
|
+
error: input.result.error?.message ?? null,
|
|
24
|
+
killMethod: input.killMethod,
|
|
25
|
+
termination: input.termination,
|
|
26
|
+
stdout: input.result.stdout,
|
|
27
|
+
stderr: input.result.stderr,
|
|
28
|
+
writeDrift: input.writeDrift,
|
|
29
|
+
executorOverheadMs: input.executorOverheadMs,
|
|
30
|
+
phaseTimings: input.phaseTimings,
|
|
31
|
+
selectionSummary: {
|
|
32
|
+
strategy: input.plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
|
|
33
|
+
changed_file_count: 0,
|
|
34
|
+
changed_surface_counts: {},
|
|
35
|
+
selected_target_count: Math.max(1, input.plan.testTargets.length),
|
|
36
|
+
fallback_used: false,
|
|
37
|
+
},
|
|
38
|
+
stdoutTailBytes: input.stdoutTailBytes,
|
|
39
|
+
stderrTailBytes: input.stderrTailBytes,
|
|
40
|
+
receiptPath: createRunReceiptRelativePath(),
|
|
41
|
+
});
|
|
42
|
+
}
|