@deeeed/metamask-harness 0.75.0 → 0.77.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 (49) hide show
  1. package/CHANGELOG.md +28 -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/call.js +3 -7
  27. package/dist/commands/debug.js +6 -25
  28. package/dist/commands/device-target.js +2 -1
  29. package/dist/commands/doctor.js +26 -137
  30. package/dist/commands/fixtures.js +13 -217
  31. package/dist/commands/list-executables.js +12 -22
  32. package/dist/commands/logs.js +1 -1
  33. package/dist/commands/manifest.js +11 -133
  34. package/dist/commands/parse-args.js +9 -4
  35. package/dist/commands/prepare.js +23 -53
  36. package/dist/commands/provision.js +3 -7
  37. package/dist/commands/reload.js +8 -47
  38. package/dist/commands/run.js +2 -5
  39. package/dist/commands/runtime-decision.js +5 -27
  40. package/dist/commands/self-test.js +1 -3
  41. package/dist/commands/status.js +20 -28
  42. package/dist/commands/stop.js +1 -1
  43. package/dist/doctor.js +9 -119
  44. package/dist/harness.js +31 -100
  45. package/dist/mm-harness-cli.js +3 -2
  46. package/dist/paths.js +8 -2
  47. package/dist/runtime-context.js +12 -17
  48. package/docs/CONTRIBUTING.md +27 -17
  49. package/package.json +2 -2
@@ -17,6 +17,15 @@ import {
17
17
  } from "@farmslot/protocol";
18
18
  import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
19
19
  import { closest } from "../command-contract.js";
20
+ import {
21
+ actionCapabilityMatrix,
22
+ actionCategory,
23
+ findRelatedActions,
24
+ fuzzyResolveActions,
25
+ resolveActionCapabilityRefusal,
26
+ searchActions,
27
+ summarizeActionCategories
28
+ } from "@farmslot/recipe-cli";
20
29
  const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
21
30
  const ACTION_MATRIX_ADAPTERS = ["mobile", "extension", "core", "terminal"];
22
31
  function actionLibraryContextArgs(librarySources) {
@@ -178,51 +187,7 @@ async function resolveActionCapabilityMatrix(librarySources) {
178
187
  return { adapter, actions: describeManifestActions(manifest, actionSources) };
179
188
  })
180
189
  );
181
- const actions = /* @__PURE__ */ new Map();
182
- for (const catalog of catalogs) {
183
- for (const action of catalog.actions) {
184
- const entry = actions.get(action.name) ?? { entries: [], adapters: /* @__PURE__ */ new Set() };
185
- entry.entries.push(action);
186
- entry.adapters.add(catalog.adapter);
187
- actions.set(action.name, entry);
188
- }
189
- }
190
- return [...actions.entries()].map(([name, entry]) => {
191
- const representative = entry.entries[0];
192
- const satisfyingAdapters = ACTION_MATRIX_ADAPTERS.filter(
193
- (adapter) => entry.adapters.has(adapter)
194
- );
195
- return {
196
- name,
197
- category: representative.category,
198
- description: entry.entries.map((action) => action.description).find(Boolean) ?? "",
199
- fields: [...new Set(entry.entries.flatMap((action) => action.fields))].sort(),
200
- support: Object.fromEntries(
201
- ACTION_MATRIX_ADAPTERS.map((adapter) => [
202
- adapter,
203
- entry.adapters.has(adapter) ? "available" : "unavailable"
204
- ])
205
- ),
206
- satisfyingAdapters
207
- };
208
- }).sort((left, right) => left.name.localeCompare(right.name));
209
- }
210
- function resolveActionCapabilityRefusal(action, adapter, matrix) {
211
- const matches = fuzzyResolveActions(matrix, action);
212
- if (matches.length !== 1 || matches[0].satisfyingAdapters.includes(adapter)) {
213
- return void 0;
214
- }
215
- return {
216
- capability: matches[0].name,
217
- satisfyingAdapters: matches[0].satisfyingAdapters
218
- };
219
- }
220
- function missingActionCapabilities(adapter, actionNames, matrix) {
221
- return [...new Set(actionNames)].flatMap((action) => {
222
- const row = matrix.find((entry) => entry.name === action);
223
- if (!row || row.satisfyingAdapters.includes(adapter)) return [];
224
- return [{ capability: row.name, satisfyingAdapters: row.satisfyingAdapters }];
225
- });
190
+ return actionCapabilityMatrix(catalogs);
226
191
  }
