@deeeed/metamask-harness 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +90 -137
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/dist/adapters/extension/console-capture.js +84 -0
  61. package/dist/adapters/extension/runtime-decision.js +14 -2
  62. package/dist/adapters/extension/surface.js +3 -2
  63. package/dist/adapters/mobile/prepare.js +22 -7
  64. package/dist/adapters/mobile/surface.js +1 -1
  65. package/dist/adapters/resolve-slot-ports.js +0 -2
  66. package/dist/adapters/slot-ports.js +8 -0
  67. package/dist/cli.js +5 -11
  68. package/dist/command-contract.js +441 -0
  69. package/dist/command-journal.js +225 -0
  70. package/dist/commands/call.js +43 -20
  71. package/dist/commands/check.js +9 -3
  72. package/dist/commands/debug.js +38 -31
  73. package/dist/commands/device-target.js +27 -12
  74. package/dist/commands/doctor.js +19 -6
  75. package/dist/commands/fixtures.js +19 -11
  76. package/dist/commands/last.js +52 -0
  77. package/dist/commands/launch/index.js +166 -59
  78. package/dist/commands/logs.js +8 -6
  79. package/dist/commands/manifest.js +114 -29
  80. package/dist/commands/parse-args.js +1 -0
  81. package/dist/commands/provision.js +10 -3
  82. package/dist/commands/run-engine.js +27 -42
  83. package/dist/commands/run-report.js +1 -1
  84. package/dist/commands/run.js +243 -41
  85. package/dist/commands/shared.js +11 -1
  86. package/dist/commands/status-probe.js +1 -1
  87. package/dist/commands/status.js +1 -1
  88. package/dist/commands/stop.js +7 -2
  89. package/dist/doctor.js +1 -4
  90. package/dist/harness.js +19 -125
  91. package/dist/json-stream.js +57 -0
  92. package/dist/live-adapter-contract.js +6 -20
  93. package/dist/mm-harness-cli.js +111 -41
  94. package/dist/paths.js +2 -5
  95. package/dist/run-diagnostics.js +35 -40
  96. package/dist/runner.js +32 -1
  97. package/dist/runtime-context.js +26 -8
  98. package/docs/CHEATSHEET.md +39 -52
  99. package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
  100. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
  101. package/docs/CODE-MAP.md +1 -1
  102. package/docs/UX-PRINCIPLES.md +2 -0
  103. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  104. package/docs/architecture.md +65 -375
  105. package/docs/live-adapter-contract.md +3 -1
  106. package/library/actions/core/perps/_controller.mjs +10 -55
  107. package/library/manifests/mobile.action-manifest.json +1 -1
  108. package/package.json +3 -2
  109. package/scripts/completions.sh +0 -0
  110. package/scripts/install-completions.sh +0 -0
  111. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  112. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  113. package/adapters/mobile/overlay/compat/README.md +0 -28
  114. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  115. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  116. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  117. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  118. package/docs/CLI-SPEC.md +0 -1102
  119. package/docs/MENTAL-MODEL.md +0 -295
@@ -65,11 +65,12 @@ const extensionSurface = {
65
65
  return [
66
66
  { label: "webpack", path: recipeRuntimePath(target, "webpack.log") },
67
67
  { label: "watcher", path: recipeRuntimePath(target, "recipe-harness-webpack.log") },
68
- { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") }
68
+ { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") },
69
+ { label: "dapp", path: recipeRuntimePath(target, "dapp-console.log") }
69
70
  ];
70
71
  },
71
72
  appLogSource(target) {
72
- return { label: "app", path: recipeRuntimePath(target, "extension-console.log") };
73
+ return { label: "extension", path: recipeRuntimePath(target, "extension-console.log") };
73
74
  },
74
75
  hints: {
75
76
  launch: "mm-harness launch",
@@ -56,14 +56,21 @@ async function prepareMobile(target, opts = {}) {
56
56
  json,
57
57
  preflightMode
58
58
  );
59
- if (confirm.status === 0) return { status: 0, output: "" };
59
+ if (confirm.status === 0) return startMobileConsoleForwarder(target, json);
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
- return dispatchActionSequence(launchActions(path.resolve(target)), target, platform, json, preflightMode);
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, json) : launch;
67
74
  }
68
75
  if (report.decision === "unknown") {
69
76
  const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
@@ -111,7 +118,15 @@ async function prepareMobile(target, opts = {}) {
111
118
  }
112
119
  }
113
120
  }
