@deeeed/metamask-harness 0.14.7 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +81 -6
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +4 -2
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +73 -34
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +21 -10
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +3 -3
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +10 -4
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +2 -2
  17. package/adapters/extension/stop-viewers.sh +0 -0
  18. package/adapters/extension/verify.sh +19 -2
  19. package/adapters/extension/wallet-fixture-state.cjs +14 -1
  20. package/adapters/manifest.json +19 -3
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  22. package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
  23. package/adapters/mobile/cleanup.sh +0 -0
  24. package/adapters/mobile/inject.sh +0 -0
  25. package/adapters/mobile/launch-metro.cjs +74 -0
  26. package/adapters/mobile/lib/metro-listener.sh +0 -0
  27. package/adapters/mobile/lib/tmux-viewer.sh +4 -4
  28. package/adapters/mobile/open-device.sh +3 -1
  29. package/adapters/mobile/prewarm-bundle.sh +0 -0
  30. package/adapters/mobile/start-metro.sh +12 -18
  31. package/adapters/mobile/stop-metro.sh +1 -0
  32. package/adapters/mobile/verify.sh +5 -5
  33. package/adapters/mobile/wait-for-bridge.sh +0 -0
  34. package/adapters/mobile/yarn-setup.sh +0 -0
  35. package/adapters/shared/activate-repo-node.sh +0 -0
  36. package/adapters/shared/activate-repo-ruby.sh +0 -0
  37. package/adapters/shared/cli-ux.sh +0 -0
  38. package/adapters/shared/ensure-runner-deps.sh +0 -0
  39. package/adapters/shared/harness-path.sh +0 -0
  40. package/adapters/shared/hash-helpers.sh +0 -0
  41. package/adapters/shared/install-repo-deps.sh +29 -0
  42. package/adapters/shared/json-field.sh +0 -0
  43. package/adapters/shared/open-debug.mjs +35 -4
  44. package/adapters/shared/open-log-window.sh +1 -1
  45. package/adapters/shared/reap-checkout-metros.sh +0 -0
  46. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  47. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  48. package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
  49. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  50. package/adapters/shared/resolve-slot-ports.sh +21 -17
  51. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  52. package/adapters/shared/tmux-session.sh +0 -5
  53. package/adapters/shared/tmux-viewer.sh +7 -2
  54. package/dist/adapters/core/surface.js +3 -0
  55. package/dist/adapters/extension/ensure-ready.js +0 -7
  56. package/dist/adapters/extension/runtime-decision.js +93 -3
  57. package/dist/adapters/extension/surface.js +17 -6
  58. package/dist/adapters/mobile/metro-env.js +67 -0
  59. package/dist/adapters/mobile/perps-env.js +101 -0
  60. package/dist/adapters/mobile/prepare.js +32 -14
  61. package/dist/adapters/mobile/runtime-decision.js +21 -2
  62. package/dist/adapters/mobile/source-freshness.js +116 -0
  63. package/dist/adapters/mobile/surface.js +3 -0
  64. package/dist/adapters/resolve-slot-ports.js +4 -0
  65. package/dist/adapters/slot-ports.js +131 -49
  66. package/dist/adapters.js +7 -2
  67. package/dist/checkout-lock.js +72 -0
  68. package/dist/cli.js +2 -0
  69. package/dist/commands/call.js +91 -54
  70. package/dist/commands/check.js +266 -46
  71. package/dist/commands/checklist.js +136 -0
  72. package/dist/commands/debug.js +21 -2
  73. package/dist/commands/doctor.js +187 -12
  74. package/dist/commands/fixtures.js +177 -42
  75. package/dist/commands/launch/extension.js +6 -1
  76. package/dist/commands/launch/index.js +54 -22
  77. package/dist/commands/logs.js +24 -4
  78. package/dist/commands/run-engine.js +223 -7
  79. package/dist/commands/run.js +67 -50
  80. package/dist/commands/shared.js +86 -5
  81. package/dist/commands/stop.js +1 -2
  82. package/dist/doctor.js +39 -17
  83. package/dist/harness.js +5 -2
  84. package/dist/heal-bounds.js +1 -1
  85. package/dist/mm-harness-cli.js +37 -9
  86. package/dist/runner.js +35 -5
  87. package/dist/runtime-context.js +228 -0
  88. package/docs/CLI-SPEC.md +15 -11
  89. package/docs/MENTAL-MODEL.md +6 -6
  90. package/library/actions/extension/perps/perps.mjs +29 -8
  91. package/library/actions/extension/platform/cdp.mjs +128 -28
  92. package/library/actions/extension/ui/navigate.mjs +1 -1
  93. package/library/actions/mobile/perps/perps.mjs +13 -1
  94. package/library/actions/mobile/platform/bridge.mjs +302 -29
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
  96. package/library/manifests/extension.action-manifest.json +32 -12
  97. package/library/manifests/mobile.action-manifest.json +25 -3
  98. package/package.json +5 -5
  99. package/scripts/completions.sh +7 -4
  100. package/scripts/install-completions.sh +0 -0