227
192
  function handleActionMatrixError(json, code, message, userAction, details = {}) {
228
193
  if (json) {
@@ -315,32 +280,6 @@ function renderHumanActionMatrix(actions) {
315
280
  )
316
281
  ].join("\n");
317
282
  }
318
- function fuzzyResolveActions(entries, query) {
319
- const exactFull = entries.filter((e) => e.name === query);
320
- if (exactFull.length > 0) return exactFull;
321
- const finalSegment2 = (name) => name.split(".").pop() ?? name;
322
- const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
323
- if (exactSegment.length > 0) return exactSegment;
324
- return entries.filter((e) => finalSegment2(e.name).includes(query));
325
- }
326
- function searchActions(entries, query) {
327
- const terms = searchTerms(query);
328
- if (terms.length === 0) return [];
329
- return entries.map((entry) => {
330
- const scores = terms.map((term) => actionSearchScore(entry, term));
331
- return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
332
- }).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
333
- }
334
- function findRelatedActions(entries, selected, limit = 5) {
335
- const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
336
- return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
337
- const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
338
- const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
339
- const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
340
- return { name: entry.name, score };
341
- }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
342
- }
343
- const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
344
283
  function summarizeDescribedAction(entry) {
345
284
  const { schema: _schema, examples: _examples, ...summary } = entry;
346
285
  return summary;
@@ -496,11 +435,6 @@ function actionDomainGroup(entry) {
496
435
  const match = /^metamask[.]([^.]+)[.]/u.exec(entry.name);
497
436
  return match ? `metamask \xB7 ${match[1]}` : "custom";
498
437
  }
499
- function summarizeActionCategories(actions) {
500
- const counts = /* @__PURE__ */ new Map();
501
- for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
502
- return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
503
- }
504
438
  function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
505
439
  const manifestRecord = isRecord(manifest) ? manifest : {};
506
440
  const actions = isRecord(manifestRecord.actions) ? manifestRecord.actions : {};
@@ -521,7 +455,7 @@ function describeManifestAction(name, kind, metadata, source) {
521
455
  return {
522
456
  name,
523
457
  kind,
524
- category: actionCategory(name),
458
+ category: actionCategory(name, ["metamask"]),
525
459
  description: typeof record.description === "string" ? record.description : "",
526
460
  fields: properties,
527
461
  schema,
@@ -539,72 +473,16 @@ function describeManifestAction(name, kind, metadata, source) {
539
473
  ])]
540
474
  };
541
475
  }
542
- function actionCategory(name) {
543
- const segments = name.split(".");
544
- if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
545
- if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
546
- if (segments[0] === "ui") return "ui";
547
- if (name.startsWith("assert_")) return "assertion";
548
- if (name === "watch_logs" || name === "index_artifacts") return "evidence";
549
- if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
550
- return segments.length > 1 ? segments[0] || "utility" : "utility";
551
- }
552
- function actionSearchScore(entry, term) {
553
- const name = entry.name.toLowerCase();
554
- const segment = finalSegment(name);
555
- const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
556
- const fields = entry.fields.map((field) => field.toLowerCase());
557
- const description = entry.description.toLowerCase();
558
- if (name === term) return 120;
559
- if (segment === term) return 110;
560
- if (entry.category === term) return 100;
561
- if (fields.includes(term)) return 90;
562
- if (nameTerms.includes(term)) return 80;
563
- if (name.includes(term)) return 70;
564
- if (fields.some((field) => field.includes(term))) return 60;
565
- if (description.includes(term)) return 50;
566
- if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
567
- return 0;
568
- }
569
- function fuzzyTermMatch(term, candidate) {
570
- if (term.length < 4 || candidate.length < 4) return false;
571
- const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
572
- return levenshtein(term, candidate) <= threshold;
573
- }
574
- function searchTerms(value) {
575
- return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
576
- }
577
- function finalSegment(name) {
578
- return name.split(".").pop() ?? name;
579
- }
580
- function levenshtein(left, right) {
581
- const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
582
- for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
583
- const current = [leftIndex];
584
- for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
585
- const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
586
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
587
- }
588
- for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
589
- }
590
- return prior[right.length] ?? right.length;
591
- }
592
476
  export {
593
477
  ACTION_MATRIX_ADAPTERS,
594
478
  actionDomain,
595
479
  actionExampleCommand,
596
480
  actionLibraryContextArgs,
597
481
  describeManifestActions,
598
- findRelatedActions,
599
- fuzzyResolveActions,
600
482
  handleActions,
601
- missingActionCapabilities,
602
483
  renderHumanActionCatalog,
603
484
  renderHumanActionExample,
604
485
  renderHumanActionMatrix,
605
486
  resolveActionCapabilityMatrix,
606
- resolveActionCapabilityRefusal,
607
- searchActions,
608
- summarizeActionCategories,
609
487
  summarizeDescribedAction
610
488
  };
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { detectAdapter } from "../harness.js";
3
- import { assertAdapter, manifestPath } from "../paths.js";
3
+ import { adapterRegistry } from "../adapters/registry.js";
4
+ import { assertAdapter } from "../paths.js";
4
5
  import { explicitRecipeTrustOptions } from "../recipe-security.js";
