@deeeed/metamask-harness 0.16.0 → 0.17.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/CHANGELOG.md +44 -0
- package/README.md +90 -137
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/console-tail.mjs +51 -24
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +5 -9
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/stamp-runtime-title.cjs +49 -0
- package/adapters/extension/start-watch.sh +16 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +105 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/manifest.json +46 -38
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +88 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +4 -61
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +90 -36
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -5
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/adapters/shared/tmux-viewer.sh +0 -0
- package/dist/adapters/extension/console-capture.js +84 -0
- package/dist/adapters/extension/runtime-decision.js +14 -2
- package/dist/adapters/extension/surface.js +3 -2
- package/dist/adapters/mobile/prepare.js +22 -7
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +0 -2
- package/dist/adapters/slot-ports.js +8 -0
- package/dist/cli.js +5 -11
- package/dist/command-contract.js +441 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +43 -20
- package/dist/commands/check.js +9 -3
- package/dist/commands/debug.js +38 -31
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +19 -6
- package/dist/commands/fixtures.js +19 -11
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/index.js +166 -59
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +114 -29
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +27 -42
- package/dist/commands/run-report.js +1 -1
- package/dist/commands/run.js +243 -41
- package/dist/commands/shared.js +11 -1
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +7 -2
- package/dist/doctor.js +1 -4
- package/dist/harness.js +19 -125
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +6 -20
- package/dist/mm-harness-cli.js +111 -41
- package/dist/paths.js +2 -5
- package/dist/run-diagnostics.js +35 -40
- package/dist/runner.js +32 -1
- package/dist/runtime-context.js +26 -8
- package/docs/CHEATSHEET.md +39 -52
- package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/UX-PRINCIPLES.md +2 -0
- package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
- package/docs/architecture.md +65 -375
- package/docs/live-adapter-contract.md +3 -1
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/manifests/mobile.action-manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/CLI-SPEC.md +0 -1102
- package/docs/MENTAL-MODEL.md +0 -295
package/dist/commands/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { walletFixturePath } from "../paths.js";
|
|
4
4
|
import { color } from "../cli-color.js";
|
|
5
5
|
import { recipeRunning } from "../heal-bounds.js";
|
|
6
|
-
import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
6
|
+
import { checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress } from "./shared.js";
|
|
7
7
|
import {
|
|
8
8
|
optionFlag,
|
|
9
9
|
optionString,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
resolveAdapter,
|
|
12
12
|
runtimeOptionsFromCli,
|
|
13
13
|
isRecord,
|
|
14
|
+
shellQuote,
|
|
15
|
+
targetPath,
|
|
14
16
|
usageError
|
|
15
17
|
} from "./parse-args.js";
|
|
16
18
|
import {
|
|
@@ -28,54 +30,119 @@ import { getAdapterSurface } from "../adapters/surface.js";
|
|
|
28
30
|
import { coreDependencyBlock } from "./core-readiness.js";
|
|
29
31
|
import { writeRunReport } from "./run-report.js";
|
|
30
32
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
import { JsonStreamWriter } from "../json-stream.js";
|
|
34
|
+
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
35
|
+
async function handleRun(parsed) {
|
|
36
|
+
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
37
|
+
const restoreStdout = stream.isolateStdout();
|
|
38
|
+
const target = targetPath(parsed.options);
|
|
39
|
+
try {
|
|
40
|
+
const exitCode = await handleRunInner(parsed, stream);
|
|
41
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
42
|
+
return exitCode;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
45
|
+
stream.error({
|
|
46
|
+
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
|
|
47
|
+
message: error instanceof Error ? error.message : String(error),
|
|
48
|
+
userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
|
|
49
|
+
});
|
|
50
|
+
stream.complete("fail", exitCode);
|
|
51
|
+
throw error;
|
|
52
|
+
} finally {
|
|
53
|
+
restoreStdout();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handleRunInner({ positional, options }, stream) {
|
|
57
|
+
if (optionFlag(options, "list")) {
|
|
58
|
+
if (stream.enabled) {
|
|
59
|
+
const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
|
|
60
|
+
stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
|
|
61
|
+
return EXIT.usage;
|
|
62
|
+
}
|
|
63
|
+
return handleListExecutables("run", options);
|
|
64
|
+
}
|
|
33
65
|
const targetRecipe = positional[0];
|
|
34
66
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
35
|
-
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
67
|
+
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options, stream);
|
|
36
68
|
const { adapter, target } = resolveAdapter(options);
|
|
37
69
|
const json = optionFlag(options, "json");
|
|
70
|
+
const jsonOutput = json && !stream.enabled;
|
|
71
|
+
const machine = json || stream.enabled;
|
|
72
|
+
stream.phase("resolve", { adapter, target, recipe: targetRecipe });
|
|
38
73
|
if (!fs.existsSync(target)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
74
|
+
const message = `target does not exist: ${target}`;
|
|
75
|
+
const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
|
|
76
|
+
if (stream.enabled) {
|
|
77
|
+
stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
|
|
78
|
+
return EXIT.usage;
|
|
79
|
+
}
|
|
80
|
+
return usageOut(jsonOutput, "run", message, userAction);
|
|
45
81
|
}
|
|
82
|
+
writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
|
|
46
83
|
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
47
84
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
48
85
|
if ("code" in dtResult) {
|
|
49
|
-
return emitRunUsageError(
|
|
86
|
+
return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
|
|
50
87
|
}
|
|
51
|
-
if (recipeRunning(target)) return emitRunRecipeRunning(
|
|
88
|
+
if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
|
|
52
89
|
if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
|
|
53
90
|
requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
54
91
|
}
|
|
92
|
+
stream.phase("validate");
|
|
55
93
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
56
94
|
if (validated.usageError) {
|
|
57
|
-
|
|
95
|
+
const userAction = runUsageRecovery(
|
|
96
|
+
validated.usageError.code,
|
|
97
|
+
validated.usageError.message,
|
|
98
|
+
adapter,
|
|
99
|
+
validated.recipeFile
|
|
100
|
+
);
|
|
101
|
+
return emitRunUsageError(
|
|
102
|
+
jsonOutput,
|
|
103
|
+
stream,
|
|
104
|
+
adapter,
|
|
105
|
+
validated.recipeFile,
|
|
106
|
+
validated.usageError.code,
|
|
107
|
+
validated.usageError.message,
|
|
108
|
+
userAction
|
|
109
|
+
);
|
|
58
110
|
}
|
|
59
111
|
if (validated.errorCount > 0) {
|
|
60
|
-
return emitRunValidationError(
|
|
112
|
+
return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
61
113
|
}
|
|
62
114
|
const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
63
115
|
if (depsBlock) {
|
|
64
|
-
return emitRunUsageError(
|
|
116
|
+
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
65
117
|
}
|
|
66
118
|
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
67
119
|
const lock = acquireCheckoutLock(target, "run");
|
|
68
|
-
if ("message" in lock)
|
|
120
|
+
if ("message" in lock) {
|
|
121
|
+
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
122
|
+
stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
|
|
123
|
+
return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
|
|
124
|
+
}
|
|
69
125
|
try {
|
|
70
|
-
const prepared = await prepareHeal(adapter, target, options,
|
|
71
|
-
|
|
126
|
+
const prepared = await prepareHeal(adapter, target, options, machine, {
|
|
127
|
+
onPhase: (phase, fields) => stream.phase(phase, fields)
|
|
128
|
+
});
|
|
129
|
+
if (typeof prepared === "number") {
|
|
130
|
+
stream.error({
|
|
131
|
+
code: "RUN_PREPARE_FAILED",
|
|
132
|
+
message: "runtime preparation failed; inspect stderr for the exact probe",
|
|
133
|
+
userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
|
|
134
|
+
});
|
|
135
|
+
return prepared;
|
|
136
|
+
}
|
|
72
137
|
const { state, heal } = prepared;
|
|
73
138
|
const librarySources = validated.librarySources;
|
|
74
139
|
const runtimeOptions = {
|
|
75
140
|
...runtimeOptionsFromCli(options),
|
|
76
141
|
...librarySources ? { librarySources } : {},
|
|
77
|
-
stdoutIsMachineContract:
|
|
142
|
+
stdoutIsMachineContract: machine,
|
|
143
|
+
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
78
144
|
};
|
|
145
|
+
stream.phase("execute");
|
|
79
146
|
const { result, violation } = await executeWithHealBounds(
|
|
80
147
|
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
81
148
|
// that only the resolver knows how to turn into a file.
|
|
@@ -84,12 +151,42 @@ async function handleRun({ positional, options }) {
|
|
|
84
151
|
target,
|
|
85
152
|
heal,
|
|
86
153
|
state,
|
|
87
|
-
() => recoverRunInfra(adapter, target,
|
|
154
|
+
() => recoverRunInfra(adapter, target, machine),
|
|
155
|
+
(code) => stream.phase("recover", { code })
|
|
88
156
|
);
|
|
89
|
-
|
|
157
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
158
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
159
|
+
if (violation !== null) {
|
|
160
|
+
const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
161
|
+
stream.error({
|
|
162
|
+
code: violation.code,
|
|
163
|
+
message: violation.message,
|
|
164
|
+
userAction,
|
|
165
|
+
originalError: violation.originalError ?? null
|
|
166
|
+
});
|
|
167
|
+
return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
|
|
168
|
+
}
|
|
90
169
|
const report = writeRunReport(result);
|
|
91
170
|
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
92
|
-
|
|
171
|
+
const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
|
|
172
|
+
if (stream.enabled) {
|
|
173
|
+
if (result.status === "fail") {
|
|
174
|
+
stream.error({
|
|
175
|
+
code: "RECIPE_EXECUTION_FAILED",
|
|
176
|
+
message: "recipe execution failed; inspect the persisted result and evidence paths",
|
|
177
|
+
userAction: failureUserAction
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
|
|
181
|
+
adapter,
|
|
182
|
+
reportPath: report.path,
|
|
183
|
+
summaryPath: result.summaryPath,
|
|
184
|
+
tracePath: result.tracePath,
|
|
185
|
+
artifactManifestPath: result.artifactManifestPath,
|
|
186
|
+
recovered: state.recovered,
|
|
187
|
+
mutations: state.mutations
|
|
188
|
+
});
|
|
189
|
+
} else if (json) {
|
|
93
190
|
console.log(
|
|
94
191
|
JSON.stringify(
|
|
95
192
|
{
|
|
@@ -101,7 +198,8 @@ async function handleRun({ positional, options }) {
|
|
|
101
198
|
recovered: state.recovered,
|
|
102
199
|
mutations: state.mutations,
|
|
103
200
|
reportPath: report.path,
|
|
104
|
-
result
|
|
201
|
+
result,
|
|
202
|
+
...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
|
|
105
203
|
},
|
|
106
204
|
null,
|
|
107
205
|
2
|
|
@@ -114,14 +212,61 @@ async function handleRun({ positional, options }) {
|
|
|
114
212
|
console.log(out("label", "summary:"));
|
|
115
213
|
for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
|
|
116
214
|
}
|
|
117
|
-
|
|
118
|
-
console.log(
|
|
215
|
+
const artifacts = runArtifactInventory(result.artifactManifestPath);
|
|
216
|
+
console.log(out("label", `artifacts (${artifacts.length}):`));
|
|
217
|
+
for (const artifact of artifacts) {
|
|
218
|
+
console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
|
|
219
|
+
}
|
|
220
|
+
if (result.status === "fail") {
|
|
221
|
+
console.error(` Next: ${failureUserAction}`);
|
|
222
|
+
}
|
|
223
|
+
console.log(out("label", "diagnostics:"));
|
|
224
|
+
const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
|
|
225
|
+
for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
|
|
226
|
+
console.log(` ${formatDiagnosticLine(line, out)}`);
|
|
227
|
+
}
|
|
119
228
|
}
|
|
120
229
|
return exitCode;
|
|
121
230
|
} finally {
|
|
122
231
|
lock.release();
|
|
123
232
|
}
|
|
124
233
|
}
|
|
234
|
+
function formatDiagnosticLine(line, out) {
|
|
235
|
+
const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
|
|
236
|
+
if (!match) return line;
|
|
237
|
+
const status = match[1];
|
|
238
|
+
const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
|
|
239
|
+
return `${out(style, status)}${match[2]}`;
|
|
240
|
+
}
|
|
241
|
+
function runArtifactInventory(manifestPathValue) {
|
|
242
|
+
const manifestPath = path.resolve(String(manifestPathValue));
|
|
243
|
+
const root = path.dirname(manifestPath);
|
|
244
|
+
let manifest;
|
|
245
|
+
try {
|
|
246
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
247
|
+
} catch {
|
|
248
|
+
return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
|
|
249
|
+
}
|
|
250
|
+
const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
251
|
+
const artifacts = [];
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
|
|
254
|
+
const absolutePath = path.resolve(root, entry.path);
|
|
255
|
+
const basename = path.basename(absolutePath);
|
|
256
|
+
const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
|
|
257
|
+
artifacts.push({ absolutePath, basename, label });
|
|
258
|
+
}
|
|
259
|
+
artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
|
|
260
|
+
const priority = /* @__PURE__ */ new Map([
|
|
261
|
+
["diagnostics.json", 0],
|
|
262
|
+
["report.md", 1],
|
|
263
|
+
["summary.json", 2],
|
|
264
|
+
["trace.json", 3],
|
|
265
|
+
["recipe.json", 4],
|
|
266
|
+
["artifact-manifest.json", 100]
|
|
267
|
+
]);
|
|
268
|
+
return artifacts.filter((artifact, index, all) => all.findIndex((candidate) => candidate.absolutePath === artifact.absolutePath) === index).sort((left, right) => (priority.get(left.basename) ?? 50) - (priority.get(right.basename) ?? 50));
|
|
269
|
+
}
|
|
125
270
|
function formatPreviewLine(line, out) {
|
|
126
271
|
const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
|
|
127
272
|
if (!match) return line;
|
|
@@ -193,12 +338,29 @@ function loadFlowCatalogs(librarySources) {
|
|
|
193
338
|
function actionUsesCoreController(action) {
|
|
194
339
|
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
195
340
|
}
|
|
196
|
-
async function handleRunPlan(recipeArg, options) {
|
|
341
|
+
async function handleRunPlan(recipeArg, options, stream) {
|
|
197
342
|
const json = optionFlag(options, "json");
|
|
343
|
+
const jsonOutput = json && !stream.enabled;
|
|
198
344
|
const { adapter, target } = resolveAdapter(options);
|
|
345
|
+
stream.phase("resolve", { adapter, target, recipe: recipeArg });
|
|
346
|
+
stream.phase("validate");
|
|
199
347
|
const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
|
|
200
348
|
if (validated.usageError) {
|
|
201
|
-
|
|
349
|
+
const userAction = runUsageRecovery(
|
|
350
|
+
validated.usageError.code,
|
|
351
|
+
validated.usageError.message,
|
|
352
|
+
adapter,
|
|
353
|
+
validated.recipeFile
|
|
354
|
+
);
|
|
355
|
+
return emitPlanUsageError(
|
|
356
|
+
jsonOutput,
|
|
357
|
+
stream,
|
|
358
|
+
adapter,
|
|
359
|
+
validated.recipeFile,
|
|
360
|
+
validated.usageError.code,
|
|
361
|
+
validated.usageError.message,
|
|
362
|
+
userAction
|
|
363
|
+
);
|
|
202
364
|
}
|
|
203
365
|
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
|
|
204
366
|
const status = errorCount === 0 ? "pass" : "fail";
|
|
@@ -252,9 +414,27 @@ async function handleRunPlan(recipeArg, options) {
|
|
|
252
414
|
detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
|
|
253
415
|
}
|
|
254
416
|
];
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
417
|
+
const payload = {
|
|
418
|
+
schemaVersion: 1,
|
|
419
|
+
command: "run",
|
|
420
|
+
mode: "plan",
|
|
421
|
+
status,
|
|
422
|
+
adapter,
|
|
423
|
+
recipe: recipeFile,
|
|
424
|
+
findings,
|
|
425
|
+
plan
|
|
426
|
+
};
|
|
427
|
+
if (status === "fail") {
|
|
428
|
+
payload.error = {
|
|
429
|
+
code: "RECIPE_VALIDATION_FAILED",
|
|
430
|
+
message: `recipe validation found ${errorCount} error(s)`,
|
|
431
|
+
userAction: runPlanProbe(adapter, recipeFile)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (stream.enabled) {
|
|
435
|
+
if (status === "fail") stream.error(payload.error);
|
|
436
|
+
stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
|
|
437
|
+
} else if (json) {
|
|
258
438
|
console.log(JSON.stringify(payload, null, 2));
|
|
259
439
|
} else {
|
|
260
440
|
console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
|
|
@@ -268,52 +448,61 @@ async function handleRunPlan(recipeArg, options) {
|
|
|
268
448
|
console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
269
449
|
}
|
|
270
450
|
}
|
|
451
|
+
if (status === "fail") console.error(` Next: ${runPlanProbe(adapter, recipeFile)}`);
|
|
271
452
|
}
|
|
272
453
|
return status === "pass" ? EXIT.ok : EXIT.validation;
|
|
273
454
|
}
|
|
274
|
-
function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
455
|
+
function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
|
|
456
|
+
stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
|
|
275
457
|
if (json) {
|
|
276
458
|
console.log(
|
|
277
459
|
JSON.stringify(
|
|
278
|
-
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
|
|
460
|
+
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message, userAction } },
|
|
279
461
|
null,
|
|
280
462
|
2
|
|
281
463
|
)
|
|
282
464
|
);
|
|
283
465
|
} else {
|
|
284
|
-
console.error(`\u2717 run --plan: ${message}
|
|
466
|
+
console.error(`\u2717 run --plan: ${message}
|
|
467
|
+
Next: ${userAction}`);
|
|
285
468
|
}
|
|
286
469
|
return EXIT.usage;
|
|
287
470
|
}
|
|
288
471
|
function runArgLooksLikeRecipeFile(value) {
|
|
289
472
|
return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
|
|
290
473
|
}
|
|
291
|
-
function emitRunRecipeRunning(json, detail) {
|
|
474
|
+
function emitRunRecipeRunning(json, stream, target, detail) {
|
|
292
475
|
const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
476
|
+
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
|
|
477
|
+
stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
|
|
293
478
|
if (json) {
|
|
294
|
-
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
|
|
479
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message, userAction } }, null, 2));
|
|
295
480
|
} else {
|
|
296
|
-
console.error(`\u2717 mm-harness: ${message}
|
|
481
|
+
console.error(`\u2717 mm-harness: ${message}
|
|
482
|
+
Next: ${userAction}`);
|
|
297
483
|
}
|
|
298
484
|
return EXIT.bounded;
|
|
299
485
|
}
|
|
300
|
-
function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
|
|
486
|
+
function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
|
|
487
|
+
stream.error({ code, message, userAction });
|
|
301
488
|
if (json) {
|
|
302
489
|
console.log(
|
|
303
490
|
JSON.stringify(
|
|
304
|
-
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message,
|
|
491
|
+
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
|
|
305
492
|
null,
|
|
306
493
|
2
|
|
307
494
|
)
|
|
308
495
|
);
|
|
309
496
|
} else {
|
|
310
497
|
console.error(`\u2717 run: ${message}`);
|
|
311
|
-
|
|
498
|
+
console.error(` Next: ${userAction}`);
|
|
312
499
|
}
|
|
313
500
|
return EXIT.usage;
|
|
314
501
|
}
|
|
315
|
-
function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
|
|
502
|
+
function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount) {
|
|
316
503
|
const message = `recipe validation found ${errorCount} error(s)`;
|
|
504
|
+
const userAction = runPlanProbe(adapter, recipeFile);
|
|
505
|
+
stream.error({ code: "RECIPE_VALIDATION_FAILED", message, userAction, findings });
|
|
317
506
|
if (json) {
|
|
318
507
|
console.log(
|
|
319
508
|
JSON.stringify(
|
|
@@ -327,7 +516,7 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
|
|
|
327
516
|
mutations: [],
|
|
328
517
|
recipe: recipeFile,
|
|
329
518
|
findings,
|
|
330
|
-
error: { code: "RECIPE_VALIDATION_FAILED", message }
|
|
519
|
+
error: { code: "RECIPE_VALIDATION_FAILED", message, userAction }
|
|
331
520
|
},
|
|
332
521
|
null,
|
|
333
522
|
2
|
|
@@ -338,9 +527,22 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
|
|
|
338
527
|
for (const finding of findings) {
|
|
339
528
|
if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
340
529
|
}
|
|
530
|
+
console.error(` Next: ${userAction}`);
|
|
341
531
|
}
|
|
342
532
|
return EXIT.validation;
|
|
343
533
|
}
|
|
534
|
+
function runPlanProbe(adapter, recipeFile) {
|
|
535
|
+
return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
|
|
536
|
+
}
|
|
537
|
+
function runUsageRecovery(code, message, adapter, recipeFile) {
|
|
538
|
+
const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
|
|
539
|
+
if (actionCommand) return actionCommand;
|
|
540
|
+
if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
|
|
541
|
+
if (code === "RECIPE_UNPARSEABLE") {
|
|
542
|
+
return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
|
|
543
|
+
}
|
|
544
|
+
return runPlanProbe(adapter, recipeFile);
|
|
545
|
+
}
|
|
344
546
|
export {
|
|
345
547
|
handleRun
|
|
346
548
|
};
|
package/dist/commands/shared.js
CHANGED
|
@@ -3,6 +3,15 @@ import path from "node:path";
|
|
|
3
3
|
import { detectAdapter } from "../harness.js";
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
|
|
5
5
|
const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
|
|
6
|
+
function writeInteractiveProgress(json, message, {
|
|
7
|
+
stdoutIsTTY = Boolean(process.stdout.isTTY),
|
|
8
|
+
stream = process.stderr
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (json || !stdoutIsTTY) return false;
|
|
11
|
+
stream.write(`${message}
|
|
12
|
+
`);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
6
15
|
function parseFlags(argv, booleans) {
|
|
7
16
|
const positional = [];
|
|
8
17
|
const options = {};
|
|
@@ -262,5 +271,6 @@ export {
|
|
|
262
271
|
spawnScriptStreaming,
|
|
263
272
|
str,
|
|
264
273
|
targetOf,
|
|
265
|
-
usageOut
|
|
274
|
+
usageOut,
|
|
275
|
+
writeInteractiveProgress
|
|
266
276
|
};
|
|
@@ -34,7 +34,7 @@ function parseBridgeEntries(raw) {
|
|
|
34
34
|
deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
|
|
35
35
|
platform: typeof e.platform === "string" ? e.platform : "",
|
|
36
36
|
// agenticPresent is only set when the bridge explicitly sends it (false = absent).
|
|
37
|
-
//
|
|
37
|
+
// An omitted value remains unknown.
|
|
38
38
|
...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
|
|
39
39
|
}));
|
|
40
40
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -31,7 +31,7 @@ async function handleStatus({ options }) {
|
|
|
31
31
|
if (adapter === "mobile") {
|
|
32
32
|
const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
|
|
33
33
|
if ("code" in dtResult) {
|
|
34
|
-
return usageOut(json, "status", dtResult.message,
|
|
34
|
+
return usageOut(json, "status", dtResult.message, dtResult.userAction);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
const next = surface.hints.relaunch;
|
package/dist/commands/stop.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
parseArgs,
|
|
6
6
|
optionFlag,
|
|
7
7
|
optionString,
|
|
8
|
-
resolveAdapter
|
|
8
|
+
resolveAdapter,
|
|
9
|
+
shellQuote
|
|
9
10
|
} from "./parse-args.js";
|
|
10
11
|
async function handleStop(argv) {
|
|
11
12
|
const { options } = parseArgs(argv, "stop");
|
|
@@ -22,6 +23,8 @@ async function handleStop(argv) {
|
|
|
22
23
|
if (stop.kind === "headless") {
|
|
23
24
|
return usageOut(json, "stop", stop.message, stop.userAction);
|
|
24
25
|
}
|
|
26
|
+
const next = adapter === "extension" ? `${surface.hints.launch} --target ${shellQuote(target)}` : void 0;
|
|
27
|
+
const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
|
|
25
28
|
if (json) {
|
|
26
29
|
console.log(
|
|
27
30
|
JSON.stringify(
|
|
@@ -33,7 +36,8 @@ async function handleStop(argv) {
|
|
|
33
36
|
status: stop.status === 0 ? "pass" : "fail",
|
|
34
37
|
...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
|
|
35
38
|
exitCode: stop.status,
|
|
36
|
-
...stop.output ? { output: stop.output } : {}
|
|
39
|
+
...stop.output ? { output: stop.output } : {},
|
|
40
|
+
...stop.status === 0 ? next ? { next } : {} : { error: { code: "DEV_SERVER_STOP_FAILED", message: stop.summary, userAction } }
|
|
37
41
|
},
|
|
38
42
|
null,
|
|
39
43
|
2
|
|
@@ -43,6 +47,7 @@ async function handleStop(argv) {
|
|
|
43
47
|
if (stop.output) process.stderr.write(`${stop.output}
|
|
44
48
|
`);
|
|
45
49
|
console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
|
|
50
|
+
if (stop.status !== 0 || next) console.error(` Next: ${stop.status === 0 ? next : userAction}`);
|
|
46
51
|
}
|
|
47
52
|
return stop.status;
|
|
48
53
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -93,10 +93,7 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
|
93
93
|
const RUNTIME_CONTEXT_FIELDS = [
|
|
94
94
|
{ key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "doctor --fix creates local identity; Farmslot may override", adapters: ["mobile", "extension", "core"] },
|
|
95
95
|
{ key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "resolved from the built extension", adapters: ["extension"] },
|
|
96
|
-
{ key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] }
|
|
97
|
-
{ key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
|
|
98
|
-
{ key: "runtimeStart.command", envVars: [], envVar: null, customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
|
|
99
|
-
{ key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] }
|
|
96
|
+
{ key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] }
|
|
100
97
|
];
|
|
101
98
|
function runtimeContextSummary(target, adapter) {
|
|
102
99
|
const contextPath = resolveRuntimeContextPath(target);
|