@deeeed/metamask-harness 0.6.3 → 0.7.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 (37) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/adapters/core/inject.sh +7 -2
  3. package/adapters/extension/inject.mjs +14 -2
  4. package/adapters/extension/start-watch.sh +11 -17
  5. package/adapters/manifest.json +57 -9
  6. package/adapters/mobile/inject.sh +7 -2
  7. package/adapters/mobile/lib/tmux-viewer.sh +31 -10
  8. package/adapters/mobile/open-device.sh +14 -1
  9. package/adapters/mobile/stop-metro.sh +1 -1
  10. package/adapters/mobile/yarn-setup.sh +15 -1
  11. package/adapters/shared/activate-repo-ruby.sh +124 -0
  12. package/adapters/shared/open-log-window.sh +55 -0
  13. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
  14. package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
  15. package/adapters/shared/resolve-farmslot-ports.sh +6 -104
  16. package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
  17. package/adapters/shared/resolve-slot-ports.mjs +20 -0
  18. package/adapters/shared/resolve-slot-ports.sh +110 -0
  19. package/adapters/shared/tmux-session.sh +35 -0
  20. package/dist/adapters/mobile/provision.js +25 -2
  21. package/dist/adapters/{resolve-farmslot-ports.js → resolve-slot-ports.js} +4 -2
  22. package/dist/adapters/slot-ports.js +8 -10
  23. package/dist/cli-commands.js +3 -3
  24. package/dist/commands/call.js +5 -0
  25. package/dist/commands/doctor.js +55 -2
  26. package/dist/commands/fixtures.js +5 -3
  27. package/dist/commands/launch/extension.js +18 -0
  28. package/dist/commands/launch/index.js +11 -4
  29. package/dist/commands/list-executables.js +48 -0
  30. package/dist/commands/logs.js +25 -2
  31. package/dist/commands/manifest.js +27 -7
  32. package/dist/commands/parse-args.js +2 -0
  33. package/dist/commands/run.js +3 -4
  34. package/dist/live-adapter-contract.js +30 -9
  35. package/dist/mm-harness-cli.js +16 -1
  36. package/dist/paths.js +4 -1
  37. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # tmux-session — resolve the run-owned tmux session a log-tail window belongs in.
