@deeeed/metamask-harness 0.75.0 → 0.76.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 (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/adapters/shared/reap-checkout-metros.sh +9 -0
  3. package/dist/adapters/core/overlay.js +34 -0
  4. package/dist/adapters/core/readiness.js +8 -0
  5. package/dist/adapters/core/surface.js +7 -0
  6. package/dist/adapters/extension/doctor-checks.js +135 -0
  7. package/dist/adapters/extension/fixtures.js +103 -0
  8. package/dist/adapters/extension/overlay.js +55 -0
  9. package/dist/adapters/extension/readiness.js +32 -0
  10. package/dist/adapters/extension/reload.js +40 -0
  11. package/dist/adapters/extension/surface.js +22 -1
  12. package/dist/adapters/harness-freshness.js +4 -2
  13. package/dist/adapters/mobile/devices.js +21 -0
  14. package/dist/adapters/mobile/doctor-checks.js +40 -0
  15. package/dist/adapters/mobile/fixtures.js +141 -0
  16. package/dist/adapters/mobile/overlay.js +27 -0
  17. package/dist/adapters/mobile/prepare-target.js +38 -0
  18. package/dist/adapters/mobile/readiness.js +39 -0
  19. package/dist/adapters/mobile/reload.js +15 -0
  20. package/dist/adapters/mobile/surface.js +28 -1
  21. package/dist/adapters/terminal/doctor-checks.js +22 -0
  22. package/dist/adapters/terminal/overlay.js +18 -0
  23. package/dist/adapters/terminal/readiness.js +11 -0
  24. package/dist/adapters/terminal/surface.js +15 -2
  25. package/dist/cli.js +3 -1
  26. package/dist/commands/debug.js +6 -25
  27. package/dist/commands/device-target.js +2 -1
  28. package/dist/commands/doctor.js +26 -137
  29. package/dist/commands/fixtures.js +13 -217
  30. package/dist/commands/logs.js +1 -1
  31. package/dist/commands/parse-args.js +9 -4
  32. package/dist/commands/prepare.js +23 -53
  33. package/dist/commands/provision.js +3 -7
  34. package/dist/commands/reload.js +8 -47
  35. package/dist/commands/runtime-decision.js +5 -27
  36. package/dist/commands/self-test.js +1 -3
  37. package/dist/commands/status.js +20 -28
  38. package/dist/commands/stop.js +1 -1
  39. package/dist/doctor.js +9 -119
  40. package/dist/harness.js +31 -100
  41. package/dist/mm-harness-cli.js +3 -2
  42. package/dist/paths.js +8 -2
  43. package/dist/runtime-context.js +12 -17
  44. package/docs/CONTRIBUTING.md +27 -17
  45. package/package.json +1 -1
package/dist/harness.js CHANGED
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
6
- import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
6
+ import { adapterRegistry } from "./adapters/registry.js";
7
7
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
8
8
  import { colorHumanMessage } from "./cli-color.js";
9
9
  const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
@@ -37,15 +37,15 @@ Examples:
37
37
 
38
38
  core supports install, verify, and cleanup (headless; no live).`);
39
39
  }
40
- function failureHint(adapter, action) {
41
- if (adapter === "core") {
40
+ function failureHint(surface, action) {
41
+ const restart = surface.harness.restart;
42
+ if (!restart) {
42
43
  if (action === "verify") {
43
- return `Install the core harness first: mm-harness install --platform core`;
44
+ return `Install the ${surface.adapter} harness first: mm-harness install --platform ${surface.adapter}`;
44
45
  }
45
46
  return "Read the error above for the specific cause, then re-run this command.";
46
47
  }
47
- const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
48
- return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
48
+ return `Read the error above for the specific cause. To (re)start the runtime, run: ${restart}`;
49
49
  }
50
50
  function successNext(adapter, action, target) {
51
51
  const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
@@ -67,7 +67,7 @@ function shellQuote(value) {
67
67
  return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`;
68
68
  }
69
69
  function isAdapter(value) {
70
- return value === "mobile" || value === "extension" || value === "core" || value === "terminal";
70
+ return value !== void 0 && adapterRegistry.has(value);
71
71
  }
