@deeeed/metamask-harness 0.5.0 → 0.6.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -0,0 +1,217 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { walletFixturePath } from "../paths.js";
4
+ import { EXIT } from "./shared.js";
5
+ import {
6
+ adapterOption,
7
+ optionFlag,
8
+ optionString,
9
+ requiredOption,
10
+ resolveAdapter,
11
+ runtimeOptionsFromCli,
12
+ targetPath,
13
+ usageError
14
+ } from "./parse-args.js";
15
+ import {
16
+ countRecipeNodes,
17
+ emitHealViolation,
18
+ executeWithHealBounds,
19
+ prepareHeal,
20
+ resolveMetaMaskLibrarySources,
21
+ runRecipe,
22
+ validateRunRecipeStatic
23
+ } from "./run-engine.js";
24
+ async function handleRun({ positional, options }) {
25
+ const targetRecipe = positional[0];
26
+ if (!targetRecipe) throw usageError("run requires <recipe.json>.");
27
+ if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
28
+ const adapter = adapterOption(options);
29
+ const json = optionFlag(options, "json");
30
+ const target = targetPath(options);
31
+ const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
32
+ const prepared = await prepareHeal(adapter, target, options, json);
33
+ if (typeof prepared === "number") return prepared;
34
+ const { state, heal } = prepared;
35
+ const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
36
+ if (validated.usageError) {
37
+ return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
38
+ }
39
+ if (validated.errorCount > 0) {
40
+ return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
41
+ }
42
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
43
+ const runtimeOptions = {
44
+ ...runtimeOptionsFromCli(options),
45
+ ...librarySources ? { librarySources } : {},
46
+ stdoutIsMachineContract: json
47
+ };
48
+ const { result, violation } = await executeWithHealBounds(
49
+ () => runRecipe(adapter, targetRecipe, artifactsDir, target, optionString(options, "actionManifest"), runtimeOptions),
50
+ adapter,
51
+ target,
52
+ heal,
53
+ state
54
+ );
55
+ if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
56
+ const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
57
+ if (json) {
58
+ console.log(
59
+ JSON.stringify(
60
+ {
61
+ schemaVersion: 1,
62
+ command: "run",
63
+ adapter,
64
+ status: result.status,
65
+ exitCode,
66
+ recovered: state.recovered,
67
+ mutations: state.mutations,
68
+ result
69
+ },
70
+ null,
71
+ 2
72
+ )
73
+ );
74
+ } else {
75
+ console.log(`MetaMask recipe run: ${result.status}
76
+ Artifacts: ${result.artifactManifestPath}`);
77
+ }
78
+ return exitCode;
79
+ }
80
+ async function handleRunPlan(recipeArg, options) {
81
+ const json = optionFlag(options, "json");
82
+ const { adapter, target } = resolveAdapter(options);
83
+ const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
84
+ if (validated.usageError) {
85
+ return emitPlanUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
86
+ }
87
+ const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
88
+ const status = errorCount === 0 ? "pass" : "fail";
89
+ const nodeCount = countRecipeNodes(recipe);
90
+ const artifactsDir = optionString(options, "artifactsDir");
91
+ const plan = [
92
+ { step: "resolve.recipe", confidence: "static", status: "ok", detail: recipeFile },
93
+ { step: "resolve.adapter", confidence: "static", status: "ok", detail: adapter },
94
+ {
95
+ step: "resolve.artifactsDir",
96
+ confidence: "static",
97
+ status: "ok",
98
+ detail: artifactsDir ? path.resolve(artifactsDir) : "(resolved to the slot artifacts dir at run time)"
99
+ },
100
+ {
101
+ step: "validate.manifest",
102
+ confidence: "static",
103
+ status: manifestOk ? "ok" : "error",
104
+ detail: manifestOk ? "action manifest is well-formed" : "action manifest failed validation"
105
+ },
106
+ {
107
+ step: "validate.schema",
108
+ confidence: "static",
109
+ status: manifestOk && schemaValid ? "ok" : "error",
110
+ detail: "recipe document schema + action existence/platform vs the adapter manifest"
111
+ },
112
+ {
113
+ step: "fixture.file",
114
+ confidence: "static",
115
+ status: "ok",
116
+ detail: fs.existsSync(walletFixturePath(target)) ? `present: ${walletFixturePath(target)}` : `absent: ${walletFixturePath(target)} (fixtures set to seed)`
117
+ },
118
+ {
119
+ step: "overlay.ensure",
120
+ confidence: "conditional",
121
+ status: "planned",
122
+ detail: "would auto-ensure the runtime overlay if missing (install phase)"
123
+ },
124
+ ...adapter === "core" ? [] : [
125
+ {
126
+ step: "launch.app",
127
+ confidence: "conditional",
128
+ status: "planned",
129
+ detail: "would launch/attach the app + heal transport (Metro/Chrome/CDP) before executing"
130
+ }
131
+ ],
132
+ {
133
+ step: "execute.nodes",
134
+ confidence: "conditional",
135
+ status: "planned",
136
+ detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
137
+ }
138
+ ];
139
+ if (json) {
140
+ const payload = { schemaVersion: 1, command: "run", mode: "plan", status, adapter, recipe: recipeFile, findings, plan };
141
+ if (status === "fail") payload.error = { code: "RECIPE_VALIDATION_FAILED", message: `recipe validation found ${errorCount} error(s)` };
142
+ console.log(JSON.stringify(payload, null, 2));
143
+ } else {
144
+ console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
145
+ for (const item of plan) {
146
+ const mark = item.status === "error" ? "\u2717" : item.status === "ok" ? "\u2713" : "\xB7";
147
+ console.log(` ${mark} [${item.confidence}] ${item.step}: ${item.detail}`);
148
+ }
149
+ if (findings.length) {
150
+ console.log("findings:");
151
+ for (const finding of findings) {
152
+ console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
153
+ }
154
+ }
155
+ }
156
+ return status === "pass" ? EXIT.ok : EXIT.validation;
157
+ }
158
+ function emitPlanUsageError(json, adapter, recipeFile, code, message) {
159
+ if (json) {
160
+ console.log(
161
+ JSON.stringify(
162
+ { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
163
+ null,
164
+ 2
165
+ )
166
+ );
167
+ } else {
168
+ console.error(`\u2717 run --plan: ${message}`);
169
+ }
170
+ return EXIT.usage;
171
+ }
172
+ function emitRunUsageError(json, adapter, recipeFile, code, message) {
173
+ if (json) {
174
+ console.log(
175
+ JSON.stringify(
176
+ { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message } },
177
+ null,
178
+ 2
179
+ )
180
+ );
181
+ } else {
182
+ console.error(`\u2717 run: ${message}`);
183
+ }
184
+ return EXIT.usage;
185
+ }
186
+ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
187
+ const message = `recipe validation found ${errorCount} error(s)`;
188
+ if (json) {
189
+ console.log(
190
+ JSON.stringify(
191
+ {
192
+ schemaVersion: 1,
193
+ command: "run",
194
+ adapter,
195
+ status: "fail",
196
+ exitCode: EXIT.validation,
197
+ recovered: [],
198
+ mutations: [],
199
+ recipe: recipeFile,
200
+ findings,
201
+ error: { code: "RECIPE_VALIDATION_FAILED", message }
202
+ },
203
+ null,
204
+ 2
205
+ )
206
+ );
207
+ } else {
208
+ console.error(`\u2717 run: ${message}`);
209
+ for (const finding of findings) {
210
+ if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
211
+ }
212
+ }
213
+ return EXIT.validation;
214
+ }
215
+ export {
216
+ handleRun
217
+ };
@@ -0,0 +1,58 @@
1
+ import { decideExtensionReadiness } from "../adapters/extension/runtime-decision.js";
2
+ import { decideMobileReadiness } from "../adapters/mobile/runtime-decision.js";
3
+ import {
4
+ adapterOption,
5
+ applyRuntimeDirOption,
6
+ optionFlag,
7
+ optionString,
8
+ parsePort,
9
+ targetPath
10
+ } from "./parse-args.js";
11
+ async function handleRuntimeDecision({ options }) {
12
+ applyRuntimeDirOption(options);
13
+ const adapter = adapterOption(options);
14
+ const target = targetPath(options);
15
+ if (adapter === "mobile") {
16
+ const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
17
+ const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
18
+ const report2 = await decideMobileReadiness(target, {
19
+ watcherPort,
20
+ metroLog: optionString(options, "metroLog"),
21
+ platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
22
+ record: optionFlag(options, "record"),
23
+ preflightMode: optionString(options, "preflightMode")
24
+ });
25
+ if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
26
+ else console.log(`${report2.decision} ${report2.reasonCode} \u2014 ${report2.reasons[0] ?? ""}`);
27
+ return 0;
28
+ }
29
+ if (adapter !== "extension") {
30
+ const report2 = {
31
+ schemaVersion: 1,
32
+ adapter,
33
+ target,
34
+ decision: "unknown",
35
+ clean: false,
36
+ reasonCode: "adapter-unsupported",
37
+ reasons: [`runtime-decision applies to extension and mobile adapters, not ${adapter}.`],
38
+ checks: {},
39
+ actions: []
40
+ };
41
+ if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
42
+ else console.log(`unknown adapter-unsupported \u2014 ${report2.reasons[0]}`);
43
+ return 0;
44
+ }
45
+ const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
46
+ const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
47
+ const report = await decideExtensionReadiness(target, {
48
+ cdpPort,
49
+ watchLog: optionString(options, "watchLog"),
50
+ record: optionFlag(options, "record")
51
+ });
52
+ if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
53
+ else console.log(`${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}`);
54
+ return 0;
55
+ }
56
+ export {
57
+ handleRuntimeDecision
58
+ };
@@ -0,0 +1,25 @@
1
+ import {
2
+ adapterOption,
3
+ optionFlag,
4
+ optionString,
5
+ parsePort,
6
+ targetPath
7
+ } from "./parse-args.js";
8
+ async function handleRuntimeHealth({ options }) {
9
+ const adapter = adapterOption(options);
10
+ if (adapter !== "extension") throw new Error("runtime-health currently applies to the extension adapter.");
11
+ const target = targetPath(options);
12
+ const cdpPort = parsePort(
13
+ optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
14
+ "runtime-health requires --cdp-port <port>."
15
+ );
16
+ const { checkExtensionRuntimeHealth, formatHealthFailure } = await import("../adapters/extension/runtime.js");
17
+ const report = await checkExtensionRuntimeHealth(target, cdpPort);
18
+ if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
19
+ else if (report.status === "PASS") console.log(`PASS extension runtime cdp=${cdpPort} target=${report.targetUrl}`);
20
+ else console.error(formatHealthFailure(report, target));
21
+ return report.status === "PASS" ? 0 : 1;
22
+ }
23
+ export {
24
+ handleRuntimeHealth
25
+ };
@@ -0,0 +1,139 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { recipeHarnessPath } from "../paths.js";
5
+ import {
6
+ adapterOption,
7
+ isRecord,
8
+ optionFlag,
9
+ optionString,
10
+ parsePort,
11
+ shellQuote,
12
+ targetPath
13
+ } from "./parse-args.js";
14
+ async function handleRuntimeLaunch({ options }) {
15
+ const adapter = adapterOption(options);
16
+ const target = targetPath(options);
17
+ if (adapter !== "extension") throw new Error("runtime-launch currently applies to the extension adapter.");
18
+ const cdpPort = parsePort(
19
+ optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
20
+ "runtime-launch requires --cdp-port <port>."
21
+ );
22
+ const chromeUserDataDir = optionString(options, "chromeUserDataDir");
23
+ const artifactsDir = path.resolve(
24
+ optionString(options, "artifactsDir") ?? recipeHarnessPath(target, "extension", "runtime-launch", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, ""))
25
+ );
26
+ const startWatch = optionFlag(options, "startWatch");
27
+ const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
28
+ const command = [
29
+ "bash",
30
+ liveScript,
31
+ "--target",
32
+ target,
33
+ "--cdp-port",
34
+ String(cdpPort),
35
+ startWatch ? "--start-watch" : "--launch-existing-dist",
36
+ "--artifacts-dir",
37
+ artifactsDir
38
+ ];
39
+ if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
40
+ const remoteFlag = optionString(options, "remoteFlag");
41
+ if (remoteFlag) command.push("--remote-flag", remoteFlag);
42
+ if (!fs.existsSync(liveScript)) {
43
+ const report2 = runtimeLaunchReport("fail", {
44
+ adapter,
45
+ target,
46
+ cdpPort,
47
+ artifactsDir,
48
+ reason: "harness_live_script_missing",
49
+ fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
50
+ command
51
+ });
52
+ printRuntimeLaunchReport(report2, optionFlag(options, "json"));
53
+ return 1;
54
+ }
55
+ fs.mkdirSync(artifactsDir, { recursive: true });
56
+ const jsonMode = optionFlag(options, "json");
57
+ if (!jsonMode) {
58
+ console.error(`recipe: runtime-launch starting (cdp=${cdpPort}${startWatch ? ", clean webpack build" : ", existing dist"})`);
59
+ } else {
60
+ console.error(JSON.stringify({
61
+ schemaVersion: 1,
62
+ type: "progress",
63
+ command: "rebuild",
64
+ phase: "runtime-launch",
65
+ message: startWatch ? "clean webpack build starting" : "launching existing dist",
66
+ cdpPort
67
+ }));
68
+ }
69
+ const result = spawnSync(command[0], command.slice(1), {
70
+ cwd: target,
71
+ encoding: "utf8",
72
+ maxBuffer: 5 * 1024 * 1024,
73
+ stdio: jsonMode ? ["ignore", "pipe", "pipe"] : "inherit"
74
+ });
75
+ const summaryPath = path.join(artifactsDir, "summary.json");
76
+ const summary = readJsonIfExists(summaryPath);
77
+ if (result.status === 0 && isRecord(summary) && summary.status === "pass") {
78
+ const report2 = runtimeLaunchReport("pass", {
79
+ adapter,
80
+ target,
81
+ cdpPort,
82
+ artifactsDir,
83
+ summaryPath,
84
+ reason: "runtime_ready",
85
+ fix: "",
86
+ command
87
+ });
88
+ printRuntimeLaunchReport(report2, optionFlag(options, "json"));
89
+ return 0;
90
+ }
91
+ const launchLogPath = path.join(artifactsDir, "launch", "logs", "launch.log");
92
+ const report = runtimeLaunchReport("fail", {
93
+ adapter,
94
+ target,
95
+ cdpPort,
96
+ artifactsDir,
97
+ summaryPath: fs.existsSync(summaryPath) ? summaryPath : void 0,
98
+ launchLogPath: fs.existsSync(launchLogPath) ? launchLogPath : void 0,
99
+ reason: "runtime_launch_failed",
100
+ fix: `Read ${fs.existsSync(launchLogPath) ? launchLogPath : summaryPath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, startWatch)}`,
101
+ command,
102
+ exitCode: result.status ?? 1
103
+ });
104
+ printRuntimeLaunchReport(report, optionFlag(options, "json"));
105
+ return 1;
106
+ }
107
+ function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, startWatch = false) {
108
+ const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
109
+ const withMode = startWatch ? `${base} --start-watch` : base;
110
+ return chromeUserDataDir ? `${withMode} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : withMode;
111
+ }
112
+ function runtimeLaunchReport(status, fields) {
113
+ return {
114
+ schemaVersion: 1,
115
+ status,
116
+ ...fields
117
+ };
118
+ }
119
+ function printRuntimeLaunchReport(report, json) {
120
+ if (json) {
121
+ console.log(JSON.stringify(report, null, 2));
122
+ return;
123
+ }
124
+ const status = String(report.status).toUpperCase();
125
+ console.log(`${status} runtime-launch ${report.reason}`);
126
+ if (report.status === "fail") console.log(`Fix: ${report.fix}`);
127
+ console.log(`Artifacts: ${report.artifactsDir}`);
128
+ }
129
+ function readJsonIfExists(file) {
130
+ if (!fs.existsSync(file)) return null;
131
+ try {
132
+ return JSON.parse(fs.readFileSync(file, "utf8"));
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ export {
138
+ handleRuntimeLaunch
139
+ };
@@ -0,0 +1,52 @@
1
+ import { mkdtemp } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { loadActionManifest, validateManifest } from "../manifest.js";
5
+ import { recipePath, runnerDir } from "../paths.js";
6
+ import {
7
+ optionFlag,
8
+ optionString,
9
+ requiredOption
10
+ } from "./parse-args.js";
11
+ import { runRecipe } from "./run-engine.js";
12
+ async function handleSelfTest({ options }) {
13
+ const result = await runSelfTest(options);
14
+ if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
15
+ else console.log(`MetaMask runner self-test: ${result.status}
16
+ Artifacts: ${result.artifactsDir}`);
17
+ return result.status === "pass" ? 0 : 1;
18
+ }
19
+ async function runSelfTest(options) {
20
+ const root = optionString(options, "artifactsDir") ? path.resolve(requiredOption(options, "artifactsDir", "self-test artifacts dir missing.")) : await mkdtemp(path.join(os.tmpdir(), "mm-harness-runner-"));
21
+ const previousAutoHud = process.env.METAMASK_RECIPE_AUTO_HUD;
22
+ const runs = [];
23
+ try {
24
+ process.env.METAMASK_RECIPE_AUTO_HUD = "0";
25
+ for (const adapter of ["mobile", "extension"]) {
26
+ const manifest = loadActionManifest(adapter);
27
+ const manifestValidation = await validateManifest(manifest);
28
+ const smokeRecipe = recipePath(
29
+ adapter === "mobile" ? "smoke.mobile.recipe.json" : "smoke.extension.recipe.json"
30
+ );
31
+ const artifactsDir = path.join(root, adapter);
32
+ const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
33
+ skipExtensionRuntimePrepare: true
34
+ });
35
+ runs.push({ adapter, manifestValidation: manifestValidation.summary, artifactsDir, result });
36
+ }
37
+ } finally {
38
+ restoreEnv("METAMASK_RECIPE_AUTO_HUD", previousAutoHud);
39
+ }
40
+ return {
41
+ status: runs.every((run) => run.result.status === "pass") ? "pass" : "fail",
42
+ artifactsDir: root,
43
+ runs
44
+ };
45
+ }
46
+ function restoreEnv(key, value) {
47
+ if (value === void 0) delete process.env[key];
48
+ else process.env[key] = value;
49
+ }
50
+ export {
51
+ handleSelfTest
52
+ };
@@ -0,0 +1,52 @@
1
+ import { color } from "../cli-color.js";
2
+ import { getAdapterSurface } from "../adapters/surface.js";
3
+ import { usageOut } from "./shared.js";
4
+ import {
5
+ parseArgs,
6
+ optionFlag,
7
+ optionString,
8
+ resolveAdapter
9
+ } from "./parse-args.js";
10
+ async function handleStop(argv) {
11
+ const { options } = parseArgs(argv, "stop");
12
+ const json = optionFlag(options, "json");
13
+ const { adapter, target } = resolveAdapter(options);
14
+ const surface = getAdapterSurface(adapter);
15
+ const explicitPort = optionString(options, "port") ?? optionString(options, "watcherPort");
16
+ if (explicitPort) {
17
+ process.env.WATCHER_PORT = explicitPort;
18
+ process.env.METRO_PORT = explicitPort;
19
+ } else {
20
+ surface.resolveSlotPorts(target);
21
+ }
22
+ const stop = surface.devServer.stop(target);
23
+ if (stop.kind === "headless") {
24
+ return usageOut(json, "stop", stop.message, stop.userAction);
25
+ }
26
+ if (json) {
27
+ console.log(
28
+ JSON.stringify(
29
+ {
30
+ schemaVersion: 1,
31
+ command: "stop",
32
+ adapter,
33
+ target,
34
+ status: stop.status === 0 ? "pass" : "fail",
35
+ ...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
36
+ exitCode: stop.status,
37
+ ...stop.output ? { output: stop.output } : {}
38
+ },
39
+ null,
40
+ 2
41
+ )
42
+ );
43
+ } else {
44
+ if (stop.output) process.stderr.write(`${stop.output}
45
+ `);
46
+ console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
47
+ }
48
+ return stop.status;
49
+ }
50
+ export {
51
+ handleStop
52
+ };
package/dist/harness.js CHANGED
@@ -4,10 +4,10 @@ import path from "node:path";
4
4
  import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
5
5
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
6
6
  import { prepareMobile } from "./adapters/mobile/prepare.js";
7
- const HARNESS_ACTIONS = ["install", "provision", "verify", "cleanup", "live"];
7
+ const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
8
8
  const ADAPTERS = ["mobile", "extension", "core"];
9
9
  function isValidAdapterAction(adapter, action) {
10
- if (adapter === "core") return action === "install" || action === "provision" || action === "verify" || action === "cleanup";
10
+ if (adapter === "core") return action === "install" || action === "verify" || action === "cleanup";
11
11
  return true;
12
12
  }
13
13
  function harnessUsage() {
@@ -19,8 +19,6 @@ is auto-detected from the repo. Pass --platform only to override.
19
19
  Commands (one copy-pasteable example each):
20
20
  install Install the recipe harness runtime overlay into the checkout.
21
21
  mm-harness install
22
- provision Install the cached Runway mobile dev client (same path as install --runway).
23
- mm-harness provision runway ios --adapter mobile
24
22
  verify Check the harness/runtime is present and healthy (no app launch).
25
23
  mm-harness verify
26
24
  cleanup Remove the installed harness overlay and restore the checkout.
@@ -404,11 +402,8 @@ async function handleHarness(argv) {
404
402
  }
405
403
  const autoDetected = parsedAdapter === void 0;
406
404
  let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
407
- if (harnessAction === "provision") {
408
- return handleRunwayInstall(adapter, target, provisionRunwayForward(forward), json, "provision");
409
- }
410
405
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
411
- return handleRunwayInstall(adapter, target, forward, json, "install");
406
+ return handleRunwayInstall(adapter, target, forward, json);
412
407
  }
413
408
  if (adapter === "extension" && (harnessAction === "live" || harnessAction === "verify")) {
414
409
  forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
@@ -501,9 +496,9 @@ async function handleHarness(argv) {
501
496
  }
502
497
  return exitCode;
503
498
  }
504
- async function handleRunwayInstall(adapter, target, forward, json, action) {
499
+ async function handleRunwayInstall(adapter, target, forward, json) {
505
500
  const { getAdapterSurface } = await import("./adapters/surface.js");
506
- const rerunCommand = action === "provision" ? runwayProvisionRerunCommand(adapter, target, forward, json) : runwayInstallRerunCommand(adapter, target, forward, json);
501
+ const rerunCommand = runwayInstallRerunCommand(adapter, target, forward, json);
507
502
  const surface = getAdapterSurface(adapter);
508
503
  const runtimeDir = argValue(forward, "--runtime-dir");
509
504
  const watcherPort = argValue(forward, "--watcher-port");
@@ -533,26 +528,15 @@ async function handleRunwayInstall(adapter, target, forward, json, action) {
533
528
  if (result.resolveOnly) {
534
529
  console.error(`\u2713 resolved Runway app for ${adapter} ${result.platform ?? ""} run=${artifact?.runId ?? "unknown"} revision=${artifact?.revision ?? "unknown"} artifact=${artifact?.artifactName ?? "unknown"}`);
535
530
  } else {
536
- const action2 = result.skipped ? "already provisioned" : "installed Runway app";
537
- console.error(`\u2713 ${action2} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
531
+ const action = result.skipped ? "already provisioned" : "installed Runway app";
532
+ console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
538
533
  }
