@deeeed/metamask-harness 0.67.1 → 0.69.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.
- package/CHANGELOG.md +21 -0
- package/adapters/mobile/metro-config.cjs +4 -0
- package/adapters/mobile/start-metro.sh +14 -3
- package/dist/adapters/mobile/prepare.js +12 -7
- package/dist/adapters/mobile/runtime-decision.js +11 -11
- package/dist/adapters/mobile/verify.js +56 -102
- package/dist/checkout-lock.js +62 -10
- package/dist/command-contract.js +8 -0
- package/dist/command-journal.js +52 -7
- package/dist/commands/call.js +3 -0
- package/dist/commands/checklist.js +13 -2
- package/dist/commands/fixtures.js +4 -5
- package/dist/commands/launch/index.js +19 -0
- package/dist/commands/list-executables.js +62 -11
- package/dist/commands/manifest.js +11 -4
- package/dist/commands/prepare.js +25 -4
- package/dist/commands/run-engine.js +8 -2
- package/dist/commands/run.js +3 -0
- package/dist/commands/runtime-launch.js +19 -7
- package/dist/commands/shared.js +48 -21
- package/dist/commands/status-watch.js +23 -0
- package/dist/execution-provenance.js +1 -1
- package/dist/harness.js +7 -11
- package/dist/json-stream.js +3 -0
- package/dist/live-adapter-contract.js +5 -0
- package/dist/mm-harness-cli.js +10 -1
- package/dist/task-view.js +8 -1
- package/docs/CONTRIBUTING.md +37 -0
- package/docs/RECIPES.md +10 -3
- package/library/actions/mobile/ui/native-navigation.mjs +18 -6
- package/package.json +5 -5
- package/scripts/completions.sh +2 -2
package/dist/command-journal.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { OperationRecord, readOperations, processIdentity, ensureRuntimeDirectory } from "@farmslot/recipe-harness/runtime/operation";
|
|
5
|
+
import { latestTaskDir } from "@farmslot/agent-runtime";
|
|
4
6
|
import { recipeRuntimeDir } from "./paths.js";
|
|
5
7
|
const COMMAND_JOURNAL_FILE = "last-command.json";
|
|
6
8
|
const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
|
|
9
|
+
"prepare",
|
|
7
10
|
"call",
|
|
8
11
|
"check",
|
|
9
12
|
"checklist",
|
|
@@ -12,6 +15,7 @@ const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
12
15
|
"fixtures",
|
|
13
16
|
"install",
|
|
14
17
|
"launch",
|
|
18
|
+
"runtime-launch",
|
|
15
19
|
"provision",
|
|
16
20
|
"recipe-quality",
|
|
17
21
|
"run",
|
|
@@ -25,6 +29,8 @@ async function withCommandJournal(command, argv, execute) {
|
|
|
25
29
|
if (!JOURNALED_COMMANDS.has(command)) return execute();
|
|
26
30
|
const handle = tryBeginCommandJournal(command, argv);
|
|
27
31
|
const previous = activeCommandJournal;
|
|
32
|
+
const parentId = process.env.MM_HARNESS_OPERATION_ID;
|
|
33
|
+
if (handle) process.env.MM_HARNESS_OPERATION_ID = handle.operation.value.id;
|
|
28
34
|
activeCommandJournal = handle;
|
|
29
35
|
try {
|
|
30
36
|
const exitCode = await execute();
|
|
@@ -35,8 +41,16 @@ async function withCommandJournal(command, argv, execute) {
|
|
|
35
41
|
throw error;
|
|
36
42
|
} finally {
|
|
37
43
|
activeCommandJournal = previous;
|
|
44
|
+
if (parentId) process.env.MM_HARNESS_OPERATION_ID = parentId;
|
|
45
|
+
else delete process.env.MM_HARNESS_OPERATION_ID;
|
|
38
46
|
}
|
|
39
47
|
}
|
|
48
|
+
function recordCommandStage(stage) {
|
|
49
|
+
activeCommandJournal?.operation.stage(stage);
|
|
50
|
+
}
|
|
51
|
+
function recordCommandOutput(chunk) {
|
|
52
|
+
activeCommandJournal?.operation.output(chunk);
|
|
53
|
+
}
|
|
40
54
|
function recordCommandEvidence(...evidence) {
|
|
41
55
|
const handle = activeCommandJournal;
|
|
42
56
|
if (!handle || evidence.length === 0) return;
|
|
@@ -58,7 +72,12 @@ function commandJournalPath(target, runtimeDir) {
|
|
|
58
72
|
return path.join(path.resolve(target), relative, COMMAND_JOURNAL_FILE);
|
|
59
73
|
}
|
|
60
74
|
function readCommandJournal(target, runtimeDir) {
|
|
61
|
-
const
|
|
75
|
+
const legacyFile = commandJournalPath(target, runtimeDir);
|
|
76
|
+
const operations = readOperations(path.join(path.dirname(legacyFile), "operations"));
|
|
77
|
+
const live = operations.filter((operation) => operation.status === "running" && processIdentity(operation.pid) === operation.processStartedAt);
|
|
78
|
+
const roots = operations.filter((operation) => !operation.parentId).sort((a, b) => (a.finishedAt ?? a.startedAt).localeCompare(b.finishedAt ?? b.startedAt));
|
|
79
|
+
const selected = live.find((operation) => !operation.parentId) ?? live.at(-1) ?? roots.at(-1) ?? operations.at(-1);
|
|
80
|
+
const file = selected ? path.join(path.dirname(legacyFile), "operations", `${selected.id}.journal.json`) : legacyFile;
|
|
62
81
|
let descriptor;
|
|
63
82
|
try {
|
|
64
83
|
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
@@ -113,12 +132,34 @@ function redactCommandArgs(argv) {
|
|
|
113
132
|
}
|
|
114
133
|
return redacted;
|
|
115
134
|
}
|
|
135
|
+
function pruneCompletedJournals(directory, operations) {
|
|
136
|
+
const completed = operations.filter((operation) => operation.status !== "running");
|
|
137
|
+
for (const operation of completed.slice(0, -100)) {
|
|
138
|
+
for (const suffix of [".json", ".journal.json", ".log"]) {
|
|
139
|
+
fs.rmSync(path.join(directory, `${operation.id}${suffix}`), { force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
116
143
|
function tryBeginCommandJournal(command, argv) {
|
|
117
144
|
try {
|
|
118
145
|
const target = invocationTarget(argv);
|
|
119
146
|
if (!fs.existsSync(target)) return void 0;
|
|
120
147
|
const runtimeDir = optionValue(argv, "--runtime-dir");
|
|
121
|
-
const
|
|
148
|
+
const legacyFile = commandJournalPath(target, runtimeDir);
|
|
149
|
+
const taskDir = latestTaskDir(path.join(target, "temp", "tasks"));
|
|
150
|
+
const signalFile = taskDir && path.join(taskDir, "SIGNAL.json");
|
|
151
|
+
const liveTask = signalFile && fs.existsSync(signalFile) && JSON.parse(fs.readFileSync(signalFile, "utf8")).status === "running";
|
|
152
|
+
const directory = path.join(path.dirname(legacyFile), "operations");
|
|
153
|
+
const operations = readOperations(directory, (error) => process.stderr.write(`mm-harness: ignoring invalid prior operation: ${error.message}
|
|
154
|
+
`));
|
|
155
|
+
pruneCompletedJournals(directory, operations);
|
|
156
|
+
const inherited = process.env.MM_HARNESS_OPERATION_ID;
|
|
157
|
+
const parent = inherited && operations.find((operation2) => operation2.id === inherited && operation2.target === target && operation2.status === "running" && processIdentity(operation2.pid) === operation2.processStartedAt);
|
|
158
|
+
const operation = new OperationRecord(directory, command, target, {
|
|
159
|
+
...parent ? { parentId: parent.id } : {},
|
|
160
|
+
...liveTask ? { mirrorDirectory: path.join(taskDir, "artifacts", "operations") } : {}
|
|
161
|
+
});
|
|
162
|
+
const file = path.join(directory, `${operation.value.id}.journal.json`);
|
|
122
163
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
123
164
|
const record = {
|
|
124
165
|
schemaVersion: 1,
|
|
@@ -132,7 +173,7 @@ function tryBeginCommandJournal(command, argv) {
|
|
|
132
173
|
finishedAt: null
|
|
133
174
|
};
|
|
134
175
|
writeAtomic(file, record);
|
|
135
|
-
return { file, record };
|
|
176
|
+
return { file, record, operation };
|
|
136
177
|
} catch (error) {
|
|
137
178
|
process.stderr.write(
|
|
138
179
|
`mm-harness: resumability journal unavailable: ${errorMessage(error)}
|
|
@@ -155,14 +196,17 @@ function tryFinishCommandJournal(handle, exitCode) {
|
|
|
155
196
|
`mm-harness: resumability journal could not finalize: ${errorMessage(error)}
|
|
156
197
|
`
|
|
157
198
|
);
|
|
199
|
+
} finally {
|
|
200
|
+
handle.operation.finish(exitCode);
|
|
158
201
|
}
|
|
159
202
|
}
|
|
160
203
|
function writeAtomic(file, record) {
|
|
161
|
-
|
|
204
|
+
ensureRuntimeDirectory(path.dirname(file));
|
|
162
205
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
163
206
|
let descriptor;
|
|
164
207
|
try {
|
|
165
208
|
descriptor = fs.openSync(temporary, "wx", 384);
|
|
209
|
+
fs.fchmodSync(descriptor, 384);
|
|
166
210
|
fs.writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}
|
|
167
211
|
`);
|
|
168
212
|
fs.fsyncSync(descriptor);
|
|
@@ -175,9 +219,8 @@ function writeAtomic(file, record) {
|
|
|
175
219
|
}
|
|
176
220
|
}
|
|
177
221
|
function invocationTarget(argv) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
);
|
|
222
|
+
const target = path.resolve(optionValue(argv, "--target") ?? process.cwd());
|
|
223
|
+
return fs.existsSync(target) ? fs.realpathSync(target) : target;
|
|
181
224
|
}
|
|
182
225
|
function evidencePaths(argv) {
|
|
183
226
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -257,6 +300,8 @@ export {
|
|
|
257
300
|
isSensitiveKey,
|
|
258
301
|
readCommandJournal,
|
|
259
302
|
recordCommandEvidence,
|
|
303
|
+
recordCommandOutput,
|
|
304
|
+
recordCommandStage,
|
|
260
305
|
redactCommandArgs,
|
|
261
306
|
redactStructuredValue,
|
|
262
307
|
withCommandJournal
|
package/dist/commands/call.js
CHANGED
|
@@ -65,6 +65,9 @@ async function handleCall(argv) {
|
|
|
65
65
|
}
|
|
66
66
|
const { action: shortName, args, rest } = parseCallArgs(argv);
|
|
67
67
|
const { options } = parseArgs(rest, "call");
|
|
68
|
+
if (["domain", "source", "sort"].some((name) => optionString(options, name) !== void 0)) {
|
|
69
|
+
throw usageError("--domain, --source, and --sort require call --list.");
|
|
70
|
+
}
|
|
68
71
|
const json = optionFlag(options, "json");
|
|
69
72
|
if (!shortName) {
|
|
70
73
|
let example = "mm-harness call <action>";
|
|
@@ -12,6 +12,7 @@ const MARK_SCRIPT = require2.resolve(
|
|
|
12
12
|
const { resolveChecklistPaths } = require2(
|
|
13
13
|
"@farmslot/agent-runtime/scripts/checklist-target.cjs"
|
|
14
14
|
);
|
|
15
|
+
const { readSubtaskIndex, SubtaskRefusal } = require2("@farmslot/agent-runtime/scripts/subtask-unit.cjs");
|
|
15
16
|
const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
|
|
16
17
|
const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
17
18
|
"--reason",
|
|
@@ -129,10 +130,10 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
|
129
130
|
const step = enumerateChecklistCheckboxes(checklist).find((item) => item.stepNumber === stepNumber);
|
|
130
131
|
return step !== void 0 && /mm-harness check diff/u.test(step.rawLabel);
|
|
131
132
|
}
|
|
132
|
-
function checkDiffGateReady(taskDir, step) {
|
|
133
|
+
function checkDiffGateReady(taskDir, step, parentChecklist) {
|
|
133
134
|
const stepNumber = numericMarkStep(step);
|
|
134
135
|
if (stepNumber === null) return true;
|
|
135
|
-
const
|
|
136
|
+
const taskPath = parentChecklist ? path.join(taskDir, parentChecklist) : resolveChecklistPaths(taskDir).taskPath;
|
|
136
137
|
let checklist = "";
|
|
137
138
|
try {
|
|
138
139
|
checklist = fs.readFileSync(taskPath, "utf8");
|
|
@@ -203,6 +204,16 @@ async function handleChecklist(argv) {
|
|
|
203
204
|
return EXIT.usage;
|
|
204
205
|
}
|
|
205
206
|
if (preserveTerminalSignal(resolvedTaskDir, step)) return EXIT.ok;
|
|
207
|
+
if (step === "sub" && forwarded[2] === "complete") {
|
|
208
|
+
try {
|
|
209
|
+
const unit = readSubtaskIndex(resolvedTaskDir)?.units.find((unit2) => unit2.id === forwarded[1]);
|
|
210
|
+
if (unit && !checkDiffGateReady(resolvedTaskDir, String(unit.parent.stepNumber), unit.parent.checklist)) return EXIT.runtime;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (!(error instanceof SubtaskRefusal)) throw error;
|
|
213
|
+
console.error(error.message);
|
|
214
|
+
return error.code;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
206
217
|
if (!checkDiffGateReady(resolvedTaskDir, step)) return EXIT.runtime;
|
|
207
218
|
teachRecipeCompletion(resolvedTaskDir, step);
|
|
208
219
|
const result = spawnSync(
|
|
@@ -559,13 +559,12 @@ async function fixturesFinalize(adapter, target, options, json) {
|
|
|
559
559
|
}
|
|
560
560
|
return exitCode;
|
|
561
561
|
}
|
|
562
|
-
function fixturesSync(adapter, target, json) {
|
|
562
|
+
async function fixturesSync(adapter, target, json) {
|
|
563
563
|
const mmHarnessBin = path.join(runnerDir, "bin/mm-harness");
|
|
564
|
-
const installResult =
|
|
564
|
+
const installResult = await spawnScriptStreaming(
|
|
565
565
|
mmHarnessBin,
|
|
566
566
|
["install", "--adapter", adapter, "--target", target],
|
|
567
|
-
target
|
|
568
|
-
json
|
|
567
|
+
target
|
|
569
568
|
);
|
|
570
569
|
if (installResult.status !== 0) {
|
|
571
570
|
if (!json) {
|
|
@@ -578,7 +577,7 @@ function fixturesSync(adapter, target, json) {
|
|
|
578
577
|
const syncArgs = ["--target", target];
|
|
579
578
|
if (process.env.CDP_PORT) syncArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
580
579
|
if (process.env.RECIPE_SLOT_ID) syncArgs.push("--slot-id", process.env.RECIPE_SLOT_ID);
|
|
581
|
-
const syncResult =
|
|
580
|
+
const syncResult = await spawnScriptStreaming(syncFixtureSh, syncArgs, target);
|
|
582
581
|
if (syncResult.status !== 0 || fixtureSummary(target, adapter).status !== "ready") {
|
|
583
582
|
if (!json) console.error("\u2717 no usable wallet fixture source was found.\n Next: mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
584
583
|
return EXIT.runtime;
|
|
@@ -320,6 +320,10 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
320
320
|
if (attempt.status === 0) {
|
|
321
321
|
return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
|
|
322
322
|
}
|
|
323
|
+
const readinessFailure = mobileReadinessFailure(attempt);
|
|
324
|
+
if (readinessFailure) {
|
|
325
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, readinessFailure);
|
|
326
|
+
}
|
|
323
327
|
if (adapter === "extension" && attempt.output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
|
|
324
328
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
325
329
|
code: "REMOTE_FLAG_NOT_PINNABLE",
|
|
@@ -503,6 +507,10 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
503
507
|
state.recovered.push(recoveryCode);
|
|
504
508
|
return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
|
|
505
509
|
}
|
|
510
|
+
const retryReadinessFailure = mobileReadinessFailure(attempt);
|
|
511
|
+
if (retryReadinessFailure) {
|
|
512
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, retryReadinessFailure);
|
|
513
|
+
}
|
|
506
514
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
507
515
|
code: "SAME_RECOVERY_TWICE",
|
|
508
516
|
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
|
|
@@ -511,6 +519,17 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
511
519
|
originalError: attempt.output.trim() || void 0
|
|
512
520
|
});
|
|
513
521
|
}
|
|
522
|
+
function mobileReadinessFailure(result) {
|
|
523
|
+
const block = result.readinessBlock;
|
|
524
|
+
if (!block) return void 0;
|
|
525
|
+
return {
|
|
526
|
+
code: block.reasonCode === "deps-not-ready" ? "MOBILE_DEPENDENCIES_NOT_READY" : "MOBILE_READINESS_BLOCKED",
|
|
527
|
+
message: block.message,
|
|
528
|
+
userAction: block.userAction,
|
|
529
|
+
recoverable: false,
|
|
530
|
+
exitCode: result.status
|
|
531
|
+
};
|
|
532
|
+
}
|
|
514
533
|
function extensionQuickReattachFailed(output) {
|
|
515
534
|
return /\[reattach\]|extension quick reattach|mm-harness launch --build/u.test(output);
|
|
516
535
|
}
|
|
@@ -5,13 +5,14 @@ import { importRecipeProtocol } from "../paths.js";
|
|
|
5
5
|
import { color } from "../cli-color.js";
|
|
6
6
|
import { EXIT } from "./shared.js";
|
|
7
7
|
import { optionFlag, optionString, optionStrings, resolveAdapter, shellQuoteArg } from "./parse-args.js";
|
|
8
|
-
import { describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
|
|
8
|
+
import { actionDomain, describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
|
|
9
9
|
import {
|
|
10
10
|
describeRunnableRecipe,
|
|
11
11
|
listRunnableRecipes,
|
|
12
12
|
resolveMetaMaskLibrarySources
|
|
13
13
|
} from "./run-engine.js";
|
|
14
14
|
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
15
|
+
const recipeDomain = (name) => name.includes(".") ? name.split(".")[0].replaceAll("_", "-") : "general";
|
|
15
16
|
function catalogDescription(description) {
|
|
16
17
|
if (!description) return "";
|
|
17
18
|
const firstSentence = description.match(/^.*?[.!?](?:\s|$)/u)?.[0]?.trim() ?? description;
|
|
@@ -20,23 +21,64 @@ function catalogDescription(description) {
|
|
|
20
21
|
async function handleListExecutables(command, options) {
|
|
21
22
|
const json = optionFlag(options, "json");
|
|
22
23
|
const { adapter } = resolveAdapter(options);
|
|
24
|
+
const domainFilter = optionString(options, "domain")?.replaceAll("_", "-");
|
|
25
|
+
const sourceFilter = optionString(options, "source");
|
|
26
|
+
const sort = optionString(options, "sort") ?? (json ? "name" : "domain");
|
|
27
|
+
const groupKey = (entry, domain) => sort === "library" ? entry.source : sort === "domain" ? domain : "";
|
|
28
|
+
const select = (available2, domainOf) => available2.filter((entry) => (!domainFilter || domainOf(entry) === domainFilter) && (!sourceFilter || entry.source === sourceFilter)).sort((left, right) => groupKey(left, domainOf(left)).localeCompare(groupKey(right, domainOf(right))) || left.name.localeCompare(right.name));
|
|
29
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
30
|
+
const stylesFor = (domains) => {
|
|
31
|
+
const palette = ["label", "accent", "ok", "warn"];
|
|
32
|
+
return new Map([...new Set(domains)].sort().map((domain, index) => [domain, palette[index % palette.length]]));
|
|
33
|
+
};
|
|
23
34
|
const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
|
|
35
|
+
const libraryLines = () => {
|
|
36
|
+
const lines = ["Libraries loaded:"];
|
|
37
|
+
for (const source of librarySources ?? []) {
|
|
38
|
+
const name = source.name ?? path.basename(source.root);
|
|
39
|
+
const kind = source.provenance?.kind === "bundled" ? "bundled" : "linked";
|
|
40
|
+
lines.push(` ${out("bold", name)} ${kind} \xB7 ${source.root}`);
|
|
41
|
+
}
|
|
42
|
+
if (domainFilter || sourceFilter) {
|
|
43
|
+
lines.push(`Filter: ${[domainFilter && `domain=${domainFilter}`, sourceFilter && `source=${sourceFilter}`].filter(Boolean).join(" ")}`);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
};
|
|
24
47
|
if (command === "run") {
|
|
25
|
-
const
|
|
48
|
+
const available2 = await listRunnableRecipes(adapter, librarySources);
|
|
49
|
+
const recipes = select(available2, (recipe) => recipeDomain(recipe.name));
|
|
26
50
|
if (json) {
|
|
27
51
|
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, recipes }, null, 2));
|
|
28
52
|
return EXIT.ok;
|
|
29
53
|
}
|
|
30
|
-
const
|
|
54
|
+
const domainStyles = stylesFor(available2.map((recipe) => recipeDomain(recipe.name)));
|
|
31
55
|
console.log(out("bold", `runnable recipes (${adapter})`));
|
|
56
|
+
for (const line of libraryLines()) console.log(line);
|
|
32
57
|
console.log(out("comment", "Inspect: mm-harness run <recipe> --describe"));
|
|
58
|
+
if (recipes.length === 0) {
|
|
59
|
+
console.log("No runnable recipes match.");
|
|
60
|
+
console.log(`Available domains: ${[...domainStyles.keys()].join(", ") || "none"}`);
|
|
61
|
+
console.log(`Available sources: ${[...new Set(available2.map((recipe) => recipe.source))].join(", ") || "none"}`);
|
|
62
|
+
}
|
|
63
|
+
const groups = /* @__PURE__ */ new Map();
|
|
33
64
|
for (const recipe of recipes) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
const group = groupKey(recipe, recipeDomain(recipe.name));
|
|
66
|
+
groups.set(group, [...groups.get(group) ?? [], recipe]);
|
|
67
|
+
}
|
|
68
|
+
for (const [group, entries] of groups) {
|
|
69
|
+
if (group) {
|
|
70
|
+
const label = sort === "library" ? `Library: ${group}` : group;
|
|
71
|
+
console.log(`
|
|
72
|
+
${out(sort === "domain" ? domainStyles.get(group) : "bold", label)} ${out("dim", `(${entries.length})`)}`);
|
|
73
|
+
}
|
|
74
|
+
for (const recipe of entries) {
|
|
75
|
+
const style = domainStyles.get(recipeDomain(recipe.name));
|
|
76
|
+
const shadowed = recipe.shadows.length > 0 ? ` shadows=${recipe.shadows.join(",")}` : "";
|
|
77
|
+
const variant = recipe.variant ? ` variant=${recipe.variant}` : " variant=all";
|
|
78
|
+
const summary = catalogDescription(recipe.description);
|
|
79
|
+
console.log(` ${out(style, recipe.name)} ${out("dim", `library=${recipe.source}${variant}${shadowed}`)}`);
|
|
80
|
+
if (summary) console.log(` ${summary}`);
|
|
81
|
+
}
|
|
40
82
|
}
|
|
41
83
|
return EXIT.ok;
|
|
42
84
|
}
|
|
@@ -47,9 +89,10 @@ async function handleListExecutables(command, options) {
|
|
|
47
89
|
);
|
|
48
90
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
49
91
|
const protocolNames = new Set(getRecipeActionManifestActionNames(manifest));
|
|
50
|
-
const
|
|
92
|
+
const available = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
|
|
93
|
+
const described = select(available, actionDomain);
|
|
51
94
|
const shortCounts = /* @__PURE__ */ new Map();
|
|
52
|
-
for (const entry of
|
|
95
|
+
for (const entry of available) {
|
|
53
96
|
const short = finalSegment(entry.name);
|
|
54
97
|
shortCounts.set(short, (shortCounts.get(short) ?? 0) + 1);
|
|
55
98
|
}
|
|
@@ -73,9 +116,17 @@ async function handleListExecutables(command, options) {
|
|
|
73
116
|
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
|
|
74
117
|
return EXIT.ok;
|
|
75
118
|
}
|
|
119
|
+
const contextLines = libraryLines();
|
|
120
|
+
if (described.length === 0) {
|
|
121
|
+
contextLines.push(`Available domains: ${[...new Set(available.map(actionDomain))].sort().join(", ") || "none"}`);
|
|
122
|
+
contextLines.push(`Available sources: ${[...new Set(available.map((entry) => entry.source))].sort().join(", ") || "none"}`);
|
|
123
|
+
}
|
|
76
124
|
console.log(renderHumanActionCatalog(described, {
|
|
77
125
|
title: `invocable actions (${adapter})`,
|
|
78
126
|
guidance: "Use: mm-harness call <name> \xB7 short names shown first where unambiguous",
|
|
127
|
+
sort,
|
|
128
|
+
domainStyles: stylesFor(available.map(actionDomain)),
|
|
129
|
+
contextLines,
|
|
79
130
|
displayName: (entry) => {
|
|
80
131
|
const short = shortByName.get(entry.name);
|
|
81
132
|
return short ? `${short} (${entry.name})` : `${entry.name} (full name only \u2014 ambiguous short)`;
|
|
@@ -345,16 +345,21 @@ function summarizeDescribedAction(entry) {
|
|
|
345
345
|
const { schema: _schema, examples: _examples, ...summary } = entry;
|
|
346
346
|
return summary;
|
|
347
347
|
}
|
|
348
|
+
function actionDomain(entry) {
|
|
349
|
+
return entry.category.replaceAll("_", "-");
|
|
350
|
+
}
|
|
348
351
|
function renderHumanActionCatalog(actions, options) {
|
|
349
352
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
350
353
|
const groups = /* @__PURE__ */ new Map();
|
|
351
354
|
for (const entry of actions) {
|
|
352
|
-
const group = actionDomainGroup(entry);
|
|
355
|
+
const group = options.sort === "name" ? "" : options.sort === "library" ? `Library: ${entry.source}` : options.sort === "domain" ? actionDomain(entry) : actionDomainGroup(entry);
|
|
353
356
|
groups.set(group, [...groups.get(group) ?? [], entry]);
|
|
354
357
|
}
|
|
355
|
-
const lines = [out("bold", options.title), out("comment", options.guidance)];
|
|
358
|
+
const lines = [out("bold", options.title), ...options.contextLines ?? [], out("comment", options.guidance)];
|
|
359
|
+
if (actions.length === 0) lines.push("No callable actions match.");
|
|
356
360
|
for (const [group, entries] of groups) {
|
|
357
|
-
|
|
361
|
+
const groupStyle = options.sort === "domain" ? options.domainStyles?.get(group) ?? "accent" : group === "official" ? "label" : "accent";
|
|
362
|
+
if (group) lines.push("", `${out(groupStyle, group)} ${out("dim", `(${entries.length})`)}`);
|
|
358
363
|
for (const entry of entries) {
|
|
359
364
|
const fieldNames = humanFieldNames(entry);
|
|
360
365
|
const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
|
|
@@ -362,7 +367,8 @@ function renderHumanActionCatalog(actions, options) {
|
|
|
362
367
|
const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
|
|
363
368
|
const source = ` ${out("dim", `[${entry.source}]`)}`;
|
|
364
369
|
const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
|
|
365
|
-
|
|
370
|
+
const nameStyle = options.domainStyles?.get(actionDomain(entry)) ?? "cmd";
|
|
371
|
+
lines.push(` ${out(nameStyle, options.displayName?.(entry) ?? entry.name)}${source}${fields}${cases}${risk}${description}`);
|
|
366
372
|
}
|
|
367
373
|
}
|
|
368
374
|
return lines.join("\n");
|
|
@@ -585,6 +591,7 @@ function levenshtein(left, right) {
|
|
|
585
591
|
}
|
|
586
592
|
export {
|
|
587
593
|
ACTION_MATRIX_ADAPTERS,
|
|
594
|
+
actionDomain,
|
|
588
595
|
actionExampleCommand,
|
|
589
596
|
actionLibraryContextArgs,
|
|
590
597
|
describeManifestActions,
|
package/dist/commands/prepare.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { acquireCheckoutLock, trackCheckoutChild } from "../checkout-lock.js";
|
|
2
|
+
import { recordCommandStage } from "../command-journal.js";
|
|
1
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { OperationOutputTail } from "@farmslot/recipe-harness/runtime/operation";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import {
|
|
@@ -168,18 +171,19 @@ function spawnStep(executable, args, target) {
|
|
|
168
171
|
// stream a recorded readiness step captures.
|
|
169
172
|
env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
|
|
170
173
|
});
|
|
174
|
+
if (child.pid) trackCheckoutChild(target, child.pid);
|
|
171
175
|
let stdout = "";
|
|
172
|
-
|
|
176
|
+
const stderr = new OperationOutputTail();
|
|
173
177
|
child.stdout?.setEncoding("utf8");
|
|
174
178
|
child.stderr?.setEncoding("utf8");
|
|
175
179
|
child.stdout?.on("data", (chunk) => {
|
|
176
180
|
stdout += chunk;
|
|
177
181
|
});
|
|
178
182
|
child.stderr?.on("data", (chunk) => {
|
|
179
|
-
stderr
|
|
183
|
+
stderr.append(chunk);
|
|
180
184
|
});
|
|
181
|
-
child.on("error", (error) => resolve({ error, status: null, stdout, stderr }));
|
|
182
|
-
child.on("close", (status) => resolve({ status, stdout, stderr }));
|
|
185
|
+
child.on("error", (error) => resolve({ error, status: null, stdout, stderr: stderr.toString() }));
|
|
186
|
+
child.on("close", (status) => resolve({ status, stdout, stderr: stderr.toString() }));
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
async function runStep(executable, target, artifactsDir, id, args, { json, onSpawn }) {
|
|
@@ -305,6 +309,22 @@ function resolvePlatform(requested, target) {
|
|
|
305
309
|
);
|
|
306
310
|
}
|
|
307
311
|
async function handlePrepare(argv) {
|
|
312
|
+
const { options } = parseArgs(argv, "prepare");
|
|
313
|
+
const target = targetPath(options);
|
|
314
|
+
if (!fs.existsSync(target)) return handlePrepareLocked(argv);
|
|
315
|
+
const lock = acquireCheckoutLock(target, "prepare");
|
|
316
|
+
if ("message" in lock) {
|
|
317
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify({ status: "fail", error: { code: "SANDBOX_BUSY", message: lock.message }, exitCode: EXIT.runtime }));
|
|
318
|
+
else console.error(`${lock.message} Next: inspect the owning operation with mm-harness status --watch`);
|
|
319
|
+
return EXIT.runtime;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return await handlePrepareLocked(argv);
|
|
323
|
+
} finally {
|
|
324
|
+
lock.release();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function handlePrepareLocked(argv) {
|
|
308
328
|
const { options } = parseArgs(argv, "prepare");
|
|
309
329
|
const json = optionFlag(options, "json");
|
|
310
330
|
const target = targetPath(options);
|
|
@@ -372,6 +392,7 @@ async function handlePrepare(argv) {
|
|
|
372
392
|
const step = await runStep(executable, target, artifactsDir, id, args, {
|
|
373
393
|
json,
|
|
374
394
|
onSpawn: (stepStartedAt) => {
|
|
395
|
+
recordCommandStage(id);
|
|
375
396
|
activeStep = { id, startedAt: new Date(stepStartedAt).toISOString() };
|
|
376
397
|
writeProgress();
|
|
377
398
|
view?.start(id, stepStartedAt);
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
importRecipeHarness,
|
|
22
22
|
importRecipeProtocol,
|
|
23
|
-
runnerDir
|
|
23
|
+
runnerDir,
|
|
24
|
+
recipeRuntimePath
|
|
24
25
|
} from "../paths.js";
|
|
25
26
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
26
27
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
@@ -277,7 +278,11 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
277
278
|
recipeDocument: recipeDocument ?? recipe,
|
|
278
279
|
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
|
|
279
280
|
...librarySources ? { librarySources } : {},
|
|
280
|
-
excludedProductRoots: [
|
|
281
|
+
excludedProductRoots: [
|
|
282
|
+
absoluteArtifactsDir,
|
|
283
|
+
recipeRuntimePath(projectRoot, "operations"),
|
|
284
|
+
recipeRuntimePath(projectRoot, "lock-members")
|
|
285
|
+
],
|
|
281
286
|
helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot)
|
|
282
287
|
};
|
|
283
288
|
const startProvenance = await captureExecutionProvenance(
|
|
@@ -416,6 +421,7 @@ function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
|
416
421
|
function recipeProcessEnvironment() {
|
|
417
422
|
const env = { ...process.env };
|
|
418
423
|
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
424
|
+
delete env.MM_HARNESS_OPERATION_ID;
|
|
419
425
|
return env;
|
|
420
426
|
}
|
|
421
427
|
function executionLibrarySources(sources, invocationTrust) {
|
package/dist/commands/run.js
CHANGED
|
@@ -147,6 +147,9 @@ async function handleRun(parsed) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
async function handleRunInner({ positional, options }, stream) {
|
|
150
|
+
if (!optionFlag(options, "list") && ["domain", "source", "sort"].some((name) => optionString(options, name) !== void 0)) {
|
|
151
|
+
throw usageError("--domain, --source, and --sort require run --list.");
|
|
152
|
+
}
|
|
150
153
|
if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
|
|
151
154
|
const message = "--describe cannot be combined with --list or --plan.";
|
|
152
155
|
const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkoutBusyOut, spawnScriptStreaming } from "./shared.js";
|
|
2
|
+
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import {
|
|
@@ -23,7 +24,18 @@ import {
|
|
|
23
24
|
usageError
|
|
24
25
|
} from "./parse-args.js";
|
|
25
26
|
import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
|
|
26
|
-
async function handleRuntimeLaunch(
|
|
27
|
+
async function handleRuntimeLaunch(args) {
|
|
28
|
+
const target = targetPath(args.options);
|
|
29
|
+
if (optionFlag(args.options, "help") || !fs.existsSync(target)) return handleRuntimeLaunchLocked(args);
|
|
30
|
+
const lock = acquireCheckoutLock(target, "runtime-launch");
|
|
31
|
+
if ("message" in lock) return checkoutBusyOut(optionFlag(args.options, "json"), "runtime-launch", lock.message, lock.path);
|
|
32
|
+
try {
|
|
33
|
+
return await handleRuntimeLaunchLocked(args);
|
|
34
|
+
} finally {
|
|
35
|
+
lock.release();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function handleRuntimeLaunchLocked({ options }) {
|
|
27
39
|
if (optionFlag(options, "help")) {
|
|
28
40
|
console.log(`mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
|
|
29
41
|
|
|
@@ -186,11 +198,11 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
186
198
|
cdpPort
|
|
187
199
|
}));
|
|
188
200
|
}
|
|
189
|
-
const result =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
const result = await spawnScriptStreaming(command[0], command.slice(1), target, {
|
|
202
|
+
forward: jsonMode ? "stderr" : "original",
|
|
203
|
+
stdin: jsonMode ? "ignore" : "inherit",
|
|
204
|
+
capture: false,
|
|
205
|
+
stage: mode
|
|
194
206
|
});
|
|
195
207
|
const summaryPath = path.join(artifactsDir, "summary.json");
|
|
196
208
|
const summary = readJsonIfExists(summaryPath);
|