72
72
  function parseHarnessArgs(args) {
73
73
  const forward = [];
@@ -170,37 +170,9 @@ function readRuntimeContextField(contextPath, field) {
170
170
  if (node === void 0 || node === null || node === "") return void 0;
171
171
  return String(node);
172
172
  }
173
- function applyExtensionRuntimeEnv(target, args) {
174
- const contextPath = resolveRuntimeContextPath(target);
175
- const contextExists = fs.existsSync(contextPath);
176
- if (contextExists) {
177
- process.env.RECIPE_RUNTIME_CONTEXT = contextPath;
178
- if (!process.env.RECIPE_SLOT_ID) {
179
- const slotId = readRuntimeContextField(contextPath, "slotId");
180
- if (slotId) process.env.RECIPE_SLOT_ID = slotId;
181
- }
182
- if (!process.env.RECIPE_HARNESS_EXTENSION_ID) {
183
- const extensionId = readRuntimeContextField(contextPath, "extensionId");
184
- if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
185
- }
186
- } else {
187
- delete process.env.RECIPE_RUNTIME_CONTEXT;
188
- }
189
- resolveExtensionSlotPorts(target);
190
- let result = [...args];
191
- if (!hasArg(result, "--cdp-port")) {
192
- const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
193
- if (cdpPort) {
194
- process.env.RECIPE_CDP_PORT = cdpPort;
195
- process.env.CDP_PORT = cdpPort;
196
- result = [...result, "--cdp-port", cdpPort];
197
- }
198
- }
199
- return result;
200
- }
201
173
  function resolveEntry(base, candidates, mode) {
202
174
  for (const candidate of candidates) {
203
- const full = path.join(base, candidate);
175
+ const full = path.resolve(base, candidate);
204
176
  try {
205
177
  const stat = fs.statSync(full);
206
178
  if (!stat.isFile()) continue;
@@ -208,7 +180,7 @@ function resolveEntry(base, candidates, mode) {
208
180
  } catch {
209
181
  }
210
182
  }
211
- return path.join(base, candidates[0]);
183
+ return path.resolve(base, candidates[0]);
212
184
  }
213
185
  function installedRunnerSource(target, adapter) {
214
186
  const pointer = path.join(recipeHarnessPath(target, adapter), "runner", ".runner-source");
@@ -217,56 +189,12 @@ function installedRunnerSource(target, adapter) {
217
189
  if (!value || !fs.existsSync(value)) return void 0;
218
190
  return value;
219
191
  }
220
- function isExecutable(file) {
221
- try {
222
- const stat = fs.statSync(file);
223
- return stat.isFile() && (stat.mode & 73) !== 0;
224
- } catch {
225
- return false;
226
- }
227
- }
228
- const INJECT_CANDIDATES = {
229
- mobile: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
230
- extension: { entry: "adapters/extension/inject.mjs", fallback: "scripts/inject-extension-harness.mjs" },
231
- core: { entry: "adapters/core/inject.sh", fallback: "scripts/inject-core-harness.sh" },
232
- terminal: { entry: "adapters/terminal/inject.mjs", fallback: "adapters/terminal/inject.mjs" }
233
- };
234
- const CLEANUP_CANDIDATES = {
235
- mobile: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh" },
236
- extension: { entry: "adapters/extension/cleanup.mjs", fallback: "scripts/cleanup-extension-harness.mjs" },
237
- core: { entry: "adapters/core/cleanup.sh", fallback: "scripts/cleanup-core-harness.sh" },
238
- terminal: { entry: "adapters/terminal/cleanup.mjs", fallback: "adapters/terminal/cleanup.mjs" }
239
- };
240
- function resolveHarnessDispatch(adapter, action, target) {
241
- if (action === "install") {
242
- const { entry, fallback } = INJECT_CANDIDATES[adapter];
243
- if (adapter === "extension" || adapter === "terminal") {
244
- return { command: process.execPath, prefixArgs: [resolveEntry(runnerDir, [entry, fallback], "file")] };
245
- }
246
- return { command: resolveEntry(runnerDir, [entry, fallback], "file"), prefixArgs: [] };
247
- }
248
- if (action === "cleanup") {
249
- const { entry, fallback } = CLEANUP_CANDIDATES[adapter];
250
- const base = adapter === "terminal" ? runnerDir : installedRunnerSource(target, adapter) ?? runnerDir;
251
- if (adapter === "extension" || adapter === "terminal") {
252
- return { command: process.execPath, prefixArgs: [resolveEntry(base, [entry, fallback], "file")] };
253
- }
254
- return { command: resolveEntry(base, [entry, fallback], "file"), prefixArgs: [] };
255
- }
256
- if (adapter === "terminal" && action === "verify") {
257
- return { command: process.execPath, prefixArgs: [path.join(runnerDir, "adapters", "terminal", "verify.mjs")] };
258
- }
259
- if (adapter === "core" && action === "verify") {
260
- const delegate = path.join(recipeHarnessPath(target, "core"), "runner", "bin", "mm-harness");
261
- if (!isExecutable(delegate)) {
262
- return {
263
- error: `core recipe harness not installed: missing delegate ${delegate}.
264
- Next: mm-harness install --platform core --target ${target}`
265
- };
266
- }
267
- return { command: delegate, prefixArgs: ["doctor", "--adapter", "core"] };
268
- }
269
- return { inProcess: adapter === "mobile" ? "mobile-verify" : "extension-verify" };
192
+ function resolveHarnessDispatch(surface, action, target) {
193
+ if (action === "verify") return surface.harness.verify(target);
194
+ const leaf = action === "install" ? surface.harness.install : surface.harness.cleanup;
195
+ const base = action === "cleanup" && surface.harness.cleanup.fromInstalledRunner ? installedRunnerSource(target, surface.adapter) ?? runnerDir : runnerDir;
196
+ const entry = resolveEntry(base, [leaf.entry, leaf.fallback], "file");
197
+ return leaf.node ? { command: process.execPath, prefixArgs: [entry] } : { command: entry, prefixArgs: [] };
270
198
  }
271
199
  async function handleHarness(argv) {
272
200
  const action = argv[0];
@@ -280,6 +208,7 @@ async function handleHarness(argv) {
280
208
  return 2;
281
209
  }
282
210
  const harnessAction = action;
211
+ const { getAdapterSurface } = await import("./adapters/surface.js");
283
212
  const { adapter: parsedAdapter, json, quiet, forward } = parseHarnessArgs(argv.slice(1));
284
213
  const rawTarget = argValue(forward, "--target");
285
214
  const target = path.resolve(rawTarget ?? process.cwd());
@@ -306,10 +235,11 @@ async function handleHarness(argv) {
306
235
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
307
236
  return handleRunwayInstall(adapter, target, forward, json);
308
237
  }
309
- if (adapter === "extension" && harnessAction === "verify") {
310
- forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
238
+ const surface = getAdapterSurface(adapter);
239
+ if (harnessAction === "verify" && surface.harness.verifyArgs) {
240
+ forwardArgs = surface.harness.verifyArgs(target, forwardArgs);
311
241
  }
312
- const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
242
+ const dispatch = resolveHarnessDispatch(surface, harnessAction, target);
313
243
  if ("error" in dispatch) {
314
244
  if (quiet) {
315
245
  return 1;
@@ -335,7 +265,7 @@ async function handleHarness(argv) {
335
265
  }
336
266
  const start = Date.now();
337
267
  if ("inProcess" in dispatch) {
338
- const runVerify = dispatch.inProcess === "mobile-verify" ? (await import("./adapters/mobile/verify.js")).runMobileVerify : (await import("./adapters/extension/verify.js")).runExtensionVerify;
268
+ const runVerify = await dispatch.inProcess();
339
269
  let exitCode;
340
270
  try {
341
271
  exitCode = await runVerify(forwardArgs, {
@@ -355,7 +285,7 @@ async function handleHarness(argv) {
355
285
  `);
356
286
  exitCode = 1;
357
287
  }
358
- return reportHarnessOutcome(harnessAction, adapter, target, exitCode, autoDetected, json, quiet, start);
288
+ return reportHarnessOutcome(harnessAction, surface, target, exitCode, autoDetected, json, quiet, start);
359
289
  }
360
290
  if (shellLeafMissing(dispatch.command)) {
361
291
  const message = missingShellLeafMessage(dispatch.command);
@@ -366,7 +296,7 @@ async function handleHarness(argv) {
366
296
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
367
297
  code: "HARNESS_SPAWN_FAILED",
368
298
  message,
369
- userAction: failureHint(adapter, harnessAction)
299
+ userAction: failureHint(surface, harnessAction)
370
300
  })
371
301
  );
372
302
  } else {
@@ -387,19 +317,20 @@ async function handleHarness(argv) {
387
317
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
388
318
  code: "HARNESS_SPAWN_FAILED",
389
319
  message: `${harnessAction} ${adapter} could not start: ${result.error.message}`,
390
- userAction: failureHint(adapter, harnessAction)
320
+ userAction: failureHint(surface, harnessAction)
391
321
  })
392
322
  );
393
323
  } else {
394
324
  console.error(`\u2717 ${harnessAction} ${adapter} could not start: ${result.error.message}
395
- ${failureHint(adapter, harnessAction)}`);
325
+ ${failureHint(surface, harnessAction)}`);
396
326
  }
397
327
  return 1;
398
328
  }
399
- return reportHarnessOutcome(harnessAction, adapter, target, result.status ?? 1, autoDetected, json, quiet, start);
329
+ return reportHarnessOutcome(harnessAction, surface, target, result.status ?? 1, autoDetected, json, quiet, start);
400
330
  }
401
- function reportHarnessOutcome(action, adapter, target, exitCode, autoDetected, json, quiet, start) {
331
+ function reportHarnessOutcome(action, surface, target, exitCode, autoDetected, json, quiet, start) {
402
332
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
333
+ const adapter = surface.adapter;
403
334
  if (quiet) {
404
335
  return exitCode;
405
336
  } else if (json) {
@@ -411,7 +342,7 @@ function reportHarnessOutcome(action, adapter, target, exitCode, autoDetected, j
411
342
  exitCode === 0 ? "pass" : "fail",
412
343
  exitCode,
413
344
  autoDetected,
414
- exitCode === 0 ? void 0 : { code: "HARNESS_FAILED", message: `${action} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(adapter, action) }
345
+ exitCode === 0 ? void 0 : { code: "HARNESS_FAILED", message: `${action} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(surface, action) }
415
346
  )
416
347
  );
417
348
  } else if (exitCode === 0) {
@@ -419,7 +350,7 @@ function reportHarnessOutcome(action, adapter, target, exitCode, autoDetected, j
419
350
  Next: ${successNext(adapter, action, target)}`));
420
351
  } else {
421
352
  console.error(colorHumanMessage(`\u2717 ${action} ${adapter} failed (exit ${exitCode}, ${seconds}s)
422
- ${failureHint(adapter, action)}`));
353
+ ${failureHint(surface, action)}`));
423
354
  }
424
355
  return exitCode;
425
356
  }
@@ -448,7 +379,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
448
379
  });
449
380
  const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
450
381
  const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
451
- const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuote(provisionedDevice)}` : "") : void 0;
382
+ const next = result.status === "pass" && surface.runwayProvision.launchNext && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter ${adapter} --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuote(provisionedDevice)}` : "") : void 0;
452
383
  const output = next ? { ...result, next } : result;
453
384
  if (json) {
454
385
  console.log(JSON.stringify(output, null, 2));
@@ -1032,8 +1032,9 @@ function detectedSlotLine(out) {
1032
1032
  if (ctx.slotId) parts.push(`slot ${out("ok", String(ctx.slotId))}`);
1033
1033
  if (ctx.simulator) parts.push(`device ${out("ok", String(ctx.simulator))}`);
1034
1034
  const devServerPort = ctx.watcherPort ?? ctx.devServerPort ?? ctx.metroPort;
1035
- if (adapter && adapter !== "core" && devServerPort) {
1036
- parts.push(`${getAdapterSurface(adapter).devServer.label} :${out("ok", String(devServerPort))}`);
1035
+ const surface = adapter ? getAdapterSurface(adapter) : void 0;
1036
+ if (surface && !surface.headless && devServerPort) {
1037
+ parts.push(`${surface.devServer.label} :${out("ok", String(devServerPort))}`);
1037
1038
  }
1038
1039
  if (ctx.gitBranch) parts.push(`branch ${out("info", String(ctx.gitBranch))}`);
1039
1040
  if (parts.length === 0) return null;
package/dist/paths.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { adapterRegistry } from "./adapters/registry.js";
4
5
  const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
5
6
  const pathDefaults = readPathDefaults();
6
7
  const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
@@ -88,8 +89,13 @@ function isProtocolRoot(candidate) {
88
89
  return fs.existsSync(path.join(candidate, "packages/recipe-harness/package.json")) && fs.existsSync(path.join(candidate, "packages/protocol/package.json"));
89
90
  }
90
91
  function assertAdapter(adapter) {
91
- if (adapter !== "mobile" && adapter !== "extension" && adapter !== "core" && adapter !== "terminal") {
92
- throw new Error("Adapter must be mobile, extension, core, or terminal.");
92
+ const ids = adapterRegistry.list();
93
+ if (ids.length === 0) {
94
+ throw new Error("internal: no adapters are registered yet; import src/adapters/surface.ts before validating an adapter");
95
+ }
96
+ if (typeof adapter !== "string" || !adapterRegistry.has(adapter)) {
97
+ const choices = ids.length > 1 ? `${ids.slice(0, -1).join(", ")}, or ${ids.at(-1)}` : ids.join("");
98
+ throw new Error(`Adapter must be ${choices}.`);
93
99
  }
94
100
  }
95
101
  function manifestPath(adapter) {
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { getAdapterSurface } from "./adapters/surface.js";
6
7
  import { recipeRuntimeDir } from "./paths.js";
7
8
  function ensureRuntimeContext(target, adapter) {
8
9
  const repoRoot = fs.realpathSync(target);
@@ -91,7 +92,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
91
92
  if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
92
93
  }
93
94
  }
94
- const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial"] : adapter === "terminal" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : ["cdpPort", "extensionId"];
95
+ const forbiddenResources = getAdapterSurface(adapter).runtimeContext.forbiddenFields;
95
96
  if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
96
97
  return true;
97
98
  }
@@ -104,11 +105,12 @@ function localDefaults(repoRoot, adapter, existing) {
104
105
  const digest = createHash("sha256").update(repoRoot).digest("hex");
105
106
  const offset = Number.parseInt(digest.slice(0, 8), 16) % 1e4;
106
107
  const defaults = { slotId: `local-${adapter}-${digest.slice(0, 8)}` };
107
- if (adapter === "extension" && !numericEnv("RECIPE_CDP_PORT", "CDP_PORT") && !existing.cdpPort) {
108
- defaults.cdpPort = claimLocalPort(repoRoot, "cdp", 2e4 + offset);
108
+ const localPorts = getAdapterSurface(adapter).runtimeContext.localPorts ?? {};
109
+ if (localPorts.cdp !== void 0 && !numericEnv("RECIPE_CDP_PORT", "CDP_PORT") && !existing.cdpPort) {
110
+ defaults.cdpPort = claimLocalPort(repoRoot, "cdp", localPorts.cdp + offset);
109
111
  }
110
- if (adapter !== "core" && adapter !== "terminal" && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
111
- defaults.watcherPort = claimLocalPort(repoRoot, "watcher", (adapter === "extension" ? 3e4 : 4e4) + offset);
112
+ if (localPorts.watcher !== void 0 && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
113
+ defaults.watcherPort = claimLocalPort(repoRoot, "watcher", localPorts.watcher + offset);
112
114
  }
113
115
  return defaults;
114
116
  }
@@ -158,21 +160,14 @@ function portClaimIsLive(claim, port) {
158
160
  }
159
161
  }
160
162
  function runtimeResources(adapter, existing, defaults) {
161
- if (adapter === "core") return {};
163
+ const { resources } = getAdapterSurface(adapter).runtimeContext;
164
+ if (!resources) return {};
162
165
  const watcherPort = numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") ?? existing.watcherPort ?? existing.metroPort ?? existing.devServerPort ?? defaults.watcherPort;
163
- const shared = {
166
+ return {
164
167
  watcherPort,
165
- devServerPort: watcherPort
168
+ devServerPort: watcherPort,
169
+ ...resources({ watcherPort, existing, defaults, envPort: numericEnv })
166
170
  };
167
- if (adapter === "extension" || adapter === "terminal") {
168
- shared.cdpPort = numericEnv("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort;
169
- } else {
170
- shared.metroPort = watcherPort;
171
- shared.simulator = process.env.IOS_SIMULATOR || existing.simulator;
172
- shared.simulatorUdid = process.env.SIM_UDID || existing.simulatorUdid;
173
- shared.adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial;
174
- }
175
- return shared;
176
171
  }
177
172
  function numericEnv(...names) {
178
173
  for (const name of names) {
@@ -62,25 +62,35 @@ exits.
62
62
 
63
63
  Commands resolve shared platform behavior through
64
64
  `src/adapters/surface.ts`. Each platform's surface registers itself in
65
- `src/adapters/registry.ts` when its module loads. A platform registered the same
66
- way is reached by `launch`, by the `run`/`call` engine and by the runner's
67
- action set with no edit to shared code (`tests/fixtures/example-adapter.ts`).
68
- The CLI front door still validates `--adapter` against the four built-ins
69
- (`src/command-contract.ts`), and the other commands still branch on the name,
70
- so a new platform reaches `bin/mm-harness` only when those families move and a
71
- plugin loader registers it.
65
+ `src/adapters/registry.ts` when its module loads, and commands look adapters up
66
+ in the registry, so a platform registered the same way is reached by the
67
+ commands that take `--adapter` with no edit to shared code
68
+ (`tests/fixtures/example-adapter.ts`); `status` detects the adapter from the
69
+ checkout.
70
+ The CLI contract (`src/command-contract.ts`) still lists the four built-ins for
71
+ `--adapter`, so a new platform reaches `bin/mm-harness` once a plugin loader
72
+ registers it and extends that list.
72
73
 
73
74
  Extend `AdapterSurface` when a platform owns the behavior; members a platform
74
- does not need are optional (`actions` is required: every platform has an
75
- action set), and Core reports headless/N/A explicitly. Put each platform's
76
- implementation under `src/adapters/<adapter>/` (`launch.ts`, `run.ts`,
77
- `actions.ts`). Do not add command-local platform ladders for surface-owned
78
- behavior: `launch` dispatches through `AdapterSurface.launch` (the device
79
- platforms share `src/commands/launch/device.ts` through a
80
- `DeviceLaunchProfile`), `run`/`call` read `run`, `recording`, `diagnostics`,
81
- `observation` and `sourceFingerprint`, and the runner reads `actions`
82
- (manifest, bundled semantic actions, live adapter scripts, `cdp.target`, the
83
- `ui.*` transport, adapter wrappers, input checks).
75
+ does not need are optional, and Core reports headless/N/A explicitly. Put each
76
+ platform's implementation under `src/adapters/<adapter>/`: `launch.ts`,
77
+ `run.ts`, `actions.ts`, `overlay.ts` (install/verify/cleanup, the runtime
78
+ context, prepare) and `readiness.ts` (doctor and status). Do not add
79
+ command-local platform ladders for surface-owned behavior:
80
+ - `launch` dispatches through `AdapterSurface.launch` (the device platforms
81
+ share `src/commands/launch/device.ts` through a `DeviceLaunchProfile`);
82
+ - `run`/`call` read `run`, `recording`, `diagnostics`, `observation` and
83
+ `sourceFingerprint`;
84
+ - the runner reads `actions` (manifest, bundled semantic actions, live adapter
85
+ scripts, `cdp.target`, the `ui.*` transport, adapter wrappers, input checks);
86
+ - `install`/`verify`/`cleanup` read `harness`, `agentic-runtime.json` reads
87
+ `runtimeContext`, `doctor`/`status` read `readiness`, and `fixtures`,
88
+ `prepare`, `debug`, `reload` and `runtime-decision` read their own members.
89
+
90
+ A few comparisons remain where a command exists for one platform
91
+ (`runtime-health`, `resolve-extension`, `ensure-ready`, `runtime-launch`) or
92
+ code compares a platform's own data (`review/knowledge.ts`, the Core jest
93
+ grouping in `check.ts`, the Mobile release-artifact state file).
84
94
 
85
95
  Each checkout owns:
86
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"