@deeeed/metamask-harness 0.17.4 → 0.17.5

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 (114) hide show
  1. package/CHANGELOG.md +6 -103
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -55
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -58
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -107
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -43
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -1,4 +1,3 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  const DEFAULTS = {
@@ -25,53 +24,14 @@ function ensureMobilePerpsEnvironment(target) {
25
24
  (key) => settingForKey(current, key).status === "missing"
26
25
  );
27
26
  if (missing.length === 0) return [];
28
- const file = path.join(fs.realpathSync(target), current.file);
29
- const snapshot = readEnvironmentSnapshot(file);
30
- const before = snapshot?.text ?? "";
27
+ const file = path.join(target, current.file);
28
+ const before = current.fileExists ? fs.readFileSync(file, "utf8") : "";
31
29
  const separator = before.length > 0 && !before.endsWith("\n") ? "\n" : "";
32
30
  const additions = missing.map((key) => `export ${key}="${DEFAULTS[key]}"`).join("\n");
33
- const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
34
- let descriptor = null;
35
- try {
36
- descriptor = fs.openSync(
37
- temporary,
38
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
39
- snapshot?.mode ?? 384
40
- );
41
- fs.writeFileSync(descriptor, `${before}${separator}${additions}
31
+ fs.writeFileSync(file, `${before}${separator}${additions}
42
32
  `);
43
- if (snapshot) fs.fchmodSync(descriptor, snapshot.mode);
44
- fs.closeSync(descriptor);
45
- descriptor = null;
46
- fs.renameSync(temporary, file);
47
- } finally {
48
- if (descriptor !== null) fs.closeSync(descriptor);
49
- fs.rmSync(temporary, { force: true });
50
- }
51
33
  return missing;
52
34
  }
53
- function readEnvironmentSnapshot(file) {
54
- let descriptor;
55
- try {
56
- descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
57
- } catch (error) {
58
- const code = error.code;
59
- if (code === "ENOENT") return null;
60
- if (code === "ELOOP") {
61
- throw new Error(`.js.env must not be a symbolic link. Next: replace ${file} with a regular file.`);
62
- }
63
- throw error;
64
- }
65
- try {
66
- const stat = fs.fstatSync(descriptor);
67
- if (!stat.isFile()) {
68
- throw new Error(`.js.env must be a regular file. Next: replace ${file} with a regular file.`);
69
- }
70
- return { text: fs.readFileSync(descriptor, "utf8"), mode: stat.mode & 511 };
71
- } finally {
72
- fs.closeSync(descriptor);
73
- }
74
- }
75
35
  function readSetting(source, key) {
76
36
  const assignment = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=(.*)$`, "u");
77
37
  let value = null;
@@ -56,21 +56,14 @@ async function prepareMobile(target, opts = {}) {
56
56
  json,
57
57
  preflightMode
58
58
  );
59
- if (confirm.status === 0) return startMobileConsoleForwarder(target, platform, json);
59
+ if (confirm.status === 0) return { status: 0, output: "" };
60
60
  if (!json) {
61
61
  process.stderr.write(
62
62
  `launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
63
63
  `
64
64
  );
65
65
  }
66
- const launch = await dispatchActionSequence(
67
- launchActions(path.resolve(target)),
68
- target,
69
- platform,
70
- json,
71
- preflightMode
72
- );
73
- return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
66
+ return dispatchActionSequence(launchActions(path.resolve(target)), target, platform, json, preflightMode);
74
67
  }
75
68
  if (report.decision === "unknown") {
76
69
  const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
@@ -118,15 +111,7 @@ async function prepareMobile(target, opts = {}) {
118
111
  }
119
112
  }
120
113
  }
121
- return startMobileConsoleForwarder(target, platform, json);
122
- }
123
- function startMobileConsoleForwarder(target, platform, json) {
124
- return dispatchAction(
125
- { id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
126
- target,
127
- platform,
128
- json
129
- );
114
+ return { status: 0, output: "" };
130
115
  }
131
116
  async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
132
117
  const resolved = path.resolve(target);
@@ -162,10 +147,6 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
162
147
  const extra = action.argv ?? [];
163
148
  return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
164
149
  }
