@deeeed/metamask-harness 0.17.5 → 0.18.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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +90 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +2 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- package/scripts/validate-human-outcomes.mjs +169 -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/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
package/dist/commands/doctor.js
CHANGED
|
@@ -4,9 +4,10 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { color } from "../cli-color.js";
|
|
6
6
|
import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDoctorCheckSummary } from "../doctor.js";
|
|
7
|
-
import { detectAdapter } from "../harness.js";
|
|
7
|
+
import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
|
|
8
8
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
9
9
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
10
|
+
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
10
11
|
import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
|
|
11
12
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
12
13
|
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
@@ -21,12 +22,13 @@ import {
|
|
|
21
22
|
import { farmslotReadyIndicator } from "./farmslot-ready.js";
|
|
22
23
|
import { ensureRuntimeContext } from "../runtime-context.js";
|
|
23
24
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
24
|
-
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
25
|
+
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
25
26
|
import {
|
|
26
27
|
actionManifestPathOption,
|
|
27
28
|
applyRuntimeDirOption,
|
|
28
29
|
optionFlag,
|
|
29
30
|
optionString,
|
|
31
|
+
shellQuote,
|
|
30
32
|
targetPath
|
|
31
33
|
} from "./parse-args.js";
|
|
32
34
|
function applyDoctorRuntimePorts(options) {
|
|
@@ -56,6 +58,14 @@ async function handleDoctor({ options }) {
|
|
|
56
58
|
return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
57
59
|
}
|
|
58
60
|
assertAdapter(adapter);
|
|
61
|
+
if (!fs.existsSync(target)) {
|
|
62
|
+
return usageOut(
|
|
63
|
+
json,
|
|
64
|
+
"doctor",
|
|
65
|
+
`target does not exist: ${target}`,
|
|
66
|
+
"pass --target <metamask-checkout> pointing to an existing checkout"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
59
69
|
if (printReady && json) {
|
|
60
70
|
return usageOut(
|
|
61
71
|
json,
|
|
@@ -72,12 +82,12 @@ async function handleDoctor({ options }) {
|
|
|
72
82
|
surface.resolveSlotPorts(target);
|
|
73
83
|
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
74
84
|
if ("code" in dtResult) {
|
|
75
|
-
return usageOut(json, "doctor", dtResult.message,
|
|
85
|
+
return usageOut(json, "doctor", dtResult.message, dtResult.userAction);
|
|
76
86
|
}
|
|
77
87
|
applyDoctorRuntimePorts(options);
|
|
78
88
|
const lock = acquireCheckoutLock(target, "doctor-fix");
|
|
79
89
|
if ("message" in lock) {
|
|
80
|
-
return
|
|
90
|
+
return checkoutBusyOut(json, "doctor", lock.message, lock.path);
|
|
81
91
|
}
|
|
82
92
|
let fixed;
|
|
83
93
|
let failed;
|
|
@@ -87,11 +97,36 @@ async function handleDoctor({ options }) {
|
|
|
87
97
|
lock.release();
|
|
88
98
|
}
|
|
89
99
|
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
90
|
-
|
|
100
|
+
let runtime2;
|
|
101
|
+
let runtimeProbeError2;
|
|
102
|
+
try {
|
|
103
|
+
runtime2 = await surface.runtimeStatus(target);
|
|
104
|
+
} catch (error2) {
|
|
105
|
+
runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
|
|
106
|
+
}
|
|
107
|
+
const ready = runtime2?.decision === "ready";
|
|
108
|
+
if (adapter === "extension" && runtime2?.reasonCode === "product-config-required") {
|
|
109
|
+
failed.push("extension-product-config");
|
|
110
|
+
}
|
|
111
|
+
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
|
|
112
|
+
if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
|
|
91
113
|
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
92
|
-
|
|
114
|
+
const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
115
|
+
const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
|
|
116
|
+
const error = status === "fail" ? {
|
|
117
|
+
code: "DOCTOR_FIX_INCOMPLETE",
|
|
118
|
+
message: "doctor --fix could not make every required check pass",
|
|
119
|
+
userAction: fixUserAction
|
|
120
|
+
} : void 0;
|
|
121
|
+
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready, runtime: runtime2, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
|
|
93
122
|
else {
|
|
94
|
-
console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
123
|
+
console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
124
|
+
if (runtime2) {
|
|
125
|
+
console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
|
|
126
|
+
for (const reason of runtime2.reasons) console.log(` ${reason}`);
|
|
127
|
+
} else if (runtimeProbeError2) {
|
|
128
|
+
console.log(`runtime probe failed: ${runtimeProbeError2}`);
|
|
129
|
+
}
|
|
95
130
|
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
96
131
|
}
|
|
97
132
|
return status === "pass" ? 0 : 1;
|
|
@@ -104,8 +139,9 @@ async function handleDoctor({ options }) {
|
|
|
104
139
|
surface.resolveSlotPorts(target);
|
|
105
140
|
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
106
141
|
if ("code" in dtResult) {
|
|
107
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
108
|
-
else console.error(`\u2717 doctor: ${dtResult.message}
|
|
142
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
|
|
143
|
+
else console.error(`\u2717 doctor: ${dtResult.message}
|
|
144
|
+
Next: ${dtResult.userAction}`);
|
|
109
145
|
return EXIT.usage;
|
|
110
146
|
}
|
|
111
147
|
applyDoctorRuntimePorts(options);
|
|
@@ -123,7 +159,7 @@ async function handleDoctor({ options }) {
|
|
|
123
159
|
requiredChecks
|
|
124
160
|
};
|
|
125
161
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
126
|
-
const capture =
|
|
162
|
+
const capture = captureHealth(adapter);
|
|
127
163
|
const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
|
|
128
164
|
const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
|
|
129
165
|
const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
|
|
@@ -142,11 +178,17 @@ async function handleDoctor({ options }) {
|
|
|
142
178
|
printReady
|
|
143
179
|
);
|
|
144
180
|
}
|
|
145
|
-
|
|
181
|
+
const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
182
|
+
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
183
|
+
const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
184
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
146
185
|
else {
|
|
147
186
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
148
187
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
149
188
|
console.log(`${out(doctorResult.status === "pass" ? "ok" : "err", doctorResult.status)} ${out("bold", adapter)} ${doctorResult.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
|
|
189
|
+
console.log(
|
|
190
|
+
`${out("label", "harness:")} ${doctorResult.runner.packageName}@${doctorResult.runner.version} ${out("accent", doctorResult.runner.installKind)} ${out("dim", `executable=${doctorResult.runner.executablePath}`)}`
|
|
191
|
+
);
|
|
150
192
|
if (runtime) {
|
|
151
193
|
const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
|
|
152
194
|
const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
|
|
@@ -173,18 +215,13 @@ async function handleDoctor({ options }) {
|
|
|
173
215
|
);
|
|
174
216
|
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
175
217
|
}
|
|
176
|
-
if (capture)
|
|
177
|
-
console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
|
|
178
|
-
if (capture.status !== "pass") {
|
|
179
|
-
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
180
|
-
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
218
|
+
if (capture) renderCaptureHealth(capture, out);
|
|
183
219
|
if (adapter === "mobile") {
|
|
184
220
|
if (deviceView) renderMobileDeviceList(deviceView, out);
|
|
185
221
|
if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
|
|
186
222
|
if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
|
|
187
223
|
}
|
|
224
|
+
if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
|
|
188
225
|
}
|
|
189
226
|
return doctorResult.status === "pass" ? 0 : 1;
|
|
190
227
|
}
|
|
@@ -207,15 +244,25 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
|
|
|
207
244
|
detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
|
|
208
245
|
};
|
|
209
246
|
}
|
|
210
|
-
function doctorFixNextActions(failed, fixture) {
|
|
247
|
+
function doctorFixNextActions(failed, fixture, adapter, target) {
|
|
211
248
|
const actions = [];
|
|
249
|
+
const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
|
|
250
|
+
const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
212
251
|
if (failed.includes("wallet-fixture")) {
|
|
213
252
|
const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
|
|
214
|
-
actions.push(`1)
|
|
215
|
-
actions.push(`2)
|
|
253
|
+
actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
|
|
254
|
+
actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
|
|
255
|
+
}
|
|
256
|
+
if (failed.includes("runtime-context")) {
|
|
257
|
+
actions.push(
|
|
258
|
+
`inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
|
|
262
|
+
if (adapter === "extension" && failed.includes("extension-product-config")) {
|
|
263
|
+
const block = extensionProductConfigBlock(target);
|
|
264
|
+
if (block) actions.push(block.userAction);
|
|
216
265
|
}
|
|
217
|
-
if (failed.includes("runtime-context")) actions.push("inspect temp/recipe/runtime/agentic-runtime.json, then retry mm-harness doctor --fix");
|
|
218
|
-
if (failed.includes("overlay")) actions.push("mm-harness install --target .");
|
|
219
266
|
return actions;
|
|
220
267
|
}
|
|
221
268
|
function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
@@ -223,7 +270,7 @@ function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
|
223
270
|
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
224
271
|
console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
225
272
|
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
226
|
-
console.error(` Next: ${getAdapterSurface(adapter).hints.
|
|
273
|
+
console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
|
|
227
274
|
}
|
|
228
275
|
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
|
|
229
276
|
const live = runtime?.decision === "ready";
|
|
@@ -241,7 +288,9 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
241
288
|
return EXIT.runtime;
|
|
242
289
|
}
|
|
243
290
|
if (json) {
|
|
244
|
-
|
|
291
|
+
const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
|
|
292
|
+
const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
|
|
293
|
+
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
|
|
245
294
|
return live ? EXIT.ok : EXIT.runtime;
|
|
246
295
|
}
|
|
247
296
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -252,6 +301,25 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
252
301
|
emitExpectLiveVerdict(adapter, target, runtime, "not-live");
|
|
253
302
|
return EXIT.runtime;
|
|
254
303
|
}
|
|
304
|
+
function captureHealth(adapter) {
|
|
305
|
+
if (adapter === "core") return null;
|
|
306
|
+
const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
|
|
307
|
+
const helper = captureHelperHealth();
|
|
308
|
+
if (!helper) {
|
|
309
|
+
return {
|
|
310
|
+
status: "fallback",
|
|
311
|
+
failing: [],
|
|
312
|
+
screenshots: { available: true, providers: fallback },
|
|
313
|
+
recording: { available: false, provider: null }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
status: helper.status,
|
|
318
|
+
failing: helper.failing,
|
|
319
|
+
screenshots: { available: true, providers: ["capture-helper", ...fallback] },
|
|
320
|
+
recording: { available: helper.status === "pass", provider: helper.status === "pass" ? "capture-helper" : null }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
255
323
|
function captureHelperHealth() {
|
|
256
324
|
if (process.platform !== "darwin") return null;
|
|
257
325
|
const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
@@ -264,6 +332,18 @@ function captureHelperHealth() {
|
|
|
264
332
|
return null;
|
|
265
333
|
}
|
|
266
334
|
}
|
|
335
|
+
function renderCaptureHealth(capture, out) {
|
|
336
|
+
const video = capture.recording.available ? "capture-helper" : "unavailable";
|
|
337
|
+
console.log(
|
|
338
|
+
`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", `(screenshots: ${capture.screenshots.providers.join(" \u2192 ")}; video: ${video})`)}`
|
|
339
|
+
);
|
|
340
|
+
if (capture.status === "warn") {
|
|
341
|
+
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
342
|
+
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
343
|
+
} else if (capture.status === "fallback") {
|
|
344
|
+
console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
267
347
|
function detectOrphanMetros(target, livePort) {
|
|
268
348
|
try {
|
|
269
349
|
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
@@ -381,6 +461,7 @@ function readTextSnapshot(file) {
|
|
|
381
461
|
}
|
|
382
462
|
}
|
|
383
463
|
export {
|
|
464
|
+
doctorFixNextActions,
|
|
384
465
|
ensureExtensionPerpsBuildFlag,
|
|
385
466
|
handleDoctor
|
|
386
467
|
};
|
|
@@ -5,21 +5,22 @@ import { runnerDir, walletFixturePath } from "../paths.js";
|
|
|
5
5
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
6
6
|
import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
|
|
7
7
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
8
|
+
import { colorHumanMessage } from "../cli-color.js";
|
|
8
9
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
9
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
10
|
+
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
10
11
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
11
|
-
const
|
|
12
|
-
async function handleFixtures(argv
|
|
12
|
+
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
13
|
+
async function handleFixtures(argv) {
|
|
13
14
|
const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
14
15
|
const json = flag(options, "json");
|
|
15
16
|
const target = targetOf(options);
|
|
16
|
-
if (!fs.existsSync(target)) return handleFixturesLocked(argv
|
|
17
|
+
if (!fs.existsSync(target)) return handleFixturesLocked(argv);
|
|
17
18
|
const lock = acquireCheckoutLock(target, "fixtures");
|
|
18
19
|
if ("message" in lock) {
|
|
19
|
-
return
|
|
20
|
+
return checkoutBusyOut(json, "fixtures", lock.message, lock.path);
|
|
20
21
|
}
|
|
21
22
|
try {
|
|
22
|
-
return await handleFixturesLocked(argv
|
|
23
|
+
return await handleFixturesLocked(argv);
|
|
23
24
|
} finally {
|
|
24
25
|
lock.release();
|
|
25
26
|
}
|
|
@@ -49,13 +50,21 @@ function ensureFixturePublicationParent(target) {
|
|
|
49
50
|
}
|
|
50
51
|
return fs.realpathSync(parent) === parent ? null : `wallet fixture runtime path escapes the checkout: ${parent}`;
|
|
51
52
|
}
|
|
52
|
-
async function handleFixturesLocked(argv
|
|
53
|
+
async function handleFixturesLocked(argv) {
|
|
53
54
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
54
55
|
const json = flag(options, "json");
|
|
55
56
|
const sub = positional[0];
|
|
56
57
|
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
57
58
|
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
58
59
|
}
|
|
60
|
+
if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
|
|
61
|
+
return usageOut(
|
|
62
|
+
json,
|
|
63
|
+
"fixtures",
|
|
64
|
+
"--dev, --from, and --force belong to fixtures init; they do not apply a fixture.",
|
|
65
|
+
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
59
68
|
const target = targetOf(options);
|
|
60
69
|
const adapter = resolveAdapter(options, target);
|
|
61
70
|
if (!adapter) {
|
|
@@ -65,7 +74,7 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
65
74
|
surface.resolveSlotPorts(target);
|
|
66
75
|
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
|
|
67
76
|
if ("code" in dtResult) {
|
|
68
|
-
return usageOut(json, "fixtures", dtResult.message,
|
|
77
|
+
return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
|
|
69
78
|
}
|
|
70
79
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
71
80
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
@@ -96,6 +105,17 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
96
105
|
return exitCode;
|
|
97
106
|
}
|
|
98
107
|
const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
108
|
+
if (!fs.existsSync(fixturePath)) {
|
|
109
|
+
return usageOut(
|
|
110
|
+
json,
|
|
111
|
+
"fixtures",
|
|
112
|
+
`no wallet fixture at ${fixturePath}.`,
|
|
113
|
+
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (adapter === "extension") {
|
|
117
|
+
return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
|
|
118
|
+
}
|
|
99
119
|
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
100
120
|
`);
|
|
101
121
|
let status;
|
|
@@ -105,8 +125,8 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
105
125
|
const previousAppRoot = process.env.APP_ROOT;
|
|
106
126
|
process.env.APP_ROOT = target;
|
|
107
127
|
try {
|
|
108
|
-
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ??
|
|
109
|
-
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout :
|
|
128
|
+
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
|
|
129
|
+
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
|
|
110
130
|
mobileSetupResult = await spawnScriptStreaming(
|
|
111
131
|
setupWalletSh,
|
|
112
132
|
["--fixture", fixturePath],
|
|
@@ -128,29 +148,8 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
128
148
|
if (!json && status === "fail") {
|
|
129
149
|
console.error(` Next: ${retryHint}`);
|
|
130
150
|
}
|
|
131
|
-
} else {
|
|
132
|
-
if (!fs.existsSync(fixturePath)) {
|
|
133
|
-
return usageOut(
|
|
134
|
-
json,
|
|
135
|
-
"fixtures",
|
|
136
|
-
`no wallet fixture at ${fixturePath}.`,
|
|
137
|
-
"create it (or pass --fixture <path>), then re-run: mm-harness fixtures set"
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
141
|
-
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
142
|
-
try {
|
|
143
|
-
const result = await deps.runOneNode("extension", "metamask.wallet.setup", {}, target, str(options, "actionManifest"));
|
|
144
|
-
status = result.status;
|
|
145
|
-
} finally {
|
|
146
|
-
if (previousFixtureEnv === void 0) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
147
|
-
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
148
|
-
}
|
|
149
|
-
if (!json && status === "fail") {
|
|
150
|
-
console.error(` Next: ${retryHint}`);
|
|
151
|
-
}
|
|
152
151
|
}
|
|
153
|
-
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}
|
|
152
|
+
const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
|
|
154
153
|
if (json) {
|
|
155
154
|
console.log(
|
|
156
155
|
JSON.stringify(
|
|
@@ -174,11 +173,68 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
174
173
|
2
|
|
175
174
|
)
|
|
176
175
|
);
|
|
177
|
-
} else {
|
|
176
|
+
} else if (status === "pass") {
|
|
178
177
|
console.error(teaching);
|
|
179
178
|
}
|
|
180
179
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
181
180
|
}
|
|
181
|
+
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
|
|
182
|
+
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
183
|
+
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
184
|
+
if (!fs.existsSync(distManifest)) {
|
|
185
|
+
const message2 = `Extension fixture needs an existing dist/chrome build; no manifest was found at ${distManifest}.`;
|
|
186
|
+
if (json) {
|
|
187
|
+
console.log(JSON.stringify({
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
command: "fixtures",
|
|
190
|
+
action: "set",
|
|
191
|
+
adapter: "extension",
|
|
192
|
+
fixture: fixturePath,
|
|
193
|
+
canonicalFixture,
|
|
194
|
+
status: "fail",
|
|
195
|
+
exitCode: EXIT.runtime,
|
|
196
|
+
error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
|
|
197
|
+
}, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
console.error(`\u2717 mm-harness fixtures set: ${message2}
|
|
200
|
+
Next: ${userAction}`);
|
|
201
|
+
}
|
|
202
|
+
return EXIT.runtime;
|
|
203
|
+
}
|
|
204
|
+
process.stderr.write(`${colorHumanMessage("\u2192 fixtures set extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026")}
|
|
205
|
+
`);
|
|
206
|
+
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
207
|
+
const args = ["--target", target, "--launch-existing-dist"];
|
|
208
|
+
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
209
|
+
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
210
|
+
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
211
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
212
|
+
});
|
|
213
|
+
const status = result.status === 0 ? "pass" : "fail";
|
|
214
|
+
const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
215
|
+
const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
|
|
216
|
+
if (json) {
|
|
217
|
+
console.log(JSON.stringify({
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
command: "fixtures",
|
|
220
|
+
action: "set",
|
|
221
|
+
adapter: "extension",
|
|
222
|
+
fixture: fixturePath,
|
|
223
|
+
canonicalFixture,
|
|
224
|
+
status,
|
|
225
|
+
exitCode,
|
|
226
|
+
profileReset: status === "pass",
|
|
227
|
+
message,
|
|
228
|
+
error: status === "fail" ? { code: "EXTENSION_FIXTURE_APPLY_FAILED", message, userAction } : null
|
|
229
|
+
}, null, 2));
|
|
230
|
+
} else if (status === "pass") {
|
|
231
|
+
console.error(message);
|
|
232
|
+
} else {
|
|
233
|
+
console.error(`\u2717 mm-harness fixtures set: ${message}
|
|
234
|
+
Next: ${userAction}`);
|
|
235
|
+
}
|
|
236
|
+
return exitCode;
|
|
237
|
+
}
|
|
182
238
|
const EXT_FIXTURE_LEAF = "adapters/extension/wallet-fixture-state.cjs";
|
|
183
239
|
function emitLeafMissing(json, action, fields, leaf) {
|
|
184
240
|
const message = `leaf could not start: ${path.basename(leaf)} (ENOENT).`;
|
|
@@ -298,7 +354,9 @@ async function fixturesFinalize(adapter, target, options, json) {
|
|
|
298
354
|
if (outPath) leafArgs.push("--out", outPath);
|
|
299
355
|
process.stderr.write(`\u2192 fixtures finalize extension \u2014 seeding account labels via CDP on port ${cdpPort} (can take ~30s)\u2026
|
|
300
356
|
`);
|
|
301
|
-
const result = await spawnScriptStreaming(process.execPath, leafArgs, target
|
|
357
|
+
const result = await spawnScriptStreaming(process.execPath, leafArgs, target, {
|
|
358
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
359
|
+
});
|
|
302
360
|
const status = result.status === 0 ? "pass" : "fail";
|
|
303
361
|
const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
304
362
|
const retry = `mm-harness fixtures finalize --fixture ${fixturePath} --state ${statePath} --cdp-port ${cdpPort} --extension-dir ${extensionDirPath}`;
|
package/dist/commands/flows.js
CHANGED
|
@@ -2,18 +2,18 @@ import path from "node:path";
|
|
|
2
2
|
import { color } from "../cli-color.js";
|
|
3
3
|
import { importRecipeHarnessCli } from "../paths.js";
|
|
4
4
|
import { EXIT } from "./shared.js";
|
|
5
|
-
import { usageError } from "./parse-args.js";
|
|
5
|
+
import { parseArgs, usageError } from "./parse-args.js";
|
|
6
6
|
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
7
|
-
const FLOWS_SUBCOMMANDS = ["list", "promote"];
|
|
8
|
-
function
|
|
7
|
+
const FLOWS_SUBCOMMANDS = ["list", "describe", "promote"];
|
|
8
|
+
function stripFrontOnlyFlags(argv) {
|
|
9
9
|
const out = [];
|
|
10
10
|
for (let i = 0; i < argv.length; i += 1) {
|
|
11
11
|
const arg = argv[i];
|
|
12
|
-
if (arg === "--target") {
|
|
12
|
+
if (arg === "--target" || arg === "--library") {
|
|
13
13
|
i += 1;
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
-
if (arg.startsWith("--target=")) continue;
|
|
16
|
+
if (arg.startsWith("--target=") || arg.startsWith("--library=")) continue;
|
|
17
17
|
out.push(arg);
|
|
18
18
|
}
|
|
19
19
|
return out;
|
|
@@ -30,14 +30,37 @@ function restoreEnv(key, value) {
|
|
|
30
30
|
if (value === void 0) delete process.env[key];
|
|
31
31
|
else process.env[key] = value;
|
|
32
32
|
}
|
|
33
|
+
function libraryEntries(argv) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
+
const argument = argv[index];
|
|
37
|
+
if (argument === "--library") {
|
|
38
|
+
const value = argv[index + 1];
|
|
39
|
+
if (value !== void 0) entries.push(value);
|
|
40
|
+
index += 1;
|
|
41
|
+
} else if (argument.startsWith("--library=")) {
|
|
42
|
+
entries.push(argument.slice("--library=".length));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
33
47
|
async function handleFlows(argv) {
|
|
34
|
-
const
|
|
48
|
+
const { positional } = parseArgs(argv);
|
|
49
|
+
const subcommand = positional[0];
|
|
50
|
+
if (subcommand === "describe" && !positional[1]) {
|
|
51
|
+
throw usageError("flows describe requires <ref>. Next: mm-harness flows list");
|
|
52
|
+
}
|
|
53
|
+
if (subcommand !== "describe" && positional[1]) {
|
|
54
|
+
throw usageError(`flows ${subcommand ?? "list"} does not accept positional ${positional[1]}.`);
|
|
55
|
+
}
|
|
56
|
+
const configuredLibraries = libraryEntries(argv);
|
|
57
|
+
const sources = await resolveMetaMaskLibrarySources(configuredLibraries);
|
|
35
58
|
if (!sources) {
|
|
36
59
|
throw usageError(
|
|
37
60
|
"flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; flows resolve personal > team > canonical). Next: update the dependency and run yarn install."
|
|
38
61
|
);
|
|
39
62
|
}
|
|
40
|
-
const forwarded =
|
|
63
|
+
const forwarded = stripFrontOnlyFlags(argv);
|
|
41
64
|
const jsonMode = forwarded.includes("--json");
|
|
42
65
|
if (!jsonMode) {
|
|
43
66
|
console.error(color("label", "flows resolve by precedence (highest wins; local shadows canonical):"));
|
|
@@ -46,16 +69,22 @@ async function handleFlows(argv) {
|
|
|
46
69
|
console.error(` ${index + 1}. ${color("cmd", name)} ${color("dim", `[${flowSourceTier(source, index, sources.length)}]`)}`);
|
|
47
70
|
});
|
|
48
71
|
}
|
|
49
|
-
const
|
|
72
|
+
const resolvedSubcommand = subcommand && FLOWS_SUBCOMMANDS.includes(subcommand) ? subcommand : "list";
|
|
73
|
+
const forwardedSubcommandIndex = forwarded.indexOf(resolvedSubcommand);
|
|
74
|
+
const delegatedArgs = forwardedSubcommandIndex === -1 ? forwarded : forwarded.filter((_, index) => index !== forwardedSubcommandIndex);
|
|
75
|
+
const withSubcommand = [resolvedSubcommand, ...delegatedArgs];
|
|
50
76
|
const { runRecipeHarnessCli } = await importRecipeHarnessCli();
|
|
51
77
|
const previousLibraryPath = process.env.RECIPE_LIBRARY_PATH;
|
|
78
|
+
const previousExitCode = process.exitCode;
|
|
52
79
|
process.env.RECIPE_LIBRARY_PATH = serializeLibrarySources(sources);
|
|
80
|
+
process.exitCode = void 0;
|
|
53
81
|
try {
|
|
54
|
-
await runRecipeHarnessCli(["flows", ...withSubcommand]);
|
|
82
|
+
await runRecipeHarnessCli(["flows", ...withSubcommand], { commandName: "mm-harness" });
|
|
83
|
+
return typeof process.exitCode === "number" ? process.exitCode : EXIT.ok;
|
|
55
84
|
} finally {
|
|
56
85
|
restoreEnv("RECIPE_LIBRARY_PATH", previousLibraryPath);
|
|
86
|
+
process.exitCode = previousExitCode;
|
|
57
87
|
}
|
|
58
|
-
return EXIT.ok;
|
|
59
88
|
}
|
|
60
89
|
export {
|
|
61
90
|
handleFlows
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readCommandJournal } from "../command-journal.js";
|
|
2
|
+
import { EXIT } from "./shared.js";
|
|
3
|
+
import {
|
|
4
|
+
optionFlag,
|
|
5
|
+
optionString,
|
|
6
|
+
targetPath
|
|
7
|
+
} from "./parse-args.js";
|
|
8
|
+
async function handleLast({ options }) {
|
|
9
|
+
const target = targetPath(options);
|
|
10
|
+
const { file, record } = readCommandJournal(target, optionString(options, "runtimeDir"));
|
|
11
|
+
const json = optionFlag(options, "json");
|
|
12
|
+
if (!record) {
|
|
13
|
+
const message = `no resumability journal exists for ${target}`;
|
|
14
|
+
const userAction = "run a proof or runtime command in this checkout, then re-run mm-harness last --json";
|
|
15
|
+
if (json) {
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
command: "last",
|
|
19
|
+
target,
|
|
20
|
+
journalPath: file,
|
|
21
|
+
status: "fail",
|
|
22
|
+
exitCode: EXIT.runtime,
|
|
23
|
+
error: { code: "LAST_NOT_FOUND", message, userAction }
|
|
24
|
+
}, null, 2));
|
|
25
|
+
} else {
|
|
26
|
+
console.error(`\u2717 mm-harness last: ${message}
|
|
27
|
+
Next: ${userAction}`);
|
|
28
|
+
}
|
|
29
|
+
return EXIT.runtime;
|
|
30
|
+
}
|
|
31
|
+
if (json) {
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
schemaVersion: 1,
|
|
34
|
+
command: "last",
|
|
35
|
+
target,
|
|
36
|
+
journalPath: file,
|
|
37
|
+
status: "pass",
|
|
38
|
+
exitCode: EXIT.ok,
|
|
39
|
+
last: record
|
|
40
|
+
}, null, 2));
|
|
41
|
+
} else {
|
|
42
|
+
const end = record.finishedAt ?? "still running or interrupted";
|
|
43
|
+
console.log(`${record.verdict.toUpperCase()} ${record.command} (exit ${record.exitCode ?? "pending"})`);
|
|
44
|
+
console.log(`started: ${record.startedAt}`);
|
|
45
|
+
console.log(`finished: ${end}`);
|
|
46
|
+
if (record.evidencePaths.length > 0) console.log(`evidence: ${record.evidencePaths.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
return EXIT.ok;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
handleLast
|
|
52
|
+
};
|