@@ -1,12 +1,12 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.js";
5
4
  import { recipeRuntimeDir } from "../paths.js";
6
5
  import {
7
6
  resolveDefaultExtensionPorts,
8
- resolveSlotPortsByRepo,
7
+ resolveExtensionRuntimeContext,
9
8
  resolveMobileRuntimeContext,
9
+ resolveSlotPortsByRepo,
10
10
  resolveMobileSlotDefaults
11
11
  } from "./resolve-slot-ports.js";
12
12
  function applyKVLines(output, overwrite) {
@@ -54,51 +54,34 @@ function resolveMobileSlotPorts(target) {
54
54
  if (defOut?.trim()) applyKVLines(defOut, false);
55
55
  }
56
56
  function resolveExtensionSlotPorts(target) {
57
- const contextPath = resolveRuntimeContextPath(target);
58
- const cdp = readRuntimeContextField(contextPath, "cdpPort");
59
- if (cdp) {
60
- process.env["CDP_PORT"] = cdp;
61
- process.env["RECIPE_CDP_PORT"] = cdp;
62
- }
63
- const dev = readRuntimeContextField(contextPath, "devServerPort");
64
- if (dev) {
65
- process.env["WATCHER_PORT"] = dev;
66
- process.env["RECIPE_WATCHER_PORT"] = dev;
67
- }
68
- if (!process.env["CDP_PORT"] || !process.env["WATCHER_PORT"]) {
69
- const poolOut = resolveSlotPortsByRepo(target);
70
- if (poolOut?.trim()) applyKVLines(poolOut, false);
71
- const defOut = resolveDefaultExtensionPorts(target);
72
- if (defOut?.trim()) applyKVLines(defOut, false);
73
- }
57
+ const resolved = path.resolve(target);
58
+ const ctxOut = resolveExtensionRuntimeContext(resolved);
59
+ if (ctxOut?.trim()) applyKVLines(ctxOut, true);
60
+ const poolOut = resolveSlotPortsByRepo(resolved);
61
+ if (poolOut?.trim()) applyKVLines(poolOut, false);
62
+ const defOut = resolveDefaultExtensionPorts(resolved);
63
+ if (defOut?.trim()) applyKVLines(defOut, false);
74
64
  }
75
- function stopExtensionWatcher(target) {
76
- const runtimeAbs = path.join(target, recipeRuntimeDir());
65
+ function extensionWatcherPids(target) {
66
+ const resolved = path.resolve(target);
67
+ const runtimeAbs = path.join(resolved, recipeRuntimeDir());
77
68
  const webpackPidFile = path.join(runtimeAbs, "recipe-harness-webpack.pid");
78
- let signalled = 0;
69
+ const ownedPids = /* @__PURE__ */ new Set();
79
70
  try {
80
71
  const pid = fs.readFileSync(webpackPidFile, "utf8").trim();
81
- if (/^\d+$/u.test(pid)) {
82
- try {
83
- process.kill(Number(pid), "SIGTERM");
84
- signalled += 1;
85
- } catch {
86
- }
87
- }
88
- fs.rmSync(webpackPidFile, { force: true });
72
+ if (/^\d+$/u.test(pid) && processAlive(Number(pid))) ownedPids.add(Number(pid));
89
73
  } catch {
90
74
  }
91
75
  try {
92
76
  const psOut = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
93
- const orphanPids = [];
94
77
  for (const line of psOut.split("\n")) {
95
78
  const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
96
79
  if (!match) continue;
97
80
  const [, pidStr, cmd] = match;
98
81
  const isWatcher = cmd.includes("yarn start") || cmd.includes("webpack --watch") || cmd.includes("development/webpack/launch.ts --watch");
99
82
  if (!isWatcher) continue;
100
- if (cmd.includes(target)) {
101
- orphanPids.push(Number(pidStr));
83
+ if (cmd.includes(resolved)) {
84
+ ownedPids.add(Number(pidStr));
102
85
  continue;
103
86
  }
104
87
  try {
@@ -106,35 +89,134 @@ function stopExtensionWatcher(target) {
106
89
  encoding: "utf8",
107
90
  timeout: 2e3
108
91
  });
109
- if (cwd.split("\n").some((l) => l.startsWith("n") && l.slice(1) === target)) {
110
- orphanPids.push(Number(pidStr));
92
+ if (cwd.split("\n").some((l) => l.startsWith("n") && l.slice(1) === resolved)) {
93
+ ownedPids.add(Number(pidStr));
111
94
  }
112
95
  } catch {
113
96
  }
114
97
  }
115
- if (orphanPids.length > 0) {
116
- for (const pid of orphanPids) {
117
- try {
118
- process.kill(pid, "SIGTERM");
119
- } catch {
120
- }
98
+ } catch {
99
+ }
100
+ return [...ownedPids];
101
+ }
102
+ function isExtensionWatcherLive(target) {
103
+ return extensionWatcherPids(target).length > 0;
104
+ }
105
+ function stopExtensionWatcher(target) {
106
+ const runtimeAbs = path.join(path.resolve(target), recipeRuntimeDir());
107
+ const webpackPidFile = path.join(runtimeAbs, "recipe-harness-webpack.pid");
108
+ const ownedPids = extensionWatcherPids(target);
109
+ for (const pid of ownedPids) {
110
+ try {
111
+ process.kill(pid, "SIGTERM");
112
+ } catch {
113
+ }
114
+ }
115
+ fs.rmSync(webpackPidFile, { force: true });
116
+ if (ownedPids.length > 0) {
117
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
118
+ for (const pid of ownedPids) {
119
+ try {
120
+ process.kill(pid, "SIGKILL");
121
+ } catch {
121
122
  }
122
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
123
- for (const pid of orphanPids) {
124
- try {
125
- process.kill(pid, "SIGKILL");
126
- } catch {
127
- }
123
+ }
124
+ }
125
+ return ownedPids.length;
126
+ }
127
+ function stopExtensionRuntime(target) {
128
+ const resolved = path.resolve(target);
129
+ const runtimeAbs = path.join(resolved, recipeRuntimeDir());
130
+ const profile = path.resolve(
131
+ process.env.CHROME_USER_DATA_DIR || path.join(runtimeAbs, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile")
132
+ );
133
+ let signalled = stopExtensionWatcher(resolved);
134
+ const ownedBrowserPids = /* @__PURE__ */ new Set();
135
+ try {
136
+ const psOut = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
137
+ for (const line of psOut.split("\n")) {
138
+ const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
139
+ if (!match) continue;
140
+ const pid = Number(match[1]);
141
+ if (pid !== process.pid && commandHasExactArg(match[2], "--user-data-dir", profile)) {
142
+ ownedBrowserPids.add(pid);
128
143
  }
129
- signalled += orphanPids.length;
130
144
  }
131
145
  } catch {
132
146
  }
133
- return signalled;
147
+ for (const pid of ownedBrowserPids) {
148
+ try {
149
+ process.kill(pid, "SIGTERM");
150
+ signalled += 1;
151
+ } catch {
152
+ }
153
+ }
154
+ if (ownedBrowserPids.size > 0) {
155
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
156
+ for (const pid of ownedBrowserPids) {
157
+ try {
158
+ process.kill(pid, 0);
159
+ process.kill(pid, "SIGKILL");
160
+ } catch {
161
+ }
162
+ }
163
+ }
164
+ let removedMarkers = 0;
165
+ for (const name of [
166
+ "recipe-harness-webpack.pid",
167
+ "webpack.pid",
168
+ "browser.pid",
169
+ "chromium.pid",
170
+ "chrome.pid",
171
+ "launcher.pid",
172
+ "sandbox.lock",
173
+ "recipe.lock"
174
+ ]) {
175
+ const marker = path.join(runtimeAbs, name);
176
+ if (!fs.existsSync(marker)) continue;
177
+ const pid = markerPid(marker);
178
+ if (pid !== null && processAlive(pid) && !ownedBrowserPids.has(pid)) continue;
179
+ fs.rmSync(marker, { force: true });
180
+ removedMarkers += 1;
181
+ }
182
+ return { signalled, browserProcesses: ownedBrowserPids.size, removedMarkers };
183
+ }
184
+ function markerPid(file) {
185
+ try {
186
+ const raw = fs.readFileSync(file, "utf8").trim();
187
+ const value = raw.startsWith("{") ? JSON.parse(raw).pid : raw;
188
+ const pid = Number(value);
189
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function processAlive(pid) {
195
+ try {
196
+ process.kill(pid, 0);
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ function commandHasExactArg(command, flag, value) {
203
+ const escapedFlag = escapeRegex(flag);
204
+ const escapedValue = escapeRegex(value);
205
+ return [
206
+ new RegExp(`(?:^|\\s)${escapedFlag}=${escapedValue}(?=\\s|$)`, "u"),
207
+ new RegExp(`(?:^|\\s)${escapedFlag}="${escapedValue}"(?=\\s|$)`, "u"),
208
+ new RegExp(`(?:^|\\s)${escapedFlag}='${escapedValue}'(?=\\s|$)`, "u"),
209
+ new RegExp(`(?:^|\\s)${escapedFlag}\\s+${escapedValue}(?=\\s|$)`, "u")
210
+ ].some((pattern) => pattern.test(command));
211
+ }
212
+ function escapeRegex(value) {
213
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
134
214
  }
135
215
  export {
136
216
  applyKVLines,
217
+ isExtensionWatcherLive,
137
218
  resolveExtensionSlotPorts,
138
219
  resolveMobileSlotPorts,
220
+ stopExtensionRuntime,
139
221
  stopExtensionWatcher
140
222
  };
package/dist/adapters.js CHANGED
@@ -465,10 +465,14 @@ function createMetaMaskUiTransport(platform, harness) {
465
465
  if (live) return live.output;
466
466
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
467
467
  }
468
- return base.execute(action, node, context);
468
+ return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node, platform) : node, context);
469
469
  }
470
470
  };
471
471
  }
472
+ function normalizeUiWaitNode(node, adapter) {
473
+ if (adapter !== "extension" || node.expected !== void 0 || typeof node.visible !== "boolean") return node;
474
+ return { ...node, expected: node.visible ? "visible" : "absent" };
475
+ }
472
476
  function createMetaMaskAdapters(adapter) {
473
477
  const platform = adapter;
474
478
  const adapters = [
@@ -534,5 +538,6 @@ export {
534
538
  createMetaMaskAdapters,
535
539
  createMetaMaskUiTransport,
536
540
  hideMobileHudOnTeardown,
537
- isMobileHudLifecycleSkip
541
+ isMobileHudLifecycleSkip,
542
+ normalizeUiWaitNode
538
543
  };
@@ -0,0 +1,72 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { recipeRuntimeDir } from "./paths.js";
5
+ function acquireCheckoutLock(target, operation) {
6
+ const repoRoot = fs.realpathSync(target);
7
+ const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
8
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
9
+ const inheritedToken = process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
10
+ for (let attempt = 0; attempt < 2; attempt += 1) {
11
+ try {
12
+ const fd = fs.openSync(lockPath, "wx", 384);
13
+ const token = inheritedToken || randomUUID();
14
+ const owner = {
15
+ schemaVersion: 1,
16
+ pid: process.pid,
17
+ operation,
18
+ repoRoot,
19
+ token,
20
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
21
+ };
22
+ fs.writeFileSync(fd, `${JSON.stringify(owner, null, 2)}
23
+ `);
24
+ fs.closeSync(fd);
25
+ if (!inheritedToken) process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN = token;
26
+ return {
27
+ path: lockPath,
28
+ release: () => {
29
+ const current = readOwner(lockPath);
30
+ if (current?.pid === process.pid && current.token === token) fs.rmSync(lockPath, { force: true });
31
+ if (!inheritedToken && process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN === token) {
32
+ delete process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
33
+ }
34
+ }
35
+ };
36
+ } catch (error) {
37
+ if (error.code !== "EEXIST") throw error;
38
+ const owner = readOwner(lockPath);
39
+ if (inheritedToken && owner?.token === inheritedToken && owner.repoRoot === repoRoot && typeof owner.pid === "number" && processAlive(owner.pid)) {
40
+ return { path: lockPath, release: () => void 0 };
41
+ }
42
+ if (owner && typeof owner.pid === "number" && processAlive(owner.pid)) {
43
+ return {
44
+ path: lockPath,
45
+ owner,
46
+ message: `checkout sandbox is busy with ${String(owner.operation ?? "another operation")} (pid ${owner.pid}).`
47
+ };
48
+ }
49
+ fs.rmSync(lockPath, { force: true });
50
+ }
51
+ }
52
+ return { path: lockPath, owner: readOwner(lockPath), message: "checkout sandbox lock could not be acquired." };
53
+ }
54
+ function readOwner(lockPath) {
55
+ try {
56
+ const value = JSON.parse(fs.readFileSync(lockPath, "utf8"));
57
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function processAlive(pid) {
63
+ try {
64
+ process.kill(pid, 0);
65
+ return true;
66
+ } catch (error) {
67
+ return error.code === "EPERM";
68
+ }
69
+ }
70
+ export {
71
+ acquireCheckoutLock
72
+ };
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { handleFixtures } from "./commands/fixtures.js";
20
20
  import { handleRecipeQuality } from "./commands/recipe-quality.js";
21
21
  import { handleStatus } from "./commands/status.js";
22
22
  import { handleCheck } from "./commands/check.js";
23
+ import { handleChecklist } from "./commands/checklist.js";
23
24
  import { parseArgs, targetPath } from "./commands/parse-args.js";
24
25
  import { runOneNode } from "./commands/run-engine.js";
25
26
  const COMMANDS = {
@@ -131,6 +132,7 @@ async function main(argv) {
131
132
  if (command === "fixtures") return handleFixtures(argv.slice(1), { runOneNode });
132
133
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
133
134
  if (command === "check") return handleCheck(argv.slice(1));
135
+ if (command === "checklist") return handleChecklist(argv.slice(1));
134
136
  const handler = COMMANDS[command];
135
137
  if (!handler) throw new Error(`Unknown command: ${command}`);
136
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 { EXIT } from "./shared.js";
9
+ import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
10
10
  import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
11
11
  import {
12
12
  parseArgs,
@@ -25,11 +25,13 @@ import {
25
25
  emitHealViolation,
26
26
  executeWithHealBounds,
27
27
  prepareHeal,
28
+ recoverRunInfra,
28
29
  resolveMetaMaskLibrarySources,
29
30
  runRecipe,
30
31
  synthesizeOneNodeRecipe,
31
32
  validateRecipeAdapterAware
32
33
  } from "./run-engine.js";
34
+ import { acquireCheckoutLock } from "../checkout-lock.js";
33
35
  async function handleCall(argv) {
34
36
  if (argv.includes("--list")) {
35
37
  const { options: options2 } = parseArgs(argv, "call");
@@ -63,6 +65,14 @@ async function handleCall(argv) {
63
65
  return EXIT.usage;
64
66
  }
65
67
  const { adapter, target } = resolveAdapter(options);
68
+ if (!fs.existsSync(target)) {
69
+ return usageOut(
70
+ json,
71
+ "call",
72
+ `target does not exist: ${target}`,
73
+ "pass --target <metamask-checkout> pointing to an existing checkout"
74
+ );
75
+ }
66
76
  getAdapterSurface(adapter).resolveSlotPorts(target);
67
77
  const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
68
78
  if ("code" in dtResult) {
@@ -119,65 +129,76 @@ async function handleCall(argv) {
119
129
  }
120
130
  return EXIT.validation;
121
131
  }
122
- const prepared = await prepareHeal(adapter, target, options, json);
123
- if (typeof prepared === "number") return prepared;
124
- const { state, heal } = prepared;
125
- const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
126
- const recipeFile = path.join(scratch, "call.recipe.json");
127
- fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
132
+ const lock = acquireCheckoutLock(target, "call");
133
+ if ("message" in lock) {
134
+ return checkoutBusyOut(json, "call", lock.message, lock.path);
135
+ }
136
+ try {
137
+ const prepared = await prepareHeal(adapter, target, options, json, {
138
+ skipMobileSourceFreshness: adapter === "mobile" && resolvedAction === "app.lifecycle"
139
+ });
140
+ if (typeof prepared === "number") return prepared;
141
+ const { state, heal } = prepared;
142
+ const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
143
+ const recipeFile = path.join(scratch, "call.recipe.json");
144
+ fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
128
145
  `);
129
- const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
130
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
131
- const callRuntimeOptions = {
132
- ...runtimeOptionsFromCli(options),
133
- ...librarySources ? { librarySources } : {},
134
- stdoutIsMachineContract: json
135
- };
136
- const { result, violation } = await executeWithHealBounds(
137
- () => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
138
- adapter,
139
- target,
140
- heal,
141
- state
142
- );
143
- if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
144
- const callOutput = readCallOutput(result.tracePath);
145
- if (json) {
146
- console.log(
147
- JSON.stringify(
148
- {
149
- schemaVersion: 1,
150
- command: "call",
151
- adapter,
152
- action: shortName,
153
- resolvedAction,
154
- args,
155
- status: result.status,
156
- summaryPath: result.summaryPath,
157
- tracePath: result.tracePath,
158
- artifactManifestPath: result.artifactManifestPath,
159
- ...callOutput !== void 0 ? { output: callOutput } : {},
160
- recovered: state.recovered,
161
- mutations: state.mutations,
162
- exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
163
- },
164
- null,
165
- 2
166
- )
146
+ const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
147
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
148
+ const callRuntimeOptions = {
149
+ ...runtimeOptionsFromCli(options),
150
+ ...librarySources ? { librarySources } : {},
151
+ stdoutIsMachineContract: json
152
+ };
153
+ const { result, violation } = await executeWithHealBounds(
154
+ () => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
155
+ adapter,
156
+ target,
157
+ heal,
158
+ state,
159
+ () => recoverRunInfra(adapter, target, json)
167
160
  );
168
- } else {
169
- const rendered = callOutput !== void 0 ? `
161
+ if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
162
+ const callOutput = readCallOutput(result.tracePath);
163
+ if (json) {
164
+ console.log(
165
+ JSON.stringify(
166
+ {
167
+ schemaVersion: 1,
168
+ command: "call",
169
+ adapter,
170
+ action: shortName,
171
+ resolvedAction,
172
+ args,
173
+ status: result.status,
174
+ summaryPath: result.summaryPath,
175
+ tracePath: result.tracePath,
176
+ artifactManifestPath: result.artifactManifestPath,
177
+ ...callOutput !== void 0 ? { output: callOutput } : {},
178
+ recovered: state.recovered,
179
+ mutations: state.mutations,
180
+ exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
181
+ },
182
+ null,
183
+ 2
184
+ )
185
+ );
186
+ } else {
187
+ const rendered = callOutput !== void 0 ? `
170
188
  Result:
171
189
  ${formatCallOutput(callOutput)}` : "";
172
- const out = (style, text) => color(style, text, { stream: process.stdout });
173
- console.log(
174
- `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
190
+ const out = (style, text) => color(style, text, { stream: process.stdout });
191
+ console.log(
192
+ `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
175
193
  ${out("label", "Result:")}
176
194
  ${formatCallOutput(callOutput)}` : ""}
177
195
  ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
178
- );
196
+ );
197
+ }
198
+ return result.status === "pass" ? EXIT.ok : EXIT.runtime;
199
+ } finally {
200
+ lock.release();
179
201
  }
180
- return result.status === "pass" ? EXIT.ok : EXIT.runtime;
181
202
  }
182
203
  function readCallOutput(tracePath) {
183
204
  try {
@@ -281,7 +302,7 @@ function parseCallArgs(argv) {
281
302
  if (pair === void 0) throw usageError("--arg requires k=v.");
282
303
  const eq = pair.indexOf("=");
283
304
  if (eq === -1) throw usageError(`--arg must be k=v: ${pair}`);
284
- args[pair.slice(0, eq)] = pair.slice(eq + 1);
305
+ args[pair.slice(0, eq)] = parseCallValue(pair.slice(eq + 1));
285
306
  continue;
286
307
  }
287
308
  if (!arg.startsWith("--") && action === void 0) {
@@ -290,13 +311,29 @@ function parseCallArgs(argv) {
290
311
  }
291
312
  if (action !== void 0 && !isForwardedOptionValue(argv, i) && isArgPair(arg)) {
292
313
  const eq = arg.indexOf("=");
293
- args[arg.slice(0, eq)] = arg.slice(eq + 1);
314
+ args[arg.slice(0, eq)] = parseCallValue(arg.slice(eq + 1));
294
315
  continue;
295
316
  }
296
317
  rest.push(arg);
297
318
  }
298
319
  return { action, args, rest };
299
320
  }
321
+ function parseCallValue(value) {
322
+ if (value === "true") return true;
323
+ if (value === "false") return false;
324
+ if (value === "null") return null;
325
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u.test(value)) {
326
+ const number = Number(value);
327
+ if (Number.isFinite(number)) return number;
328
+ }
329
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}") || value.startsWith('"') && value.endsWith('"')) {
330
+ try {
331
+ return JSON.parse(value);
332
+ } catch {
333
+ }
334
+ }
335
+ return value;
336
+ }
300
337
  function isArgPair(value) {
301
338
  const eq = value.indexOf("=");
302
339
  return eq > 0;