5
6
  import { ADAPTER_DETECT_NEXT } from "./shared.js";
6
7
  import { EXIT } from "./shared.js";
@@ -179,12 +180,16 @@ function requiredOption(options, key, message) {
179
180
  if (!value) throw usageError(message);
180
181
  return value;
181
182
  }
183
+ function adapterUsageError(error) {
184
+ if (error instanceof Error && error.message.startsWith("internal:")) return error;
185
+ return usageError(error instanceof Error ? error.message : String(error));
186
+ }
182
187
  function adapterOption(options) {
183
188
  const adapter = optionString(options, "adapter");
184
189
  try {
185
190
  assertAdapter(adapter);
186
191
  } catch (error) {
187
- throw usageError(error instanceof Error ? error.message : String(error));
192
+ throw adapterUsageError(error);
188
193
  }
189
194
  return adapter;
190
195
  }
@@ -193,7 +198,7 @@ function targetPath(options) {
193
198
  }
194
199
  function actionManifestPathOption(options, adapter) {
195
200
  const configured = optionString(options, "actionManifest");
196
- return configured ? path.resolve(configured) : manifestPath(adapter);
201
+ return configured ? path.resolve(configured) : adapterRegistry.get(adapter).actions.manifestPath();
197
202
  }
198
203
  function parsePort(value, errorMessage) {
199
204
  const port = Number(value);
@@ -238,7 +243,7 @@ function resolveAdapter(options) {
238
243
  try {
239
244
  assertAdapter(adapter);
240
245
  } catch (error) {
241
- throw usageError(error instanceof Error ? error.message : String(error));
246
+ throw adapterUsageError(error);
242
247
  }
243
248
  return { adapter, target };
244
249
  }
@@ -10,7 +10,9 @@ import {
10
10
  import { color, stripAnsi } from "../cli-color.js";
11
11
  import { runnerProvenance } from "../doctor.js";
12
12
  import { detectAdapter } from "../harness.js";
13
- import { harnessExecutable, manifestPath, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
13
+ import { adapterRegistry } from "../adapters/registry.js";
14
+ import { getAdapterSurface } from "../adapters/surface.js";
15
+ import { harnessExecutable, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
14
16
  import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
15
17
  import { EXIT } from "./shared.js";
16
18
  import { formatDuration } from "./status-watch.js";
@@ -52,8 +54,8 @@ const STEP_HINT = {
52
54
  launch: "can take minutes on Mobile: Metro bundle and app start",
53
55
  fixtures: "under a minute"
54
56
  };
55
- function stepChain(platform) {
56
- return platform === "core" ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
57
+ function stepChain(surface) {
58
+ return surface.headless ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
57
59
  }
58
60
  function lastRunDurations(recordPath) {
59
61
  let parsed;
@@ -225,36 +227,6 @@ function readDevices(artifactsDir) {
225
227
  const devices = parsed?.devices;
226
228
  return Array.isArray(devices) ? devices : [];
227
229
  }
228
- function readRuntimeContextTarget(target) {
229
- const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
230
- let parsed;
231
- try {
232
- parsed = JSON.parse(fs.readFileSync(file, "utf8"));
233
- } catch {
234
- return {};
235
- }
236
- const record = parsed ?? {};
237
- const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
238
- return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
239
- }
240
- function mobilePlatformFromConfiguredTarget(env, context) {
241
- const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
242
- const android = Boolean(
243
- env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
244
- );
245
- if (ios === android) return void 0;
246
- return ios ? "ios" : "android";
247
- }
248
- function mobilePlatformFromDevices(devices) {
249
- const selected = devices.filter((device) => device?.selected === true);
250
- const candidates = selected.length > 0 ? selected : devices;
251
- const platforms = [
252
- ...new Set(
253
- candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
254
- )
255
- ];
256
- return platforms.length === 1 ? platforms[0] : void 0;
257
- }
258
230
  function mobileTargetAmbiguityError() {
259
231
  const choices = [
260
232
  "1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
@@ -300,8 +272,9 @@ function mobilePlatformOption(value) {
300
272
  }
301
273
  function resolvePlatform(requested, target) {
302
274
  const platform = requested ?? detectAdapter(target);
303
- if (platform === "mobile" || platform === "extension" || platform === "core") {
304
- return platform;
275
+ const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
276
+ if (surface?.prepare) {
277
+ return surface;
305
278
  }
306
279
  throw usageError(
307
280
  `could not detect the platform of ${target}
@@ -328,21 +301,22 @@ async function handlePrepareLocked(argv) {
328
301
  const { options } = parseArgs(argv, "prepare");
329
302
  const json = optionFlag(options, "json");
330
303
  const target = targetPath(options);
331
- const platform = resolvePlatform(optionString(options, "platform"), target);
304
+ const surface = resolvePlatform(optionString(options, "platform"), target);
305
+ const platform = surface.adapter;
332
306
  const artifactsDir = path.resolve(
333
307
  optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
334
308
  );
335
309
  const executable = harnessExecutable();
336
310
  const targetArgs = ["--target", target];
337
311
  const clearMetro = optionFlag(options, "clearMetro");
338
- if (clearMetro && platform !== "mobile") {
312
+ if (clearMetro && !surface.prepare.clearMetro) {
339
313
  throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
340
314
  Next: ${USAGE}`);
341
315
  }
342
316
  const remoteFlag = optionString(options, "remoteFlag");
343
317
  if (remoteFlag !== void 0) {
344
- if (platform === "core") {
345
- throw usageError(`--remote-flag needs an app runtime; this checkout is core
318
+ if (surface.headless) {
319
+ throw usageError(`--remote-flag needs an app runtime; this checkout is ${platform}
346
320
  Next: ${USAGE}`);
347
321
  }
348
322
  try {
@@ -357,7 +331,7 @@ async function handlePrepareLocked(argv) {
357
331
  let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
358
332
  const recordPath = path.join(artifactsDir, READINESS_RECORD);
359
333
  const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
360
- const chain = stepChain(platform);
334
+ const chain = stepChain(surface);
361
335
  const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
362
336
  view?.begin();
363
337
  const steps = [];
@@ -406,35 +380,33 @@ async function handlePrepareLocked(argv) {
406
380
  try {
407
381
  await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
408
382
  await run("status", ["status", "--json", ...targetArgs]);
409
- if (platform === "mobile" && !mobilePlatform && !failed()) {
410
- mobilePlatform = mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(readDevices(artifactsDir));
383
+ const { devicePlatform } = surface.prepare;
384
+ if (devicePlatform && !mobilePlatform && !failed()) {
385
+ mobilePlatform = devicePlatform(target, readDevices(artifactsDir));
411
386
  if (!mobilePlatform) return ambiguousMobileTarget(json);
412
387
  }
413
- if (platform === "core") {
414
- const reason = "core is headless: no app surface to launch or seed";
388
+ if (surface.headless) {
389
+ const reason = `${platform} is headless: no app surface to launch or seed`;
415
390
  addStep(skippedStep("launch", `${executable} launch`, reason));
416
391
  addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
417
- } else if (platform === "mobile") {
418
- const device = mobilePlatform ? [mobilePlatform] : [];
392
+ } else {
393
+ const device = devicePlatform && mobilePlatform ? [mobilePlatform] : [];
419
394
  await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
420
395
  await run("fixtures", [
421
396
  "fixtures",
422
397
  "set",
423
- ...mobilePlatform ? ["--platform", mobilePlatform] : [],
398
+ ...device.flatMap((platformName) => ["--platform", platformName]),
424
399
  ...remoteFlagArgs,
425
400
  "--json",
426
401
  ...targetArgs
427
402
  ]);
428
- } else {
429
- await run("launch", ["launch", ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
430
- await run("fixtures", ["fixtures", "set", ...remoteFlagArgs, "--json", ...targetArgs]);
431
403
  }
432
404
  await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
433
405
  } finally {
434
406
  clearInterval(heartbeat);
435
407
  view?.end();
436
408
  }
437
- const provenance = runnerProvenance(manifestPath(platform));
409
+ const provenance = runnerProvenance(surface.actions.manifestPath());
438
410
  const record = {
439
411
  schemaVersion: 1,
440
412
  harness: {
@@ -462,7 +434,5 @@ async function handlePrepareLocked(argv) {
462
434
  }
463
435
  export {
464
436
  handlePrepare,
465
- mobilePlatformFromConfiguredTarget,
466
- mobilePlatformFromDevices,
467
437
  mobileTargetAmbiguityError
468
438
  };
@@ -1,8 +1,7 @@
1
1
  import { detectAdapter } from "../harness.js";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
- import { clearMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
5
- import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
4
+ import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
6
5
  import {
7
6
  applyWatcherPortOption,
8
7
  optionFlag,
@@ -27,10 +26,7 @@ async function handleProvision({ positional, options, rawArgv }) {
27
26
  const surface = getAdapterSurface(adapter);
28
27
  const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
29
28
  const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
30
- if (adapter === "mobile") {
31
- writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
32
- if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
33
- }
29
+ surface.runwayProvision.before?.(target, { json, platform, resolveOnly: optionFlag(options, "resolveOnly") });
34
30
  const result = await surface.runwayProvision.run(target, {
35
31
  json,
36
32
  platform,
@@ -50,7 +46,7 @@ async function handleProvision({ positional, options, rawArgv }) {
50
46
  });
51
47
  const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
52
48
  const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
53
- const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
49
+ const next = result.status === "pass" && surface.runwayProvision.launchNext && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter ${adapter} --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
54
50
  const output = next ? { ...result, next } : result;
55
51
  if (json) {
56
52
  console.log(JSON.stringify(output, null, 2));
@@ -1,13 +1,9 @@
1
- import path from "node:path";
2
- import { runnerDir } from "../paths.js";
3
1
  import { getAdapterSurface } from "../adapters/surface.js";
4
2
  import {
5
3
  ADAPTER_DETECT_NEXT,
6
- EXIT,
7
4
  flag,
8
5
  parseFlags,
9
6
  resolveAdapter,
10
- spawnScript,
11
7
  targetOf,
12
8
  usageOut
13
9
  } from "./shared.js";
@@ -25,55 +21,20 @@ async function handleReload(argv) {
25
21
  ADAPTER_DETECT_NEXT
26
22
  );
27
23
  }
28
- if (adapter === "core") {
24
+ const surface = getAdapterSurface(adapter);
25
+ if (surface.headless) {
29
26
  return usageOut(
30
27
  json,
31
28
  "reload",
32
- "core is headless; there is no app runtime to reload.",
33
- "mm-harness run <core-recipe>"
29
+ `${adapter} is headless; there is no app runtime to reload.`,
30
+ `mm-harness run <${adapter}-recipe>`
34
31
  );
35
32
  }
36
- getAdapterSurface(adapter).resolveSlotPorts(target);
37
- if (adapter === "extension") {
38
- if (!process.env.CDP_PORT) {
39
- return usageOut(
40
- json,
41
- "reload",
42
- "the Extension CDP port could not be resolved.",
43
- "mm-harness launch"
44
- );
45
- }
46
- const script2 = path.join(runnerDir, "adapters/extension/reattach.sh");
47
- const args2 = ["--target", target, "--cdp-port", process.env.CDP_PORT];
48
- if (process.env.WATCHER_PORT) {
49
- args2.push("--watcher-port", process.env.WATCHER_PORT);
50
- }
51
- const result2 = spawnScript(script2, args2, target, json);
52
- if (json) {
53
- console.log(
54
- JSON.stringify(
55
- {
56
- ok: result2.status === 0,
57
- adapter,
58
- method: "cdp-reattach",
59
- command: "reload",
60
- cdpPort: Number(process.env.CDP_PORT),
61
- ...result2.status === 0 ? {} : { error: result2.output.slice(-4e3) || "Extension reload failed." }
62
- },
63
- null,
64
- 2
65
- )
66
- );
67
- }
68
- return result2.status === 0 ? EXIT.ok : EXIT.runtime;
33
+ if (!surface.reload) {
34
+ return usageOut(json, "reload", `reload does not support the ${adapter} adapter.`, surface.hints.launch);
69
35
  }
70
- const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
71
- const args = [];
72
- if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
73
- if (json) args.push("--json");
74
- const result = spawnScript(process.execPath, [script, ...args], target, json);
75
- if (json) process.stdout.write(result.output);
76
- return result.status === 0 ? EXIT.ok : EXIT.runtime;
36
+ surface.resolveSlotPorts(target);
37
+ return surface.reload(target, json);
77
38
  }
78
39
  export {
79
40
  handleReload
@@ -38,11 +38,8 @@ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run
38
38
  import { recipeTrustFailure } from "../recipe-security.js";
39
39
  import { ProvenanceDriftError } from "../execution-provenance.js";
40
40
  import { recordCommandEvidence } from "../command-journal.js";
41
- import {
42
- actionLibraryContextArgs,
43
- missingActionCapabilities,
44
- resolveActionCapabilityMatrix
45
- } from "./manifest.js";
41
+ import { actionLibraryContextArgs, resolveActionCapabilityMatrix } from "./manifest.js";
42
+ import { missingActionCapabilities } from "@farmslot/recipe-cli";
46
43
  import {
47
44
  startRunNetworkObservation
48
45
  } from "../network-observation.js";
@@ -1,32 +1,16 @@
1
- import { decideExtensionReadiness } from "../adapters/extension/runtime-decision.js";
2
- import { decideMobileReadiness } from "../adapters/mobile/runtime-decision.js";
1
+ import { getAdapterSurface } from "../adapters/surface.js";
3
2
  import {
4
3
  adapterOption,
5
4
  applyRuntimeDirOption,
6
5
  optionFlag,
7
- optionString,
8
- parsePort,
9
6
  targetPath
10
7
  } from "./parse-args.js";
11
8
  async function handleRuntimeDecision({ options }) {
12
9
  applyRuntimeDirOption(options);
13
10
  const adapter = adapterOption(options);
14
11
  const target = targetPath(options);
15
- if (adapter === "mobile") {
16
- const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
17
- const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
18
- const report2 = await decideMobileReadiness(target, {
19
- watcherPort,
20
- metroLog: optionString(options, "metroLog"),
21
- platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
22
- record: optionFlag(options, "record"),
23
- preflightMode: optionString(options, "preflightMode")
24
- });
25
- if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
26
- else console.log(`${report2.decision} ${report2.reasonCode} \u2014 ${report2.reasons[0] ?? ""}`);
27
- return 0;
28
- }
29
- if (adapter !== "extension") {
12
+ const decide = getAdapterSurface(adapter).runtimeDecision;
13
+ if (!decide) {
30
14
  const report2 = {
31
15
  schemaVersion: 1,
32
16
  adapter,
@@ -42,15 +26,9 @@ async function handleRuntimeDecision({ options }) {
42
26
  else console.log(`unknown adapter-unsupported \u2014 ${report2.reasons[0]}`);
43
27
  return 0;
44
28
  }
45
- const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
46
- const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
47
- const report = await decideExtensionReadiness(target, {
48
- cdpPort,
49
- watchLog: optionString(options, "watchLog"),
50
- record: optionFlag(options, "record")
51
- });
29
+ const { report, line } = await decide(target, options);
52
30
  if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
53
- else console.log(`${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}`);
31
+ else console.log(line);
54
32
  return 0;
55
33
  }
56
34
  export {
@@ -25,9 +25,7 @@ async function runSelfTest(options) {
25
25
  for (const adapter of ["mobile", "extension"]) {
26
26
  const manifest = loadActionManifest(adapter);
27
27
  const manifestValidation = await validateManifest(manifest);
28
- const smokeRecipe = recipePath(
29
- adapter === "mobile" ? "mobile/runner/smoke.recipe.json" : "extension/runner/smoke.recipe.json"
30
- );
28
+ const smokeRecipe = recipePath(`${adapter}/runner/smoke.recipe.json`);
31
29
  const artifactsDir = path.join(root, adapter);
32
30
  const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
33
31
  skipExtensionRuntimePrepare: true