114
- return { status: 0, output: "" };
121
+ return startMobileConsoleForwarder(target, json);
122
+ }
123
+ function startMobileConsoleForwarder(target, json) {
124
+ return dispatchAction(
125
+ { id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
126
+ target,
127
+ "ios",
128
+ json
129
+ );
115
130
  }
116
131
  async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
117
132
  const resolved = path.resolve(target);
@@ -147,6 +162,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
147
162
  const extra = action.argv ?? [];
148
163
  return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
149
164
  }
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
+ }
150
169
  case "prewarm-bundle": {
151
170
  const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
152
171
  return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
@@ -156,10 +175,6 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
156
175
  const extra = action.argv ?? [];
157
176
  return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
158
177
  }
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
- }
163
178
  case "launch-mobile-runtime": {
164
179
  const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
165
180
  return spawnScriptStreaming(
@@ -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, "metro.log") };
50
+ return { label: "app", path: recipeRuntimePath(target, "app-console.log") };
51
51
  },
52
52
  hints: {
53
53
  launch: "mm-harness launch ios",
@@ -7,7 +7,6 @@ import {
7
7
  resolveExtensionRuntimeContext,
8
8
  resolveExtensionRuntimePorts,
9
9
  resolveSlotPortsByRepo,
10
- resolveFarmslotPortsByRepo,
11
10
  resolveMobileRuntimeContext,
12
11
  resolveMobileRuntimePorts,
13
12
  resolveMobileSlotDefaults
@@ -20,7 +19,6 @@ export {
20
19
  resolveDefaultExtensionPorts,
21
20
  resolveExtensionRuntimeContext,
22
21
  resolveExtensionRuntimePorts,
23
- resolveFarmslotPortsByRepo,
24
22
  resolveMobileRuntimeContext,
25
23
  resolveMobileRuntimePorts,
26
24
  resolveMobileSlotDefaults,
@@ -105,7 +105,13 @@ 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");
108
109
  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
+ }
109
115
  for (const pid of ownedPids) {
110
116
  try {
111
117
  process.kill(pid, "SIGTERM");
@@ -113,6 +119,7 @@ function stopExtensionWatcher(target) {
113
119
  }
114
120
  }
115
121
  fs.rmSync(webpackPidFile, { force: true });
122
+ fs.rmSync(syncPidFile, { force: true });
116
123
  if (ownedPids.length > 0) {
117
124
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
118
125
  for (const pid of ownedPids) {
@@ -164,6 +171,7 @@ function stopExtensionRuntime(target) {
164
171
  let removedMarkers = 0;
165
172
  for (const name of [
166
173
  "recipe-harness-webpack.pid",
174
+ "recipe-harness-webpack-sync.pid",
167
175
  "webpack.pid",
168
176
  "browser.pid",
169
177
  "chromium.pid",
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { detectAdapter, handleHarness } from "./harness.js";
2
- import { handleActions, handleManifest } from "./commands/manifest.js";
2
+ import { handleActions } 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";
24
25
  import { parseArgs, targetPath } from "./commands/parse-args.js";
25
26
  import { runOneNode } from "./commands/run-engine.js";
26
27
  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", "live"];
38
+ const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
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,9 +62,6 @@ 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
-
68
65
  RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
69
66
  provision Install the cached Runway mobile dev client (artifact cache + sim install).
70
67
  mm-harness provision runway ios --adapter mobile
@@ -72,12 +69,8 @@ RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
72
69
  mm-harness install # inside a checkout, auto-detected
73
70
  verify Check the overlay/runtime is present and healthy (no launch).
74
71
  mm-harness verify
75
- live Launch/reuse the app, then verify live control end-to-end.
76
- mm-harness live
77
72
  cleanup Remove the installed overlay and restore the checkout.
78
73
  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.
81
74
 
82
75
  ADVANCED \u2014 internal runtime probes (rarely typed by hand):
83
76
  runtime-health runtime-decision runtime-launch
@@ -88,7 +81,7 @@ ONE bin: mm-harness is the only command. No per-platform binaries \u2014 platfor
88
81
  auto-detected, the positional target forces it (mm-harness launch ios), and
89
82
  platform-specific needs are FLAGS on the same command (e.g. --sidebar, --full-build).
90
83
  Human happy path = the bare command; agents add depth via flags (--json, --target, ports).
91
- See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).
84
+ See README.md for the current workflow and docs/CHEATSHEET.md for examples.
92
85
  `);
93
86
  }
94
87
  async function main(argv) {
@@ -133,6 +126,7 @@ async function main(argv) {
133
126
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
134
127
  if (command === "check") return handleCheck(argv.slice(1));
135
128
  if (command === "checklist") return handleChecklist(argv.slice(1));
129
+ if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
136
130
  const handler = COMMANDS[command];
137
131
  if (!handler) throw new Error(`Unknown command: ${command}`);
138
132
  return handler(parseArgs(argv.slice(1), command));
@@ -0,0 +1,441 @@
1
+ const bool = () => ({ kind: "boolean" });
2
+ const value = (choices) => ({ kind: "value", ...choices ? { choices } : {} });
3
+ const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { choices } : {} });
4
+ const options = (...groups) => Object.assign({}, ...groups);
5
+ const HELP = { "--help": bool(), "-h": bool() };
6
+ const JSON = { "--json": bool() };
7
+ const JSON_STREAM = { "--json-stream": bool() };
8
+ const TARGET = { "--target": value(), "--project-root": value() };
9
+ const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
10
+ const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
11
+ const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "ios", "android"]) };
12
+ const DEVICE = { "--device": value() };
13
+ const RUNTIME_PORTS = {
14
+ "--cdp-port": value(),
15
+ "--watcher-port": value(),
16
+ "--metro-port": value(),
17
+ "--runtime-dir": value()
18
+ };
19
+ const RECIPE_RUNTIME = {
20
+ "--artifacts-dir": value(),
21
+ "--action-manifest": value(),
22
+ "--library": value(),
23
+ "--heal": value(["off", "infra-only", "auto"]),
24
+ "--slot": value(),
25
+ "--validation-runtime-dir": value(),
26
+ "--launch-existing-dist": bool(),
27
+ "--record-video": optionalValue(["full-run", "off"]),
28
+ "--record": bool()
29
+ };
30
+ const RUNWAY = {
31
+ "--branch": value(),
32
+ "--default-branch": value(),
33
+ "--run": value(),
34
+ "--cache-root": value(),
35
+ "--simulator": value(),
36
+ "--runtime": value(),
37
+ "--device-type": value(),
38
+ "--slot": value(),
39
+ "--watcher-port": value(),
40
+ "--runtime-dir": value(),
41
+ "--force": bool(),
42
+ "--resolve-only": bool()
43
+ };
44
+ const PUBLIC_COMMAND_CONTRACTS = {
45
+ status: {
46
+ aliases: ["health", "home"],
47
+ options: options(HELP, JSON, TARGET, DEVICE, {
48
+ "--fast": bool(),
49
+ "--all-devices": bool()
50
+ })
51
+ },
52
+ checklist: {
53
+ usage: "mm-harness checklist mark <task-dir> <step> [options]",
54
+ options: options(HELP, {
55
+ "--mark-last": bool(),
56
+ "--already-fixed": bool(),
57
+ "--no-self-review": bool(),
58
+ "--skip-learnings": bool(),
59
+ "--skip-checklist": bool(),
60
+ "--reason": value(),
61
+ "--checklist": value(),
62
+ "--signal": value()
63
+ }),
64
+ positionals: [
65
+ { label: "action", choices: ["mark"] },
66
+ { label: "task-dir" },
67
+ {
68
+ label: "step",
69
+ validate: (step) => {
70
+ if (["start", "complete", "no-change", "blocked"].includes(step)) return true;
71
+ const numeric = Number(step);
72
+ return Number.isInteger(numeric) && numeric >= 1;
73
+ },
74
+ validDescription: "start|complete|no-change|blocked|a positive numeric step"
75
+ }
76
+ ],
77
+ minimumPositionals: 3
78
+ },
79
+ actions: {
80
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
81
+ "--action": value(),
82
+ "--categories": bool(),
83
+ "--category": value(),
84
+ "--action-manifest": value(),
85
+ "--raw": bool()
86
+ }),
87
+ positionals: [{ label: "query" }],
88
+ noPositionalsWith: ["--action", "--categories", "--raw"]
89
+ },
90
+ stop: {
91
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
92
+ "--port": value(),
93
+ "--watcher-port": value()
94
+ })
95
+ },
96
+ call: {
97
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
98
+ "--list": bool(),
99
+ "--arg": value()
100
+ }),
101
+ positionals: [{ label: "action" }],
102
+ minimumPositionals: 0,
103
+ requiredUnless: ["--list"],
104
+ noPositionalsWith: ["--list"],
105
+ leadingPositionals: 1,
106
+ variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
107
+ },
108
+ flows: {
109
+ options: options(HELP, JSON, TARGET, {
110
+ "--library": value(),
111
+ "--from": value(),
112
+ "--flow": value(),
113
+ "--to": value(),
114
+ "--domain": value(),
115
+ "--run": value(),
116
+ "--force": bool()
117
+ }),
118
+ positionals: [{ label: "action", choices: ["list", "promote"] }]
119
+ },
120
+ run: {
121
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
122
+ "--list": bool(),
123
+ "--plan": bool()
124
+ }),
125
+ positionals: [{ label: "recipe" }],
126
+ minimumPositionals: 1,
127
+ requiredUnless: ["--list"],
128
+ noPositionalsWith: ["--list"]
129
+ },
130
+ last: {
131
+ options: options(HELP, JSON, TARGET, {
132
+ "--runtime-dir": value()
133
+ })
134
+ },
135
+ doctor: {
136
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
137
+ "--fix": bool(),
138
+ "--expect-live": bool(),
139
+ "--print-ready": bool(),
140
+ "--all-devices": bool(),
141
+ "--action-manifest": value(),
142
+ "--port": value()
143
+ })
144
+ },
145
+ check: {
146
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
147
+ "--base": value(),
148
+ "--profile": value(["fast", "full"]),
149
+ "--fix": bool(),
150
+ "--artifacts-dir": value()
151
+ }),
152
+ positionals: [{ label: "action", choices: ["diff"] }],
153
+ minimumPositionals: 1
154
+ },
155
+ "recipe-quality": {
156
+ options: options(HELP, JSON, {
157
+ "--input": value(),
158
+ "--output": value()
159
+ }),
160
+ positionals: [{ label: "action", choices: ["build"] }]
161
+ },
162
+ provision: {
163
+ options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
164
+ "--platform": value(["ios"]),
165
+ "--device-platform": value(["ios"])
166
+ }),
167
+ positionals: [
168
+ { label: "source", choices: ["runway", "ios"] },
169
+ { label: "platform", choices: ["ios"] }
170
+ ]
171
+ },
172
+ install: {
173
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNWAY, {
174
+ "--runway": bool()
175
+ }),
176
+ allowPassthrough: true
177
+ },
178
+ verify: {
179
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, RUNTIME_PORTS),
180
+ allowPassthrough: true
181
+ },
182
+ cleanup: {
183
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM),
184
+ allowPassthrough: true
185
+ },
186
+ launch: {
187
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
188
+ "--build": bool(),
189
+ "--verify": bool(),
190
+ "--runway": bool(),
191
+ "--watch": bool(),
192
+ "--sidepanel": bool(),
193
+ "--fullscreen": bool(),
194
+ "--url": value(),
195
+ "--heal": value(["off", "infra-only", "auto"])
196
+ }),
197
+ positionals: [{ label: "platform", choices: ["ios", "android"] }]
198
+ },
199
+ logs: {
200
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
201
+ "--full": bool(),
202
+ "--window": bool(),
203
+ "--events": value(),
204
+ "--source": value()
205
+ })
206
+ },
207
+ debug: {
208
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
209
+ "--worker": bool(),
210
+ "--dev-menu": bool(),
211
+ "--no-open": bool()
212
+ })
213
+ },
214
+ update: {
215
+ options: options(HELP, JSON, { "--check": bool() })
216
+ },
217
+ fixtures: {
218
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
219
+ "--from": value(),
220
+ "--dev": bool(),
221
+ "--force": bool(),
222
+ "--fixture": value(),
223
+ "--out": value(),
224
+ "--state": value(),
225
+ "--cdp-port": value(),
226
+ "--extension-dir": value(),
227
+ "--extension-id-file": value(),
228
+ "--action-manifest": value()
229
+ }),
230
+ positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
231
+ minimumPositionals: 1
232
+ }
233
+ };
234
+ function validatePublicInvocation(argv, examples = {}) {
235
+ const token = argv[0];
236
+ if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
237
+ const resolved = resolveCommand(token);
238
+ if (!resolved) {
239
+ const names = publicCommandTokens();
240
+ const suggestion = closest(token, names);
241
+ if (token.startsWith("-")) {
242
+ return {
243
+ code: "CLI_UNKNOWN_OPTION",
244
+ command: "mm-harness",
245
+ message: `unknown top-level option '${token}'. Valid top-level options: --help, --version.`,
246
+ userAction: actionFor(token, closest(token, ["--help", "--version"]), "mm-harness --help")
247
+ };
248
+ }
249
+ return {
250
+ code: "CLI_UNKNOWN_COMMAND",
251
+ command: token,
252
+ message: `unknown command '${token}'. Valid commands: ${Object.keys(PUBLIC_COMMAND_CONTRACTS).join(", ")}.`,
253
+ userAction: actionFor(token, suggestion, suggestion ? exampleFor(resolveCommand(suggestion)?.name, examples) : "mm-harness --help")
254
+ };
255
+ }
256
+ const { name, contract } = resolved;
257
+ const tokens = argv.slice(1);
258
+ const positionals = [];
259
+ const seenOptions = /* @__PURE__ */ new Set();
260
+ const validOptions = Object.keys(contract.options);
261
+ for (let index = 0; index < tokens.length; index += 1) {
262
+ const argument = tokens[index] ?? "";
263
+ if (argument === "--") {
264
+ if (!contract.allowPassthrough) {
265
+ return usageFailure("CLI_UNEXPECTED_PASSTHROUGH", name, "this command does not accept `--` passthrough.", contract, examples);
266
+ }
267
+ break;
268
+ }
269
+ if (!argument.startsWith("-") || argument === "-") {
270
+ positionals.push(argument);
271
+ continue;
272
+ }
273
+ const equals = argument.indexOf("=");
274
+ const optionName = equals === -1 ? argument : argument.slice(0, equals);
275
+ const inlineValue = equals === -1 ? void 0 : argument.slice(equals + 1);
276
+ const spec = contract.options[optionName];
277
+ if (!spec) {
278
+ const suggestion = closest(optionName, validOptions);
279
+ return {
280
+ code: "CLI_UNKNOWN_OPTION",
281
+ command: name,
282
+ message: `unknown option '${optionName}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
283
+ userAction: actionFor(optionName, suggestion, exampleFor(name, examples))
284
+ };
285
+ }
286
+ seenOptions.add(optionName);
287
+ if (spec.kind === "boolean") {
288
+ if (inlineValue !== void 0) {
289
+ return usageFailure("CLI_INVALID_OPTION_VALUE", name, `${optionName} is a flag and does not take a value.`, contract, examples);
290
+ }
291
+ continue;
292
+ }
293
+ if (spec.kind === "optional-value" && inlineValue === void 0) continue;
294
+ const optionValue = inlineValue ?? tokens[index + 1];
295
+ const nextTokenIsOption = inlineValue === void 0 && (optionValue === "-h" || optionValue === "--" || optionValue?.startsWith("--"));
296
+ if (optionValue === void 0 || spec.kind === "value" && optionValue === "" || nextTokenIsOption) {
297
+ return usageFailure("CLI_MISSING_OPTION_VALUE", name, `${optionName} requires a value.`, contract, examples);
298
+ }
299
+ if (inlineValue === void 0) index += 1;
300
+ if (spec.choices && !spec.choices.includes(optionValue)) {
301
+ const suggestion = closest(optionValue, spec.choices);
302
+ return {
303
+ code: "CLI_INVALID_OPTION_VALUE",
304
+ command: name,
305
+ message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
306
+ userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
307
+ };
308
+ }
309
+ }
310
+ if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
311
+ if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
312
+ const missing = contract.positionals?.[0]?.label ?? "argument";
313
+ return usageFailure("CLI_MISSING_POSITIONAL", name, `${name} requires <${missing}> first.`, contract, examples);
314
+ }
315
+ const forbiddingOption = contract.noPositionalsWith?.find((option) => seenOptions.has(option));
316
+ if (forbiddingOption && positionals.length > 0) {
317
+ return usageFailure(
318
+ "CLI_EXCESS_POSITIONAL",
319
+ name,
320
+ `${forbiddingOption} does not accept positional '${positionals[0]}'.`,
321
+ contract,
322
+ examples
323
+ );
324
+ }
325
+ const minimum = contract.requiredUnless?.some((option) => seenOptions.has(option)) ? 0 : contract.minimumPositionals ?? 0;
326
+ if (positionals.length < minimum) {
327
+ const missingSpec = contract.positionals?.[positionals.length];
328
+ const missing = missingSpec?.label ?? "argument";
329
+ const valid = missingSpec?.choices?.length ? ` Choose one: ${missingSpec.choices.join("|")}.` : "";
330
+ const usage = contract.usage ? ` Usage: ${contract.usage}.` : "";
331
+ return usageFailure("CLI_MISSING_POSITIONAL", name, `missing required <${missing}>.${valid}${usage}`, contract, examples);
332
+ }
333
+ const fixed = contract.positionals ?? [];
334
+ for (let index = 0; index < Math.min(positionals.length, fixed.length); index += 1) {
335
+ const invalid = invalidPositional(positionals[index] ?? "", fixed[index]);
336
+ if (invalid) return positionalFailure(name, positionals[index] ?? "", fixed[index], contract, examples);
337
+ }
338
+ if (positionals.length > fixed.length && !contract.variadic) {
339
+ const unexpected = positionals[fixed.length] ?? "";
340
+ return usageFailure("CLI_EXCESS_POSITIONAL", name, `unexpected positional '${unexpected}'; this command accepts ${formatPositionals(contract)}.`, contract, examples);
341
+ }
342
+ if (contract.variadic) {
343
+ for (const positional of positionals.slice(fixed.length)) {
344
+ if (invalidPositional(positional, contract.variadic)) {
345
+ return positionalFailure(name, positional, contract.variadic, contract, examples);
346
+ }
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+ function publicCommandTokens() {
352
+ const tokens = [];
353
+ for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
354
+ tokens.push(name, ...contract.aliases ?? []);
355
+ }
356
+ return tokens;
357
+ }
358
+ function publicCommandNames() {
359
+ return Object.keys(PUBLIC_COMMAND_CONTRACTS);
360
+ }
361
+ function resolveCommand(token) {
362
+ const direct = PUBLIC_COMMAND_CONTRACTS[token];
363
+ if (direct) return { name: token, contract: direct };
364
+ for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
365
+ if (contract.aliases?.includes(token)) return { name, contract };
366
+ }
367
+ return null;
368
+ }
369
+ function usageFailure(code, command, message, contract, examples) {
370
+ return {
371
+ code,
372
+ command,
373
+ message: `${message} Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
374
+ userAction: `Try: ${exampleFor(command, examples)}. Inspect options: mm-harness ${command} --help`
375
+ };
376
+ }
377
+ function positionalFailure(command, received, spec, contract, examples) {
378
+ const valid = spec.validDescription ?? (spec.choices ? spec.choices.join(", ") : spec.pattern || spec.validate ? `a valid <${spec.label}>` : `<${spec.label}>`);
379
+ const suggestion = spec.choices ? closest(received, spec.choices) : void 0;
380
+ return {
381
+ code: "CLI_INVALID_POSITIONAL",
382
+ command,
383
+ message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}. Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
384
+ userAction: actionFor(received, suggestion, exampleFor(command, examples))
385
+ };
386
+ }
387
+ function invalidPositional(value2, spec) {
388
+ if (!spec) return true;
389
+ if (spec.choices && !spec.choices.includes(value2)) return true;
390
+ if (spec.validate && !spec.validate(value2)) return true;
391
+ return Boolean(spec.pattern && !spec.pattern.test(value2));
392
+ }
393
+ function formatOptions(contract) {
394
+ return Object.entries(contract.options).map(([name, spec]) => {
395
+ if (spec.kind === "boolean") return name;
396
+ const valueLabel = spec.choices?.join("|") ?? "value";
397
+ return spec.kind === "optional-value" ? `${name}[=${valueLabel}]` : `${name} <${valueLabel}>`;
398
+ }).join(", ");
399
+ }
400
+ function formatChoiceList(choices) {
401
+ if (choices.length === 1) return choices[0] ?? "";
402
+ if (choices.length === 2) return `${choices[0]} or ${choices[1]}`;
403
+ return `${choices.slice(0, -1).join(", ")}, or ${choices[choices.length - 1]}`;
404
+ }
405
+ function formatPositionals(contract) {
406
+ const fixed = (contract.positionals ?? []).map((spec) => `<${spec.label}>`);
407
+ if (contract.variadic) fixed.push(`[<${contract.variadic.label}> ...]`);
408
+ return fixed.length > 0 ? fixed.join(" ") : "no positionals";
409
+ }
410
+ function actionFor(received, suggestion, example) {
411
+ return `${suggestion ? `Did you mean '${suggestion}' instead of '${received}'? ` : ""}Try: ${example}`;
412
+ }
413
+ function exampleFor(command, examples) {
414
+ return command ? examples[command] ?? `mm-harness ${command} --help` : "mm-harness --help";
415
+ }
416
+ function closest(input, candidates) {
417
+ if (candidates.length === 0) return void 0;
418
+ const ranked = candidates.map((candidate) => ({ candidate, distance: levenshtein(input, candidate) })).sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate));
419
+ const best = ranked[0];
420
+ if (!best) return void 0;
421
+ const threshold = Math.max(2, Math.floor(Math.max(input.length, best.candidate.length) / 3));
422
+ return best.distance <= threshold ? best.candidate : void 0;
423
+ }
424
+ function levenshtein(left, right) {
425
+ const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
426
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
427
+ const current = [leftIndex];
428
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
429
+ const substitution = prior[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
430
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
431
+ }
432
+ for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
433
+ }
434
+ return prior[right.length] ?? right.length;
435
+ }
436
+ export {
437
+ PUBLIC_COMMAND_CONTRACTS,
438
+ publicCommandNames,
439
+ publicCommandTokens,
440
+ validatePublicInvocation
441
+ };