165
- case "start-console-forwarder": {
166
- const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
167
- return spawnScriptStreaming(leaf, ["--target", cwd, ...action.argv ?? []], target);
168
- }
169
150
  case "prewarm-bundle": {
170
151
  const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
171
152
  return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
@@ -175,6 +156,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
175
156
  const extra = action.argv ?? [];
176
157
  return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
177
158
  }
159
+ case "clear-metro-cache": {
160
+ const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
161
+ return spawnScriptStreaming(leaf, ["--target", cwd, "--clear"], target);
162
+ }
178
163
  case "launch-mobile-runtime": {
179
164
  const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
180
165
  return spawnScriptStreaming(
@@ -20,47 +20,32 @@ const SOURCE_PATHS = [
20
20
  const ENV_INPUTS = [".js.env", ".env", ".env.local"];
21
21
  const BASELINE_FILE = "mobile-source-baseline.json";
22
22
  function mobileSourceFingerprint(target) {
23
- try {
24
- const hash = createHash("sha256");
25
- hash.update(git(target, ["rev-parse", "HEAD"]));
26
- hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
27
- const untracked = git(target, [
28
- "ls-files",
29
- "--others",
30
- "--exclude-standard",
31
- "-z",
32
- "--",
33
- ...SOURCE_PATHS
34
- ]).toString("utf8").split("\0").filter(Boolean).sort();
35
- for (const relative of untracked) {
36
- hash.update(`untracked\0${relative}\0`);
37
- hash.update(fs.readFileSync(path.join(target, relative)));
38
- hash.update("\0");
39
- }
40
- for (const relative of ENV_INPUTS) {
41
- const absolute = path.join(target, relative);
42
- hash.update(`env\0${relative}\0`);
43
- hash.update(fs.existsSync(absolute) ? fs.readFileSync(absolute) : "<absent>");
44
- hash.update("\0");
45
- }
46
- return { status: "ok", fingerprint: hash.digest("hex") };
47
- } catch {
48
- const resolved = path.resolve(target);
49
- return {
50
- fingerprint: null,
51
- status: "unavailable",
52
- error: {
53
- code: "MOBILE_SOURCE_UNAVAILABLE",
54
- message: `Mobile source fingerprint requires a readable Git checkout and source inputs: ${resolved}`,
55
- userAction: "mm-harness doctor --adapter mobile --target <metamask-mobile-checkout>"
56
- }
57
- };
23
+ const hash = createHash("sha256");
24
+ hash.update(git(target, ["rev-parse", "HEAD"]));
25
+ hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
26
+ const untracked = git(target, [
27
+ "ls-files",
28
+ "--others",
29
+ "--exclude-standard",
30
+ "-z",
31
+ "--",
32
+ ...SOURCE_PATHS
33
+ ]).toString("utf8").split("\0").filter(Boolean).sort();
34
+ for (const relative of untracked) {
35
+ hash.update(`untracked\0${relative}\0`);
36
+ hash.update(fs.readFileSync(path.join(target, relative)));
37
+ hash.update("\0");
38
+ }
39
+ for (const relative of ENV_INPUTS) {
40
+ const absolute = path.join(target, relative);
41
+ hash.update(`env\0${relative}\0`);
42
+ hash.update(fs.existsSync(absolute) ? fs.readFileSync(absolute) : "<absent>");
43
+ hash.update("\0");
58
44
  }
45
+ return hash.digest("hex");
59
46
  }
60
47
  function mobileSourceCheck(target) {
61
- const result = mobileSourceFingerprint(target);
62
- if (result.status === "unavailable") return result;
63
- const { fingerprint } = result;
48
+ const fingerprint = mobileSourceFingerprint(target);
64
49
  const baseline = readBaseline(target);
65
50
  return {
66
51
  fingerprint,
@@ -68,7 +53,7 @@ function mobileSourceCheck(target) {
68
53
  };
69
54
  }
70
55
  function recordMobileSourceBaseline(target, expectedFingerprint) {
71
- if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) return false;
56
+ if (mobileSourceFingerprint(target) !== expectedFingerprint) return false;
72
57
  const marker = recipeRuntimePath(target, BASELINE_FILE);
73
58
  const temporary = `${marker}.${process.pid}.${Date.now()}.tmp`;
74
59
  const baseline = {
@@ -80,9 +65,9 @@ function recordMobileSourceBaseline(target, expectedFingerprint) {
80
65
  try {
81
66
  fs.writeFileSync(temporary, `${JSON.stringify(baseline, null, 2)}
82
67
  `, { mode: 384 });
83
- if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) return false;
68
+ if (mobileSourceFingerprint(target) !== expectedFingerprint) return false;
84
69
  fs.renameSync(temporary, marker);
85
- if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) {
70
+ if (mobileSourceFingerprint(target) !== expectedFingerprint) {
86
71
  if (readBaseline(target)?.fingerprint === expectedFingerprint) fs.rmSync(marker, { force: true });
87
72
  return false;
88
73
  }
@@ -47,7 +47,7 @@ const mobileSurface = {
47
47
  return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
48
48
  },
49
49
  appLogSource(target) {
50
- return { label: "app", path: recipeRuntimePath(target, "app-console.log") };
50
+ return { label: "app", path: recipeRuntimePath(target, "metro.log") };
51
51
  },
52
52
  hints: {
53
53
  launch: "mm-harness launch ios",
@@ -7,6 +7,7 @@ import {
7
7
  resolveExtensionRuntimeContext,
8
8
  resolveExtensionRuntimePorts,
9
9
  resolveSlotPortsByRepo,
10
+ resolveFarmslotPortsByRepo,
10
11
  resolveMobileRuntimeContext,
11
12
  resolveMobileRuntimePorts,
12
13
  resolveMobileSlotDefaults
@@ -19,6 +20,7 @@ export {
19
20
  resolveDefaultExtensionPorts,
20
21
  resolveExtensionRuntimeContext,
21
22
  resolveExtensionRuntimePorts,
23
+ resolveFarmslotPortsByRepo,
22
24
  resolveMobileRuntimeContext,
23
25
  resolveMobileRuntimePorts,
24
26
  resolveMobileSlotDefaults,
@@ -105,32 +105,17 @@ function isExtensionWatcherLive(target) {
105
105
  function stopExtensionWatcher(target) {
106
106
  const runtimeAbs = path.join(path.resolve(target), recipeRuntimeDir());
107
107
  const webpackPidFile = path.join(runtimeAbs, "recipe-harness-webpack.pid");
108
- const syncPidFile = path.join(runtimeAbs, "recipe-harness-webpack-sync.pid");
109
108
  const ownedPids = extensionWatcherPids(target);
110
- try {
111
- const syncPid = fs.readFileSync(syncPidFile, "utf8").trim();
112
- if (/^\d+$/u.test(syncPid) && processAlive(Number(syncPid))) ownedPids.push(Number(syncPid));
113
- } catch {
114
- }
115
109
  for (const pid of ownedPids) {
116
- try {
117
- process.kill(-pid, "SIGTERM");
118
- } catch {
119
- }
120
110
  try {
121
111
  process.kill(pid, "SIGTERM");
122
112
  } catch {
123
113
  }
124
114
  }
125
115
  fs.rmSync(webpackPidFile, { force: true });
126
- fs.rmSync(syncPidFile, { force: true });
127
116
  if (ownedPids.length > 0) {
128
117
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
129
118
  for (const pid of ownedPids) {
130
- try {
131
- process.kill(-pid, "SIGKILL");
132
- } catch {
133
- }
134
119
  try {
135
120
  process.kill(pid, "SIGKILL");
136
121
  } catch {
@@ -179,7 +164,6 @@ function stopExtensionRuntime(target) {
179
164
  let removedMarkers = 0;
180
165
  for (const name of [
181
166
  "recipe-harness-webpack.pid",
182
- "recipe-harness-webpack-sync.pid",
183
167
  "webpack.pid",
184
168
  "browser.pid",
185
169
  "chromium.pid",
package/dist/adapters.js CHANGED
@@ -287,15 +287,8 @@ async function handleMobileNavigate(payload, context) {
287
287
  return isRecord(live.result) ? { ...live.result, liveAdapter: live.script } : live.result;
288
288
  }
289
289
  async function handleMobilePress(payload, context) {
290
- const identifiers = ["test_id", "testID", "selector"].map((field) => optionalScalarText(payload[field], `ui.press.${field}`)).filter((value) => value !== void 0);
291
- const text = optionalScalarText(payload.text, "ui.press.text");
292
- if (identifiers.length + Number(text !== void 0) !== 1) {
293
- throw new Error("ui.press requires exactly one of: test_id, testID, selector, text.");
294
- }
295
- const target = text ?? identifiers[0];
296
- if (target.trim() === "") throw new Error("ui.press target must be non-empty.");
297
- const input = mobileUiInput(context, "press", payload);
298
- return text !== void 0 ? bridgeCommand(input, ["press-text", target]) : bridgeCommand(input, ["press-test-id", target]);
290
+ const target = firstScalarText(payload, ["test_id", "testID", "selector", "text"], "ui.press");
291
+ return bridgeCommand(mobileUiInput(context, "press", payload), ["press-test-id", target]);
299
292
  }
300
293
  async function handleMobileSetInput(payload, context) {
301
294
  const input = mobileUiInput(context, "set_input", payload);
@@ -472,12 +465,12 @@ function createMetaMaskUiTransport(platform, harness) {
472
465
  if (live) return live.output;
473
466
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
474
467
  }
475
- return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node, platform) : node, context);
468
+ return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
476
469
  }
477
470
  };
478
471
  }
479
- function normalizeUiWaitNode(node, adapter) {
480
- if (adapter !== "extension" || node.expected !== void 0 || typeof node.visible !== "boolean") return node;
472
+ function normalizeUiWaitNode(node) {
473
+ if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
481
474
  return { ...node, expected: node.visible ? "visible" : "absent" };
482
475
  }
483
476
  function createMetaMaskAdapters(adapter) {
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
16
16
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
17
17
  { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
18
- { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action"] },
18
+ { name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
19
19
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
20
20
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
21
21
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { detectAdapter, handleHarness } from "./harness.js";
2
- import { handleActions } from "./commands/manifest.js";
2
+ import { handleActions, handleManifest } from "./commands/manifest.js";
3
3
  import { handleDoctor } from "./commands/doctor.js";
4
4
  import { handleProvision } from "./commands/provision.js";
5
5
  import { handleRuntimeHealth } from "./commands/runtime-health.js";
@@ -21,10 +21,10 @@ import { handleRecipeQuality } from "./commands/recipe-quality.js";
21
21
  import { handleStatus } from "./commands/status.js";
22
22
  import { handleCheck } from "./commands/check.js";
23
23
  import { handleChecklist } from "./commands/checklist.js";
24
- import { handleLast } from "./commands/last.js";
25
24
  import { parseArgs, targetPath } from "./commands/parse-args.js";
26
25
  import { runOneNode } from "./commands/run-engine.js";
27
26
  const COMMANDS = {
27
+ manifest: handleManifest,
28
28
  actions: handleActions,
29
29
  doctor: handleDoctor,
30
30
  "runtime-health": handleRuntimeHealth,
@@ -35,7 +35,7 @@ const COMMANDS = {
35
35
  run: handleRun,
36
36
  "self-test": handleSelfTest
37
37
  };
38
- const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
38
+ const OVERLAY_COMMANDS = ["install", "verify", "cleanup", "live"];
39
39
  function usage() {
40
40
  console.error(`mm-harness \u2014 the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay.
41
41
  Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.
@@ -62,6 +62,9 @@ PROVE \u2014 run recipes and inspect capabilities:
62
62
  mm-harness check diff --profile fast --artifacts-dir artifacts/validation
63
63
  actions Describe the actions a manifest declares.
64
64
  mm-harness actions --adapter mobile
65
+ manifest Print/validate the action manifest for an adapter.
66
+ mm-harness manifest --adapter extension --json
67
+
65
68
  RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
66
69
  provision Install the cached Runway mobile dev client (artifact cache + sim install).
67
70
  mm-harness provision runway ios --adapter mobile
@@ -69,8 +72,12 @@ RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
69
72
  mm-harness install # inside a checkout, auto-detected
70
73
  verify Check the overlay/runtime is present and healthy (no launch).
71
74
  mm-harness verify
75
+ live Launch/reuse the app, then verify live control end-to-end.
76
+ mm-harness live
72
77
  cleanup Remove the installed overlay and restore the checkout.
73
78
  mm-harness cleanup
79
+ # Open sub-question (Arthur): keep these top-level (mm-harness install) or group
80
+ # them under an \`overlay\` command (mm-harness overlay install)? Top-level for now.
74
81
 
75
82
  ADVANCED \u2014 internal runtime probes (rarely typed by hand):
76
83
  runtime-health runtime-decision runtime-launch
@@ -81,7 +88,7 @@ ONE bin: mm-harness is the only command. No per-platform binaries \u2014 platfor
81
88
  auto-detected, the positional target forces it (mm-harness launch ios), and
82
89
  platform-specific needs are FLAGS on the same command (e.g. --sidebar, --full-build).
83
90
  Human happy path = the bare command; agents add depth via flags (--json, --target, ports).
84
- See README.md for the current workflow and docs/CHEATSHEET.md for examples.
91
+ See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).
85
92
  `);
86
93
  }
87
94
  async function main(argv) {
@@ -104,7 +111,7 @@ async function main(argv) {
104
111
  const target = targetPath(options);
105
112
  const adapterRaw = (typeof options["adapter"] === "string" ? options["adapter"] : void 0) ?? (typeof options["platform"] === "string" ? options["platform"] : void 0) ?? detectAdapter(target);
106
113
  if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core") {
107
- await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
114
+ await handleHarness(["install", "--platform", adapterRaw, "--target", target]);
108
115
  }
109
116
  } catch {
110
117
  }
@@ -126,7 +133,6 @@ async function main(argv) {
126
133
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
127
134
  if (command === "check") return handleCheck(argv.slice(1));
128
135
  if (command === "checklist") return handleChecklist(argv.slice(1));
129
- if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
130
136
  const handler = COMMANDS[command];
131
137
  if (!handler) throw new Error(`Unknown command: ${command}`);
132
138
  return handler(parseArgs(argv.slice(1), command));
@@ -6,7 +6,7 @@ import { loadActionManifest } from "../manifest.js";
6
6
  import { importRecipeProtocol } from "../paths.js";
7
7
  import { recipeRunning } from "../heal-bounds.js";
8
8
  import { color } from "../cli-color.js";
9
- import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
9
+ import { EXIT } from "./shared.js";
10
10
  import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
11
11
  import {
12
12
  parseArgs,
@@ -15,7 +15,6 @@ import {
15
15
  optionString,
16
16
  resolveAdapter,
17
17
  runtimeOptionsFromCli,
18
- shellQuote,
19
18
  usageError
20
19
  } from "./parse-args.js";
21
20
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -33,7 +32,6 @@ import {
33
32
  validateRecipeAdapterAware
34
33
  } from "./run-engine.js";
35
34
  import { acquireCheckoutLock } from "../checkout-lock.js";
36
- import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
37
35
  async function handleCall(argv) {
38
36
  if (argv.includes("--list")) {
39
37
  const { options: options2 } = parseArgs(argv, "call");
@@ -61,42 +59,25 @@ async function handleCall(argv) {
61
59
  }
62
60
  const message = `call requires <action>. Example: ${example}`;
63
61
  const userAction = `${example} # see the vocabulary: ${discovery}`;
64
- if (json) console.log(JSON.stringify({
65
- schemaVersion: 1,
66
- command: "call",
67
- status: "fail",
68
- error: { code: "CLI_MISSING_POSITIONAL", message, userAction },
69
- exitCode: EXIT.usage
70
- }, null, 2));
62
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", error: { code: "USAGE", message, userAction } }, null, 2));
71
63
  else console.error(`${message}
72
64
  See the vocabulary: ${discovery}`);
73
65
  return EXIT.usage;
74
66
  }
75
67
  const { adapter, target } = resolveAdapter(options);
76
- if (!fs.existsSync(target)) {
77
- return usageOut(
78
- json,
79
- "call",
80
- `target does not exist: ${target}`,
81
- "pass --target <metamask-checkout> pointing to an existing checkout"
82
- );
83
- }
84
68
  getAdapterSurface(adapter).resolveSlotPorts(target);
85
69
  const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
86
70
  if ("code" in dtResult) {
87
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
88
- else console.error(`\u2717 call: ${dtResult.message}
89
- Next: ${dtResult.userAction}`);
71
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
72
+ else console.error(`\u2717 call: ${dtResult.message}`);
90
73
  return EXIT.usage;
91
74
  }
92
75
  if (recipeRunning(target)) {
93
76
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
94
- const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
95
77
  if (json) {
96
- console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
78
+ console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
97
79
  } else {
98
- console.error(`\u2717 mm-harness call: ${msg}
99
- Next: ${userAction}`);
80
+ console.error(`\u2717 mm-harness call: ${msg}`);
100
81
  }
101
82
  return EXIT.bounded;
102
83
  }
@@ -106,19 +87,16 @@ async function handleCall(argv) {
106
87
  const names = getRecipeActionManifestActionNames(manifest);
107
88
  const resolution = resolveActionName(shortName, names);
108
89
  if (resolution.status === "unknown") {
109
- const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
110
- const userAction = `mm-harness actions --adapter ${adapter} --json`;
111
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
112
- else console.error(`\u2717 call: ${message}
113
- Next: ${userAction}`);
90
+ const message = `\u2717 call: unknown action "${shortName}" for the ${adapter} adapter.
91
+ See the vocabulary: mm-harness actions --adapter ${adapter} --json`;
92
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
93
+ else console.error(message);
114
94
  return EXIT.usage;
115
95
  }
116
96
  if (resolution.status === "ambiguous") {
117
- const message = `"${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
118
- const userAction = `mm-harness actions --action ${resolution.candidates[0]} --adapter ${adapter} --json`;
119
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates, userAction } }, null, 2));
120
- else console.error(`\u2717 call: ${message}
121
- Next: ${userAction}`);
97
+ const message = `\u2717 call: "${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
98
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates } }, null, 2));
99
+ else console.error(message);
122
100
  return EXIT.usage;
123
101
  }
124
102
  const resolvedAction = resolution.resolved;
@@ -135,19 +113,19 @@ async function handleCall(argv) {
135
113
  const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
136
114
  const validation = await validateRecipeAdapterAware(recipe, manifest);
137
115
  if (validation.status === "invalid") {
138
- const message = `call ${resolvedAction}: recipe validation failed`;
139
- const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
140
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
116
+ const message = `\u2717 call ${resolvedAction}: recipe validation failed`;
117
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message } }, null, 2));
141
118
  else {
142
- console.error(`\u2717 ${message}`);
119
+ console.error(message);
143
120
  for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
144
- console.error(` Next: ${userAction}`);
145
121
  }
146
122
  return EXIT.validation;
147
123
  }
148
124
  const lock = acquireCheckoutLock(target, "call");
149
125
  if ("message" in lock) {
150
- return checkoutBusyOut(json, "call", lock.message, lock.path);
126
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "SANDBOX_BUSY", message: lock.message } }, null, 2));
127
+ else console.error(`\u2717 mm-harness call: ${lock.message}`);
128
+ return EXIT.bounded;
151
129
  }
152
130
  try {
153
131
  const prepared = await prepareHeal(adapter, target, options, json, {
@@ -176,7 +154,6 @@ async function handleCall(argv) {
176
154
  );
177
155
  if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
178
156
  const callOutput = readCallOutput(result.tracePath);
179
- const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
180
157
  if (json) {
181
158
  console.log(
182
159
  JSON.stringify(
@@ -191,13 +168,10 @@ async function handleCall(argv) {
191
168
  summaryPath: result.summaryPath,
192
169
  tracePath: result.tracePath,
193
170
  artifactManifestPath: result.artifactManifestPath,
194
- ...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
195
- ...result.sideFindings ? { sideFindings: result.sideFindings } : {},
196
171
  ...callOutput !== void 0 ? { output: callOutput } : {},
197
172
  recovered: state.recovered,
198
173
  mutations: state.mutations,
199
- exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
200
- ...result.status === "fail" ? { error: { code: "ACTION_EXECUTION_FAILED", message: `${resolvedAction} failed; inspect the persisted result and evidence paths`, userAction: failureUserAction } } : {}
174
+ exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
201
175
  },
202
176
  null,
203
177
  2
@@ -208,19 +182,11 @@ async function handleCall(argv) {
208
182
  Result:
209
183
  ${formatCallOutput(callOutput)}` : "";
210
184
  const out = (style, text) => color(style, text, { stream: process.stdout });
211
- const diagnostics = formatRunDiagnosticsForHuman(
212
- readRunDiagnosticsDocument(result.diagnosticsPath),
213
- adapter
214
- );
215
- const renderedDiagnostics = `${out("label", "Diagnostics:")}
216
- ${diagnostics.map((line) => ` ${line}`).join("\n")}
217
- `;
218
185
  console.log(
219
186
  `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
220
187
  ${out("label", "Result:")}
221
188
  ${formatCallOutput(callOutput)}` : ""}
222
- ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
223
- Next: ${failureUserAction}` : "")
189
+ ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
224
190
  );
225
191
  }
226
192
  return result.status === "pass" ? EXIT.ok : EXIT.runtime;
@@ -354,7 +320,7 @@ function parseCallValue(value) {
354
320
  const number = Number(value);
355
321
  if (Number.isFinite(number)) return number;
356
322
  }
357
- if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}") || value.startsWith('"') && value.endsWith('"')) {
323
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
358
324
  try {
359
325
  return JSON.parse(value);
360
326
  } catch {
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { detectAdapter } from "../harness.js";
5
5
  import { EXIT, usageOut } from "./shared.js";
6
- import { optionFlag, optionString, parseArgs, shellQuote } from "./parse-args.js";
6
+ import { optionFlag, optionString, parseArgs } from "./parse-args.js";
7
7
  const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
8
8
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
9
9
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
@@ -111,7 +111,6 @@ async function handleCheck(argv) {
111
111
  const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
112
112
  const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
113
113
  const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
114
- const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
115
114
  const summary = {
116
115
  schemaVersion: 1,
117
116
  command: "check",
@@ -130,7 +129,6 @@ async function handleCheck(argv) {
130
129
  fixes,
131
130
  checks,
132
131
  guidance,
133
- ...status === "fail" ? { error: { code: "CHECK_DIFF_FAILED", message: "one or more bounded checks failed", userAction: failureUserAction } } : {},
134
132
  artifacts: {
135
133
  dir: artifactsDir,
136
134
  summaryJson: path.join(artifactsDir, "validation-summary.json"),
@@ -520,14 +518,10 @@ function renderHuman(summary) {
520
518
  if (check.status === "fail" && check.logPath) console.log(` log: ${check.logPath}`);
521
519
  }
522
520
  for (const item of summary.guidance) console.log(` Next: ${item}`);
523
- if (summary.guidance.length === 0 && summary.status === "fail" && summary.error?.userAction) {
524
- console.log(` Next: ${summary.error.userAction}`);
525
- }
526
521
  console.log(`Artifacts: ${summary.artifacts.summaryJson}`);
527
522
  }
528
523
  function failEnvelope(input) {
529
524
  const exitCode = EXIT.usage;
530
- const userAction = USAGE;
531
525
  const body = {
532
526
  schemaVersion: 1,
533
527
  command: "check",
@@ -540,11 +534,11 @@ function failEnvelope(input) {
540
534
  baseRef: input.baseRef,
541
535
  baseSource: "unknown",
542
536
  artifacts: { dir: input.artifactsDir },
543
- error: { code: input.code, message: input.message, userAction }
537
+ error: { code: input.code, message: input.message }
544
538
  };
545
539
  if (input.json) console.log(JSON.stringify(body, null, 2));
546
540
  else console.error(`\u2717 check diff: ${input.message}
547
- Next: ${userAction}`);
541
+ Next: ${USAGE}`);
548
542
  return exitCode;
549
543
  }
550
544
  export {