3
+ #
4
+ # Responsibility split: the orchestrator (farmslot) NAMES the session; the harness
5
+ # only POPULATES windows inside it. The name is therefore never guessed from the
6
+ # slot number — a renamed pool or a manual run would land the window (and its
7
+ # tail -F) in the wrong place. Resolution ladder, first hit wins:
8
+ # 1. RECIPE_TMUX_SESSION — the orchestrator's explicit hand-off.
9
+ # 2. agentic-runtime.json `session` — the prepared checkout's recorded session.
10
+ # 3. the current session — but ONLY inside a tmux client (never the
11
+ # last-attached session reported outside one, which would be a foreign leak).
12
+ # Prints the resolved name (empty when none). Callers still gate on has-session
13
+ # before creating a window.
14
+
15
+ # shellcheck disable=SC2329 # sourced by leaves and by contract tests.
16
+ resolve_run_tmux_session() {
17
+ local runtime_dir="${1:-}" ctx="" session=""
18
+ if [ -n "${RECIPE_TMUX_SESSION:-}" ]; then
19
+ printf '%s\n' "$RECIPE_TMUX_SESSION"
20
+ return 0
21
+ fi
22
+ ctx="${RECIPE_RUNTIME_CONTEXT:-}"
23
+ if [ -z "$ctx" ] && [ -n "$runtime_dir" ]; then ctx="$runtime_dir/agentic-runtime.json"; fi
24
+ if [ -n "$ctx" ] && [ -f "$ctx" ] && command -v node >/dev/null 2>&1; then
25
+ session="$(RUN_TMUX_CTX="$ctx" node -e 'try{const d=JSON.parse(require("fs").readFileSync(process.env.RUN_TMUX_CTX,"utf8"));const s=d&&d.session;if(s!==undefined&&s!==null&&s!=="")process.stdout.write(String(s));}catch{}' 2>/dev/null || true)"
26
+ if [ -n "$session" ]; then
27
+ printf '%s\n' "$session"
28
+ return 0
29
+ fi
30
+ fi
31
+ if [ -n "${TMUX:-}" ]; then
32
+ tmux display-message -p '#S' 2>/dev/null || true
33
+ fi
34
+ return 0
35
+ }
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { recipeRuntimeDir } from "../../paths.js";
7
- import { resolveFarmslotPortsByRepo } from "../../../adapters/shared/resolve-farmslot-ports-core.mjs";
7
+ import { resolveSlotPortsByRepo } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
8
8
  const RUNWAY_IOS_METADATA = {
9
9
  artifactName: "ios-app-main-dev-expo",
10
10
  workflow: "expo-dev-build.yml",
@@ -13,7 +13,7 @@ const RUNWAY_IOS_METADATA = {
13
13
  fallbackRepo: "MetaMask/metamask-mobile"
14
14
  };
15
15
  function resolvePoolIdentity(target) {
16
- const out = resolveFarmslotPortsByRepo(target);
16
+ const out = resolveSlotPortsByRepo(target);
17
17
  const id = {};
18
18
  if (!out) return id;
19
19
  for (const line of String(out).split("\n")) {
@@ -95,6 +95,7 @@ async function provisionRunwayMobile(target, options) {
95
95
  log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
96
96
  bootSimulator(sim.udid ?? sim.name, options);
97
97
  execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
98
+ preapproveDeepLinkScheme(sim.udid ?? sim.name, RUNWAY_IOS_METADATA.bundleId, options);
98
99
  const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
99
100
  return {
100
101
  schemaVersion: 1,
@@ -412,6 +413,28 @@ function bootSimulator(device, options) {
412
413
  log(options, `runway: bootstatus wait for ${device} did not confirm; continuing to install`);
413
414
  }
414
415
  }
416
+ function preapproveDeepLinkScheme(device, bundleId, options) {
417
+ const scheme = process.env.IOS_DEV_CLIENT_SCHEME ?? "expo-metamask";
418
+ try {
419
+ execFileSync(
420
+ "xcrun",
421
+ [
422
+ "simctl",
423
+ "spawn",
424
+ device,
425
+ "defaults",
426
+ "write",
427
+ "com.apple.launchservices.schemeapproval",
428
+ `com.apple.CoreSimulator.CoreSimulatorBridge-->${scheme}`,
429
+ "-string",
430
+ bundleId
431
+ ],
432
+ { stdio: ["ignore", "ignore", "ignore"], timeout: 15e3 }
433
+ );
434
+ log(options, `runway: pre-approved deep-link scheme ${scheme} \u2192 ${bundleId}`);
435
+ } catch {
436
+ }
437
+ }
415
438
  function ensureSimulator(name, runtime, deviceType) {
416
439
  const existing = findSimulator(name);
417
440
  if (existing) return { name, udid: existing, created: false, runtime, deviceType };
@@ -4,11 +4,12 @@ import {
4
4
  realRepoPath,
5
5
  resolveDefaultExtensionPorts,
6
6
  resolveExtensionRuntimePorts,
7
+ resolveSlotPortsByRepo,
7
8
  resolveFarmslotPortsByRepo,
8
9
  resolveMobileRuntimeContext,
9
10
  resolveMobileRuntimePorts,
10
11
  resolveMobileSlotDefaults
11
- } from "../../adapters/shared/resolve-farmslot-ports-core.mjs";
12
+ } from "../../adapters/shared/resolve-slot-ports-core.mjs";
12
13
  export {
13
14
  formatKvLines,
14
15
  inferSlotSuffix,
@@ -18,5 +19,6 @@ export {
18
19
  resolveFarmslotPortsByRepo,
19
20
  resolveMobileRuntimeContext,
20
21
  resolveMobileRuntimePorts,
21
- resolveMobileSlotDefaults
22
+ resolveMobileSlotDefaults,
23
+ resolveSlotPortsByRepo
22
24
  };
@@ -5,10 +5,10 @@ import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.j
5
5
  import { recipeRuntimeDir } from "../paths.js";
6
6
  import {
7
7
  resolveDefaultExtensionPorts,
8
- resolveFarmslotPortsByRepo,
8
+ resolveSlotPortsByRepo,
9
9
  resolveMobileRuntimeContext,
10
10
  resolveMobileSlotDefaults
11
- } from "./resolve-farmslot-ports.js";
11
+ } from "./resolve-slot-ports.js";
12
12
  function applyKVLines(output, overwrite) {
13
13
  for (const line of output.split("\n")) {
14
14
  const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
@@ -41,7 +41,7 @@ function applyKVLines(output, overwrite) {
41
41
  function resolveMobileSlotPorts(target) {
42
42
  const ctxOut = resolveMobileRuntimeContext(target);
43
43
  if (ctxOut?.trim()) applyKVLines(ctxOut, true);
44
- const poolOut = resolveFarmslotPortsByRepo(target);
44
+ const poolOut = resolveSlotPortsByRepo(target);
45
45
  if (poolOut?.trim()) applyKVLines(poolOut, false);
46
46
  const defOut = resolveMobileSlotDefaults(target);
47
47
  if (defOut?.trim()) applyKVLines(defOut, false);
@@ -58,14 +58,12 @@ function resolveExtensionSlotPorts(target) {
58
58
  process.env["WATCHER_PORT"] = dev;
59
59
  process.env["RECIPE_WATCHER_PORT"] = dev;
60
60
  }
61
- if (process.env["CDP_PORT"]) return;
62
- const poolOut = resolveFarmslotPortsByRepo(target);
63
- if (poolOut?.trim()) {
64
- applyKVLines(poolOut, true);
65
- return;
61
+ if (!process.env["CDP_PORT"] || !process.env["WATCHER_PORT"]) {
62
+ const poolOut = resolveSlotPortsByRepo(target);
63
+ if (poolOut?.trim()) applyKVLines(poolOut, false);
64
+ const defOut = resolveDefaultExtensionPorts(target);
65
+ if (defOut?.trim()) applyKVLines(defOut, false);
66
66
  }
67
- const defOut = resolveDefaultExtensionPorts(target);
68
- if (defOut?.trim()) applyKVLines(defOut, false);
69
67
  }
70
68
  function stopExtensionWatcher(target) {
71
69
  const runtimeAbs = path.join(target, recipeRuntimeDir());
@@ -11,11 +11,11 @@ const SPEC = {
11
11
  { name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
12
12
  { name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
13
13
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
14
- { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f"] },
14
+ { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
15
15
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
16
16
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
17
- { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir"] },
18
- { name: "run", desc: "Execute a proof recipe", args: ["recipe.json"] },
17
+ { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port"] },
18
+ { name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
19
19
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
20
20
  { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
21
21
  { name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
@@ -14,6 +14,7 @@ import {
14
14
  runtimeOptionsFromCli,
15
15
  usageError
16
16
  } from "./parse-args.js";
17
+ import { handleListExecutables } from "./list-executables.js";
17
18
  import {
18
19
  emitHealViolation,
19
20
  executeWithHealBounds,
@@ -24,6 +25,10 @@ import {
24
25
  validateRecipeAdapterAware
25
26
  } from "./run-engine.js";
26
27
  async function handleCall(argv) {
28
+ if (argv.includes("--list")) {
29
+ const { options: options2 } = parseArgs(argv, "call");
30
+ return handleListExecutables("call", options2);
31
+ }
27
32
  if (argv.length > 0 && argv[0].startsWith("--")) {
28
33
  const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
29
34
  console.error(message);
@@ -7,7 +7,7 @@ import { assertAdapter, runnerDir } from "../paths.js";
7
7
  import { getAdapterSurface } from "../adapters/surface.js";
8
8
  import { loadActionManifest, validateManifest } from "../manifest.js";
9
9
  import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
10
- import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
10
+ import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
11
11
  import {
12
12
  actionManifestPathOption,
13
13
  applyRuntimeDirOption,
@@ -15,10 +15,24 @@ import {
15
15
  optionString,
16
16
  targetPath
17
17
  } from "./parse-args.js";
18
+ function applyDoctorRuntimePorts(options) {
19
+ const cdpPort = optionString(options, "cdpPort");
20
+ if (cdpPort) {
21
+ process.env.CDP_PORT = cdpPort;
22
+ process.env.RECIPE_CDP_PORT = cdpPort;
23
+ }
24
+ const watcherPort = optionString(options, "watcherPort") ?? optionString(options, "port") ?? optionString(options, "metroPort");
25
+ if (watcherPort) {
26
+ process.env.WATCHER_PORT = watcherPort;
27
+ process.env.METRO_PORT = watcherPort;
28
+ process.env.RECIPE_WATCHER_PORT = watcherPort;
29
+ }
30
+ }
18
31
  async function handleDoctor({ options }) {
19
32
  applyRuntimeDirOption(options);
20
33
  const target = targetPath(options);
21
34
  const json = optionFlag(options, "json");
35
+ const expectLive = optionFlag(options, "expectLive");
22
36
  const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
23
37
  const adapter = explicitAdapter ?? detectAdapter(target);
24
38
  if (!adapter) {
@@ -40,11 +54,14 @@ async function handleDoctor({ options }) {
40
54
  try {
41
55
  const surface = getAdapterSurface(adapter);
42
56
  surface.resolveSlotPorts(target);
57
+ applyDoctorRuntimePorts(options);
43
58
  runtime = await surface.runtimeStatus(target);
44
59
  } catch {
45
60
  }
46
61
  const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
47
- if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros }, null, 2));
62
+ const capture = captureHelperHealth();
63
+ if (expectLive) return emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, json);
64
+ if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
48
65
  else {
49
66
  const out = (style, text) => color(style, text, { stream: process.stdout });
50
67
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
@@ -67,9 +84,45 @@ async function handleDoctor({ options }) {
67
84
  );
68
85
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
69
86
  }
87
+ if (capture) {
88
+ console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
89
+ if (capture.status !== "pass") {
90
+ if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
91
+ console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
92
+ }
93
+ }
70
94
  }
71
95
  return result.status === "pass" ? 0 : 1;
72
96
  }
97
+ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, json) {
98
+ const live = runtime?.decision === "ready";
99
+ if (json) {
100
+ console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
101
+ return live ? EXIT.ok : EXIT.runtime;
102
+ }
103
+ const out = (style, text) => color(style, text, { stream: process.stdout });
104
+ if (live) {
105
+ console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
106
+ return EXIT.ok;
107
+ }
108
+ const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
109
+ console.error(`${out("err", "not-live")} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
110
+ for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
111
+ console.error(` Next: ${getAdapterSurface(adapter).hints.relaunch}`);
112
+ return EXIT.runtime;
113
+ }
114
+ function captureHelperHealth() {
115
+ if (process.platform !== "darwin") return null;
116
+ const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
117
+ try {
118
+ const out = execFileSync(bin, ["doctor", "--json"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
119
+ const parsed = JSON.parse(out);
120
+ const failing = (parsed.checks ?? []).filter((c) => c.required === true && c.ok === false).map((c) => c.name ?? c.id ?? "check");
121
+ return { status: parsed.ok === true && failing.length === 0 ? "pass" : "warn", failing };
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
73
126
  function detectOrphanMetros(target, livePort) {
74
127
  try {
75
128
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runnerDir, walletFixturePath } from "../paths.js";
4
4
  import { getAdapterSurface } from "../adapters/surface.js";
5
- import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from "./shared.js";
5
+ import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
6
6
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
7
7
  const RECOVERABLE_SETUP_WALLET_PATTERNS = [
8
8
  "CDP not reachable",
@@ -49,13 +49,15 @@ async function handleFixtures(argv, deps) {
49
49
  return exitCode;
50
50
  }
51
51
  const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
52
+ process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
53
+ `);
52
54
  let status;
53
55
  if (adapter === "mobile") {
54
56
  const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
55
57
  const previousAppRoot = process.env.APP_ROOT;
56
58
  process.env.APP_ROOT = target;
57
59
  try {
58
- let result = spawnScript(setupWalletSh, ["--fixture", fixturePath], target, json);
60
+ let result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
59
61
  if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env["RECIPE_SETUP_WALLET_RETRIED"]) {
60
62
  process.env["RECIPE_SETUP_WALLET_RETRIED"] = "1";
61
63
  if (!json) process.stderr.write(" setup-wallet: recoverable failure \u2014 restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n");
@@ -63,7 +65,7 @@ async function handleFixtures(argv, deps) {
63
65
  const platform = str(options, "platform") ?? process.env["MOBILE_PLATFORM"] ?? "ios";
64
66
  const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: "auto", clearMetro: true });
65
67
  if (relaunchResult.status === 0) {
66
- result = spawnScript(setupWalletSh, ["--fixture", fixturePath], target, json);
68
+ result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
67
69
  }
68
70
  }
69
71
  status = result.status === 0 ? "pass" : "fail";
@@ -2,10 +2,26 @@ import { execFileSync } from "node:child_process";
2
2
  import http from "node:http";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
5
6
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
6
7
  import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
7
8
  import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
8
9
  import { spawnScriptStreaming } from "../shared.js";
10
+ function extensionDepsBlock(target) {
11
+ if (!fs.existsSync(path.join(target, "package.json"))) return null;
12
+ const deps = depsCheck(target);
13
+ const userAction = "yarn install --immutable # then re-run: mm-harness launch";
14
+ if (deps.status === "missing") {
15
+ return { message: "extension dependencies are not installed (no yarn install-state markers).", userAction };
16
+ }
17
+ if (deps.status === "stale") {
18
+ return {
19
+ message: "extension dependencies are stale (package.json/yarn.lock changed since the last install) \u2014 webpack would crash mid-build.",
20
+ userAction
21
+ };
22
+ }
23
+ return null;
24
+ }
9
25
  async function launchExtension(target, tier, wantWatch) {
10
26
  if (wantWatch) {
11
27
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
@@ -90,7 +106,9 @@ async function extensionRebuild(target) {
90
106
  return result;
91
107
  }
92
108
  export {
109
+ extensionDepsBlock,
93
110
  extensionReattach,
94
111
  extensionRebuild,
112
+ extensionRuntimeReusable,
95
113
  launchExtension
96
114
  };
@@ -23,7 +23,7 @@ import {
23
23
  newHealState,
24
24
  parseHeal
25
25
  } from "../../heal-bounds.js";
26
- import { launchExtension } from "./extension.js";
26
+ import { extensionDepsBlock, extensionRuntimeReusable, launchExtension } from "./extension.js";
27
27
  import { launchMobile } from "./mobile.js";
28
28
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
29
29
  "build",
@@ -32,9 +32,7 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
32
32
  "sidepanel",
33
33
  "fullscreen",
34
34
  "runway",
35
- "json",
36
- "jsonStream",
37
- "yes"
35
+ "json"
38
36
  ]);
39
37
  async function handleLaunch(argv) {
40
38
  const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
@@ -111,6 +109,15 @@ async function handleLaunch(argv) {
111
109
  if (json) liveArgs.push("--json");
112
110
  return handleHarness(liveArgs);
113
111
  }
112
+ if (adapter === "extension") {
113
+ const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
114
+ if (willBuild) {
115
+ const depsBlock = extensionDepsBlock(target);
116
+ if (depsBlock) {
117
+ return usageOut(json, "launch", depsBlock.message, depsBlock.userAction);
118
+ }
119
+ }
120
+ }
114
121
  let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
115
122
  if (attempt.status === 0) {
116
123
  return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
@@ -0,0 +1,48 @@
1
+ import { loadActionManifest } from "../manifest.js";
2
+ import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
3
+ import { EXIT } from "./shared.js";
4
+ import { optionFlag, optionString, resolveAdapter } from "./parse-args.js";
5
+ import { resolveMetaMaskLibrarySources } from "./run-engine.js";
6
+ const finalSegment = (name) => name.split(".").pop() ?? name;
7
+ async function listFlowIds() {
8
+ try {
9
+ const sources = await resolveMetaMaskLibrarySources(void 0);
10
+ if (!sources || sources.length === 0) return [];
11
+ const harness = await importRecipeHarness();
12
+ const resolution = await harness.loadRecipeLibraries(sources);
13
+ return [...resolution.flows.keys()].sort();
14
+ } catch {
15
+ return [];
16
+ }
17
+ }
18
+ async function handleListExecutables(command, options) {
19
+ const json = optionFlag(options, "json");
20
+ const { adapter } = resolveAdapter(options);
21
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
22
+ const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
23
+ const names = getRecipeActionManifestActionNames(manifest).slice().sort();
24
+ const shortCounts = /* @__PURE__ */ new Map();
25
+ for (const name of names) shortCounts.set(finalSegment(name), (shortCounts.get(finalSegment(name)) ?? 0) + 1);
26
+ const actions = names.map((name) => {
27
+ const short = finalSegment(name);
28
+ return { name, short: shortCounts.get(short) === 1 ? short : null };
29
+ });
30
+ const flows = await listFlowIds();
31
+ if (json) {
32
+ console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions, flows }, null, 2));
33
+ return EXIT.ok;
34
+ }
35
+ console.log(`invocable for ${adapter}:`);
36
+ console.log(` actions (mm-harness call <name> \u2014 short or full):`);
37
+ for (const entry of actions) {
38
+ console.log(entry.short ? ` ${entry.short} (${entry.name})` : ` ${entry.name} (full name only \u2014 ambiguous short)`);
39
+ }
40
+ if (flows.length > 0) {
41
+ console.log(` flows (mm-harness run <flow>):`);
42
+ for (const flow of flows) console.log(` ${flow}`);
43
+ }
44
+ return EXIT.ok;
45
+ }
46
+ export {
47
+ handleListExecutables
48
+ };
@@ -2,8 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runnerDir } from "../paths.js";
4
4
  import { getAdapterSurface } from "../adapters/surface.js";
5
- import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnInherit, str, targetOf, usageOut } from "./shared.js";
6
- const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json"]);
5
+ import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "./shared.js";
6
+ const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json", "window"]);
7
7
  async function handleLogs(argv) {
8
8
  const { options } = parseFlags(argv, LOGS_BOOLEANS);
9
9
  const json = flag(options, "json");
@@ -43,6 +43,29 @@ async function handleLogs(argv) {
43
43
  surface.hints.launch
44
44
  );
45
45
  }
46
+ if (flag(options, "window")) {
47
+ surface.resolveSlotPorts(target);
48
+ const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
49
+ const windowName = adapter === "mobile" ? `metro-${port}` : `webpack-${port}`;
50
+ const devLog = logSources[0].path;
51
+ const leaf = path.join(runnerDir, "adapters/shared/open-log-window.sh");
52
+ const result = spawnScript(
53
+ leaf,
54
+ ["--window", windowName, "--log", devLog, "--runtime-dir", path.dirname(devLog)],
55
+ target,
56
+ json
57
+ );
58
+ if (json) {
59
+ console.log(
60
+ JSON.stringify(
61
+ { schemaVersion: 1, command: "logs", action: "window", adapter, window: windowName, logFile: devLog, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
62
+ null,
63
+ 2
64
+ )
65
+ );
66
+ }
67
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
68
+ }
46
69
  if (json) {
47
70
  console.log(JSON.stringify({ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: EXIT.ok }, null, 2));
48
71
  return EXIT.ok;
@@ -1,4 +1,5 @@
1
1
  import { loadActionManifest, validateManifest } from "../manifest.js";
2
+ import { EXIT } from "./shared.js";
2
3
  import {
3
4
  actionManifestPathOption,
4
5
  isRecord,
@@ -19,9 +20,22 @@ async function handleActions({ options }) {
19
20
  const { adapter } = resolveAdapter(options);
20
21
  const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
21
22
  await validateManifest(manifest);
23
+ const json = optionFlag(options, "json");
22
24
  const action = optionString(options, "action");
23
- const actions = describeManifestActions(manifest, action);
24
- if (optionFlag(options, "json")) {
25
+ const all = describeManifestActions(manifest);
26
+ const actions = action ? fuzzyResolveActions(all, action) : all;
27
+ if (action && actions.length === 0) {
28
+ const message = `no action matches "${action}" for the ${adapter} adapter.`;
29
+ const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
30
+ if (json) {
31
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
32
+ } else {
33
+ console.error(`\u2717 mm-harness actions: ${message}
34
+ Next: ${userAction}`);
35
+ }
36
+ return EXIT.usage;
37
+ }
38
+ if (json) {
25
39
  console.log(JSON.stringify({ adapter, actions }, null, 2));
26
40
  } else {
27
41
  for (const entry of actions) {
@@ -31,7 +45,15 @@ async function handleActions({ options }) {
31
45
  }
32
46
  return 0;
33
47
  }
34
- function describeManifestActions(manifest, filterAction) {
48
+ function fuzzyResolveActions(entries, query) {
49
+ const exactFull = entries.filter((e) => e.name === query);
50
+ if (exactFull.length > 0) return exactFull;
51
+ const finalSegment = (name) => name.split(".").pop() ?? name;
52
+ const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
53
+ if (exactSegment.length > 0) return exactSegment;
54
+ return entries.filter((e) => finalSegment(e.name).includes(query));
55
+ }
56
+ function describeManifestActions(manifest) {
35
57
  const manifestRecord = isRecord(manifest) ? manifest : {};
36
58
  const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
37
59
  const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
@@ -45,12 +67,10 @@ function describeManifestActions(manifest, filterAction) {
45
67
  }
46
68
  return [];
47
69
  }) : [];
48
- const entries = [
70
+ return [
49
71
  ...official.map((name) => describeManifestAction(name, "official", metadata[name])),
50
72
  ...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
51
- ].filter((entry) => !filterAction || entry.name === filterAction);
52
- if (filterAction && entries.length === 0) throw new Error(`Action not found in manifest: ${filterAction}`);
53
- return entries;
73
+ ];
54
74
  }
55
75
  function describeManifestAction(name, kind, metadata) {
56
76
  const record = isRecord(metadata) ? metadata : {};
@@ -23,10 +23,12 @@ function parseArgs(argv, command) {
23
23
  "startWatch",
24
24
  "record",
25
25
  "plan",
26
+ "list",
26
27
  "raw",
27
28
  "fix",
28
29
  "force",
29
30
  "resolveOnly",
31
+ "expectLive",
30
32
  "help"
31
33
  ]);
32
34
  for (let i = 0; i < argv.length; i += 1) {
@@ -3,13 +3,11 @@ import path from "node:path";
3
3
  import { walletFixturePath } from "../paths.js";
4
4
  import { EXIT } from "./shared.js";
5
5
  import {
6
- adapterOption,
7
6
  optionFlag,
8
7
  optionString,
9
8
  requiredOption,
10
9
  resolveAdapter,
11
10
  runtimeOptionsFromCli,
12
- targetPath,
13
11
  usageError
14
12
  } from "./parse-args.js";
15
13
  import {
@@ -21,13 +19,14 @@ import {
21
19
  runRecipe,
22
20
  validateRunRecipeStatic
23
21
  } from "./run-engine.js";
22
+ import { handleListExecutables } from "./list-executables.js";
24
23
  async function handleRun({ positional, options }) {
24
+ if (optionFlag(options, "list")) return handleListExecutables("run", options);
25
25
  const targetRecipe = positional[0];
26
26
  if (!targetRecipe) throw usageError("run requires <recipe.json>.");
27
27
  if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
28
- const adapter = adapterOption(options);
28
+ const { adapter, target } = resolveAdapter(options);
29
29
  const json = optionFlag(options, "json");
30
- const target = targetPath(options);
31
30
  const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
32
31
  const prepared = await prepareHeal(adapter, target, options, json);
33
32
  if (typeof prepared === "number") return prepared;
@@ -119,18 +119,39 @@ function runProcess(command, args, options) {
119
119
  });
120
120
  });
121
121
  }
122
- function commandFor(file) {
122
+ function commandFor(file, projectRoot, platform) {
123
123
  if (file.endsWith(".sh")) return { command: "bash", args: [file] };
124
- if (file.endsWith(".mjs") || file.endsWith(".js")) {
125
- if (!importsSourceTypescript(file)) return { command: process.execPath, args: [file] };
124
+ const needsTsx = platform === "core" || importsSourceTypescript(file);
125
+ if (!needsTsx && (file.endsWith(".mjs") || file.endsWith(".js"))) {
126
+ return { command: process.execPath, args: [file] };
127
+ }
128
+ const tsxBin = resolveTsxBin(projectRoot);
129
+ if (!tsxBin) {
130
+ const where = projectRoot ?? "the checkout";
131
+ const error = new Error(
132
+ `this action runs TypeScript from the checkout but no tsx runtime was found.
133
+ Next: run 'yarn install' in ${where} (tsx is a dev dependency of the checkout); if package imports still fail, run 'yarn build' there`
134
+ );
135
+ error.exitCode = 2;
136
+ throw error;
126
137
  }
127
- const localTsx = path.join(runnerDir, "node_modules/.bin/tsx");
128
- const tsxBin = process.env.TSX_BIN || (existsSync(localTsx) ? localTsx : path.join(
129
- resolveRequiredLocalProtocolRoot("TypeScript live adapter execution"),
130
- "node_modules/.bin/tsx"
131
- ));
132
138
  return { command: tsxBin, args: [file] };
133
139
  }
140
+ function resolveTsxBin(projectRoot) {
141
+ if (process.env.TSX_BIN) return process.env.TSX_BIN;
142
+ const candidates = [];
143
+ if (projectRoot) candidates.push(path.join(projectRoot, "node_modules/.bin/tsx"));
144
+ candidates.push(path.join(runnerDir, "node_modules/.bin/tsx"));
145
+ for (const candidate of candidates) {
146
+ if (existsSync(candidate)) return candidate;
147
+ }
148
+ try {
149
+ const protocolTsx = path.join(resolveRequiredLocalProtocolRoot("TypeScript live adapter execution"), "node_modules/.bin/tsx");
150
+ if (existsSync(protocolTsx)) return protocolTsx;
151
+ } catch {
152
+ }
153
+ return null;
154
+ }
134
155
  function importsSourceTypescript(file) {
135
156
  return importsSourceTypescriptFrom(file, /* @__PURE__ */ new Set());
136
157
  }
@@ -201,7 +222,7 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
201
222
  };
202
223
  await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
203
224
  `);
204
- const command = commandFor(script);
225
+ const command = commandFor(script, context.projectRoot, platform);
205
226
  const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
206
227
  const result = await runProcess(command.command, [...command.args, inputPath], {
207
228
  cwd: context.projectRoot,