539
534
  } else {
540
- const label = action === "provision" ? "provision" : "install --runway";
541
- console.error(`\u2717 mm-harness ${label}: ${result.error?.message ?? "runway install failed"}
535
+ console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
542
536
  Next: ${result.error?.userAction ?? rerunCommand}`);
543
537
  }
544
538
  return result.exitCode;
545
539
  }
546
- function provisionRunwayForward(forward) {
547
- const normalized = [];
548
- let index = 0;
549
- if (forward[index] === "runway") index += 1;
550
- if (forward[index] && !forward[index].startsWith("-")) {
551
- if (!hasArg(forward, "--platform")) normalized.push("--platform", forward[index]);
552
- index += 1;
553
- }
554
- return [...normalized, ...forward.slice(index)];
555
- }
556
540
  function runwayInstallRerunCommand(adapter, target, forward, json) {
557
541
  const parts = ["mm-harness", "install", "--runway", "--adapter", adapter, "--target", shellQuote(target)];
558
542
  const valueFlags = [
@@ -578,30 +562,6 @@ function runwayInstallRerunCommand(adapter, target, forward, json) {
578
562
  if (json) parts.push("--json");
579
563
  return parts.join(" ");
580
564
  }
581
- function runwayProvisionRerunCommand(adapter, target, forward, json) {
582
- const parts = ["mm-harness", "provision", "runway", shellQuote(argValue(forward, "--platform") ?? "ios"), "--adapter", adapter, "--target", shellQuote(target)];
583
- const valueFlags = [
584
- "--branch",
585
- "--default-branch",
586
- "--run",
587
- "--cache-root",
588
- "--simulator",
589
- "--device",
590
- "--slot",
591
- "--watcher-port",
592
- "--runtime-dir",
593
- "--runtime",
594
- "--device-type"
595
- ];
596
- for (const flag of valueFlags) {
597
- const value = argValue(forward, flag);
598
- if (value) parts.push(flag, shellQuote(value));
599
- }
600
- if (hasArg(forward, "--force")) parts.push("--force");
601
- if (hasArg(forward, "--resolve-only")) parts.push("--resolve-only");
602
- if (json) parts.push("--json");
603
- return parts.join(" ");
604
- }
605
565
  function harnessSummary(action, adapter, target, status, exitCode, autoDetected, error) {
606
566
  return JSON.stringify({
607
567
  schemaVersion: 1,