@deeeed/metamask-harness 0.80.0 → 0.81.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 (76) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/adapters/manifest.json +14 -30
  3. package/adapters/terminal/launch.mjs +17 -13
  4. package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
  5. package/adapters/terminal/lib/readiness.mjs +20 -12
  6. package/adapters/terminal/policy.mjs +35 -0
  7. package/adapters/terminal/verify.mjs +10 -3
  8. package/adapters/terminal/wallet-host.mjs +43 -225
  9. package/dist/adapters/core/actions.js +4 -1
  10. package/dist/adapters/extension/actions.js +6 -2
  11. package/dist/adapters/extension/network-observer.js +13 -0
  12. package/dist/adapters/extension/run.js +112 -5
  13. package/dist/adapters/mobile/actions.js +4 -1
  14. package/dist/adapters/mobile/network-observer.js +68 -0
  15. package/dist/adapters/mobile/performance-observer.js +1 -3
  16. package/dist/adapters/mobile/run.js +32 -9
  17. package/dist/adapters/perps-order-inputs.js +36 -0
  18. package/dist/adapters/proof-failure.js +11 -0
  19. package/dist/adapters/terminal/actions.js +4 -1
  20. package/dist/adapters/terminal/doctor-checks.js +2 -0
  21. package/dist/adapters/terminal/launch.js +3 -0
  22. package/dist/adapters/terminal/overlay.js +8 -1
  23. package/dist/adapters/terminal/run.js +8 -3
  24. package/dist/adapters/terminal/surface.js +2 -0
  25. package/dist/adapters.js +20 -4
  26. package/dist/cli.js +31 -6
  27. package/dist/commands/check.js +30 -6
  28. package/dist/commands/completion-candidates.js +5 -5
  29. package/dist/commands/self-test.js +4 -3
  30. package/dist/engine.js +57 -0
  31. package/dist/funding-execution-context.js +81 -0
  32. package/dist/mm-harness-cli.js +3 -3
  33. package/dist/performance-observation.js +1 -3
  34. package/dist/recipe-assistance-context.js +6 -7
  35. package/dist/runner.js +2 -1
  36. package/docs/CONTRIBUTING.md +4 -2
  37. package/docs/RECIPES.md +31 -3
  38. package/library/actions/core/perps/_controller.mjs +24 -28
  39. package/library/actions/core/perps/_rewards.mjs +186 -0
  40. package/library/actions/core/perps/close_positions.mjs +1 -1
  41. package/library/actions/core/perps/place_order.mjs +167 -45
  42. package/library/actions/core/perps/quote_fees.mjs +7 -4
  43. package/library/actions/core/wallet/list_accounts.mjs +1 -1
  44. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  45. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  46. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  47. package/library/actions/extension/perps/perps.mjs +320 -3
  48. package/library/actions/extension/perps/place_order.mjs +3 -1
  49. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  50. package/library/actions/extension/platform/cdp.mjs +71 -3
  51. package/library/actions/mobile/perps/perps.mjs +216 -11
  52. package/library/actions/mobile/perps/place_order.mjs +3 -1
  53. package/library/actions/shared/perps/order-policy.mjs +177 -0
  54. package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
  55. package/library/actions/terminal/app/launch.mjs +3 -2
  56. package/library/actions/terminal/platform/hud.mjs +2 -1
  57. package/library/actions/terminal/platform/page.mjs +2 -1
  58. package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
  59. package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
  60. package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
  61. package/library/manifests/core.action-manifest.json +6 -2
  62. package/library/manifests/extension.action-manifest.json +4 -0
  63. package/library/manifests/mobile.action-manifest.json +12 -0
  64. package/package.json +4 -4
  65. package/site/assets/help-recipes.json +1 -0
  66. package/adapters/terminal/lib/origin.mjs +0 -30
  67. package/adapters/terminal/lib/page-script.mjs +0 -246
  68. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  69. package/dist/commands/call.js +0 -671
  70. package/dist/commands/list-executables.js +0 -245
  71. package/dist/commands/manifest.js +0 -477
  72. package/dist/commands/run-engine.js +0 -1301
  73. package/dist/commands/run.js +0 -730
  74. package/dist/discovery.js +0 -15
  75. package/dist/metamask-action-validation.js +0 -68
  76. package/dist/network-observation.js +0 -290
@@ -0,0 +1,68 @@
1
+ import path from "node:path";
2
+ import brokerModule from "@farmslot/recipe-runner/cdp-broker";
3
+ import configModule from "@farmslot/adapter-rn/bridge-runtime/lib/config.cjs";
4
+ const { brokerSocketPath, createBrokerClient } = brokerModule;
5
+ const { resolvePort } = configModule;
6
+ async function createMobileNetworkBackend(target, env) {
7
+ const { client } = await connectMobileBroker(target, env);
8
+ return {
9
+ start(params) {
10
+ return client.control("capture-start", params, 1e4);
11
+ },
12
+ async end(id) {
13
+ return asRecord(await client.control("capture-end", { id }, 1e4));
14
+ },
15
+ async close() {
16
+ client.close();
17
+ }
18
+ };
19
+ }
20
+ async function connectMobileBroker(target, env) {
21
+ const runtimeDir = env.RECIPE_RUNTIME_DIR ? path.resolve(target, env.RECIPE_RUNTIME_DIR) : path.join(target, "temp", "recipe", "runtime");
22
+ const socketPath = brokerSocketPath(
23
+ runtimeDir,
24
+ resolvePort(env, target)
25
+ );
26
+ const discoveryClient = await createBrokerClient(socketPath, "", 1e4);
27
+ let targets;
28
+ try {
29
+ targets = await discoveryClient.control(
30
+ "resolve-targets",
31
+ { nameIncludes: mobileTargetPin(env) },
32
+ 1e4
33
+ );
34
+ } finally {
35
+ discoveryClient.close();
36
+ }
37
+ const deviceId = selectMobileBrokerTarget(targets, env);
38
+ const client = await createBrokerClient(socketPath, deviceId, 1e4);
39
+ return { client, deviceId };
40
+ }
41
+ function selectMobileBrokerTarget(value, env) {
42
+ const targets = Array.isArray(value) ? value.map(asRecord).filter((target) => target.deviceId) : [];
43
+ const pin = mobileTargetPin(env);
44
+ const candidates = pin ? targets.filter(
45
+ (target) => String(target.name ?? "").toLowerCase().includes(pin.toLowerCase())
46
+ ) : targets;
47
+ if (candidates.length !== 1) {
48
+ throw new Error(
49
+ `Mobile network observation requires one broker target; found ${candidates.length}.`
50
+ );
51
+ }
52
+ return String(candidates[0].deviceId);
53
+ }
54
+ function mobileTargetPin(env) {
55
+ const platform = String(
56
+ env.RECIPE_RN_EXPLICIT_PLATFORM ?? env.RECIPE_HARNESS_PLATFORM ?? ""
57
+ ).toLowerCase();
58
+ return String(
59
+ platform === "android" ? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? "" : platform === "ios" ? env.IOS_SIMULATOR ?? "" : env.IOS_SIMULATOR ?? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? ""
60
+ ).trim();
61
+ }
62
+ function asRecord(value) {
63
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
64
+ }
65
+ export {
66
+ connectMobileBroker,
67
+ createMobileNetworkBackend
68
+ };
@@ -1,9 +1,7 @@
1
1
  import {
2
2
  createCdpTraceCollector
3
3
  } from "../performance/cdp-trace.js";
4
- import {
5
- connectMobileBroker
6
- } from "../../network-observation.js";
4
+ import { connectMobileBroker } from "./network-observer.js";
7
5
  import { createAndroidGfxinfoPerformanceBackend } from "./android-gfxinfo-performance.js";
8
6
  async function createMobilePerformanceBackend(target, env) {
9
7
  if (resolvePlatform(env) === "android") {
@@ -1,4 +1,4 @@
1
- import { isRecord } from "@farmslot/recipe-cli/harness";
1
+ import { EXIT, isRecord } from "@farmslot/recipe-cli/harness";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { IOS_SIMULATOR_RECORDING_TARGET_PREFIX } from "@farmslot/adapter-rn";
@@ -8,6 +8,7 @@ import {
8
8
  resolveAndroidDeviceSerial,
9
9
  resolveMobileIosSimulatorName
10
10
  } from "./recording-target.js";
11
+ import { emitProofFailure } from "../proof-failure.js";
11
12
  import { mobileAppIdentityEnv } from "./app-identity.js";
12
13
  import { requireCurrentMobileReleaseArtifactState } from "./release-artifact-state.js";
13
14
  const EXPLICIT_DEVICE_KEYS = [
@@ -51,7 +52,10 @@ const mobileRun = {
51
52
  ADB_SERIAL: process.env.ADB_SERIAL,
52
53
  ANDROID_SERIAL: process.env.ANDROID_SERIAL,
53
54
  ...mobileAppIdentityEnv(),
54
- METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME: process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME
55
+ METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME: process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME,
56
+ // The platform `--platform`/`--device` pinned; the React Native bridge
57
+ // matches its target against it.
58
+ RECIPE_RN_EXPLICIT_PLATFORM: process.env.RECIPE_RN_EXPLICIT_PLATFORM
55
59
  }),
56
60
  // An installed release artifact is opaque: no harness HUD inside it.
57
61
  autoHud: () => process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1",
@@ -65,12 +69,28 @@ const mobileRun = {
65
69
  // source only matters for a development build the caller did not restart.
66
70
  runtimeCheck({ target, heal, state, json, appRestartAuthored }) {
67
71
  if (requireCurrentMobileReleaseArtifactState(target) !== null || appRestartAuthored) return void 0;
68
- return async () => {
69
- const { ensureMobileProofRuntime } = await import("../../commands/run-engine.js");
70
- return ensureMobileProofRuntime(target, heal, state, json);
71
- };
72
+ return () => ensureMobileProofRuntime(target, heal, state, json);
72
73
  }
73
74
  };
75
+ async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
76
+ const source = await import("./source-freshness.js");
77
+ const check = deps.check ?? (() => source.mobileSourceCheck(target));
78
+ const initial = check();
79
+ if (initial.status === "unavailable") {
80
+ emitProofFailure(json, { recoverable: false, error: initial.error });
81
+ return EXIT.usage;
82
+ }
83
+ if (initial.status === "current") return null;
84
+ const targetArg = JSON.stringify(path.resolve(target));
85
+ const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
86
+ const message = `Mobile loaded source is not current (${initial.status}).`;
87
+ emitProofFailure(json, {
88
+ recoverable: true,
89
+ mutations: state.mutations,
90
+ error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
91
+ });
92
+ return EXIT.infra;
93
+ }
74
94
  const mobileRecording = {
75
95
  target: (context) => resolveMobileRecordingTarget(context),
76
96
  // An Android device records through the mirror, with screenrecord as the
@@ -127,11 +147,13 @@ const mobileDiagnostics = {
127
147
  };
128
148
  const mobileObservation = {
129
149
  network: {
130
- backend: async (target, env) => (await import("../../network-observation.js")).createMobileNetworkBackend(target, env)
150
+ backend: async (target, env) => (await import("./network-observer.js")).createMobileNetworkBackend(target, env)
131
151
  },
132
152
  performance: {
133
- backend: async (target, env) => (await import("./performance-observer.js")).createMobilePerformanceBackend(target, env),
134
- platform: (env) => devicePlatformFromEnv(env)
153
+ start: async (context) => (await import("../../performance-observation.js")).startRunPerformanceObservation({
154
+ backend: async (target, env) => (await import("./performance-observer.js")).createMobilePerformanceBackend(target, env),
155
+ platform: (env) => devicePlatformFromEnv(env)
156
+ }, context)
135
157
  }
136
158
  };
137
159
  function runMobileIssueCommand(projectRoot, command) {
@@ -150,6 +172,7 @@ function runMobileIssueCommand(projectRoot, command) {
150
172
  }
151
173
  }
152
174
  export {
175
+ ensureMobileProofRuntime,
153
176
  mobileDiagnostics,
154
177
  mobileObservation,
155
178
  mobileRecording,
@@ -0,0 +1,36 @@
1
+ function perpsOrderInputFindings(nodeId, node, { ensureChecksOnly }) {
2
+ const action = node.action;
3
+ const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
4
+ const createsOrder = action === "metamask.perps.place_order" || !ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
5
+ if (!createsOrder) return [];
6
+ const findings = [];
7
+ const requireOneOf = (names, message) => {
8
+ const present = names.some((name) => {
9
+ const value = node[name];
10
+ return value !== void 0 && value !== null && value !== "";
11
+ });
12
+ if (present) return;
13
+ findings.push({
14
+ severity: "error",
15
+ code: "recipe.missing_param",
16
+ path: `workflow.nodes.${nodeId}.${names[0]}`,
17
+ message
18
+ });
19
+ };
20
+ requireOneOf(
21
+ ["market", "symbol"],
22
+ `${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
23
+ );
24
+ requireOneOf(
25
+ ["side"],
26
+ `${action} requires side=long or side=short before it may create an order`
27
+ );
28
+ requireOneOf(
29
+ ["amount", "notional"],
30
+ `${action} requires amount=<usd> or notional=<usd> before it may create an order`
31
+ );
32
+ return findings;
33
+ }
34
+ export {
35
+ perpsOrderInputFindings
36
+ };
@@ -0,0 +1,11 @@
1
+ function emitProofFailure(json, failure) {
2
+ if (json) {
3
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", ...failure }, null, 2));
4
+ return;
5
+ }
6
+ console.error(`\u2717 ${failure.error.message}`);
7
+ console.error(` Next: ${failure.error.userAction}`);
8
+ }
9
+ export {
10
+ emitProofFailure
11
+ };
@@ -1,4 +1,5 @@
1
1
  import { manifestPath } from "../../paths.js";
2
+ import { perpsOrderInputFindings } from "../perps-order-inputs.js";
2
3
  const terminalActions = {
3
4
  manifestPath: () => manifestPath("terminal"),
4
5
  semantic: [
@@ -36,7 +37,9 @@ const terminalActions = {
36
37
  }
37
38
  return standard();
38
39
  }
39
- })
40
+ }),
41
+ // ensure_positions and ensure_orders with an open or present state place an order.
42
+ inputFindings: (nodeId, node) => perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: false })
40
43
  };
41
44
  export {
42
45
  terminalActions
@@ -2,7 +2,9 @@ import { terminalAppPort, terminalCdpPort } from "./surface.js";
2
2
  async function terminalDoctorChecks(target) {
3
3
  const { terminalReadiness } = await import("../../../adapters/terminal/lib/readiness.mjs");
4
4
  const { resolveBrowser } = await import("../../../adapters/terminal/launch.mjs");
5
+ const { policy } = await import("../../../adapters/terminal/policy.mjs");
5
6
  const report = await terminalReadiness({
7
+ policy,
6
8
  target,
7
9
  appPort: terminalAppPort(),
8
10
  cdpPort: terminalCdpPort(),
@@ -46,6 +46,9 @@ async function launchTerminal({ options, target, jsonOutput, stream, usage }) {
46
46
  cdpPort,
47
47
  "--app-port",
48
48
  appPort,
49
+ // The Terminal's venue policy (Hyperliquid hosts, refusal, start page).
50
+ "--policy-module",
51
+ path.join(runnerDir, "adapters", "terminal", "policy.mjs"),
49
52
  "--signer",
50
53
  str(options, "signer") ?? process.env.TERMINAL_SIGNER ?? "extension",
51
54
  "--account",
@@ -3,7 +3,14 @@ import { runnerDir } from "../../paths.js";
3
3
  const terminalHarness = {
4
4
  install: { entry: "adapters/terminal/inject.mjs", fallback: "adapters/terminal/inject.mjs", node: true },
5
5
  cleanup: { entry: "adapters/terminal/cleanup.mjs", fallback: "adapters/terminal/cleanup.mjs", node: true },
6
- verify: () => ({ command: process.execPath, prefixArgs: [path.join(runnerDir, "adapters", "terminal", "verify.mjs")] }),
6
+ verify: () => ({
7
+ command: process.execPath,
8
+ prefixArgs: [
9
+ path.join(runnerDir, "adapters", "terminal", "verify.mjs"),
10
+ "--policy-module",
11
+ path.join(runnerDir, "adapters", "terminal", "policy.mjs")
12
+ ]
13
+ }),
7
14
  restart: "mm-harness launch"
8
15
  };
9
16
  const terminalRuntimeContext = {
@@ -40,11 +40,16 @@ function walletRequestFindings(lines) {
40
40
  }
41
41
  const terminalObservation = {
42
42
  network: {
43
- backend: async (_target, env, artifactsDir) => (await import("../../network-observation.js")).createExtensionNetworkBackend(env, artifactsDir)
43
+ backend: async (_target, env, artifactsDir) => (await import("../extension/network-observer.js")).createExtensionNetworkBackend(env, artifactsDir)
44
44
  },
45
45
  performance: {
46
- backend: async (target, env, ports) => (await import("../../performance-observation.js")).createExtensionPerformanceObserver(target, env, ports),
47
- platform: (env) => devicePlatformFromEnv(env)
46
+ start: async (context) => {
47
+ const observation = await import("../../performance-observation.js");
48
+ return observation.startRunPerformanceObservation({
49
+ backend: (target, env, ports) => observation.createExtensionPerformanceObserver(target, env, ports),
50
+ platform: (env) => devicePlatformFromEnv(env)
51
+ }, context);
52
+ }
48
53
  }
49
54
  };
50
55
  export {
@@ -39,7 +39,9 @@ const terminalSurface = defineAdapter({
39
39
  },
40
40
  async runtimeStatus(target) {
41
41
  const { terminalReadiness: terminalReadiness2 } = await import("../../../adapters/terminal/lib/readiness.mjs");
42
+ const { policy } = await import("../../../adapters/terminal/policy.mjs");
42
43
  const report = await terminalReadiness2({
44
+ policy,
43
45
  target: path.resolve(target),
44
46
  appPort: terminalAppPort(),
45
47
  cdpPort: terminalCdpPort(),
package/dist/adapters.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import http from "node:http";
2
2
  import { createHash } from "node:crypto";
3
3
  import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
4
- import { runLiveAdapterScript } from "@farmslot/recipe-cli/harness";
4
+ import { liveAdapterProcessTimeoutMs, runLiveAdapterScript, runNetworkCaptureAction } from "@farmslot/recipe-cli/harness";
5
5
  import { trustedMutationContextExtras } from "./funding-execution-context.js";
6
6
  import { LIBRARY_ACTION_NAMESPACE } from "./paths.js";
7
7
  import { liveTsxCandidates } from "./live-tsx.js";
8
8
  import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
9
9
  import { isRecord, traceText } from "./action-values.js";
10
10
  import { getAdapterSurface } from "./adapters/surface.js";
11
- import { handleRunNetworkAction } from "./network-observation.js";
12
11
  import { handleRunPerformanceAction } from "./performance-observation.js";
13
12
  function simpleAdapter(action, executor) {
14
13
  return {
@@ -100,6 +99,12 @@ function liveRuntimeConfigured(platform, node) {
100
99
  if (!getAdapterSurface(platform).actions.liveNeedsCdpPort) return true;
101
100
  return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
102
101
  }
102
+ const IMMEDIATE_CLOSE_ALLOWANCE_MS = 18e4;
103
+ function withImmediateCloseBudget(action, node) {
104
+ if (action !== `${LIBRARY_ACTION_NAMESPACE}.perps.place_order`) return node;
105
+ if (node.close_immediately !== true && node.closeImmediately !== true) return node;
106
+ return { ...node, live_adapter_timeout_ms: liveAdapterProcessTimeoutMs(node) + IMMEDIATE_CLOSE_ALLOWANCE_MS };
107
+ }
103
108
  async function runLiveFirst(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
104
109
  if (node.allow_static_placeholder === true) {
105
110
  throw new Error(
@@ -111,7 +116,7 @@ async function runLiveFirst(platform, action, node, context, forceLive = false,
111
116
  platform,
112
117
  action,
113
118
  namespace: LIBRARY_ACTION_NAMESPACE,
114
- node,
119
+ node: withImmediateCloseBudget(action, node),
115
120
  context,
116
121
  prepared: preparedLiveAdapters?.get(action),
117
122
  contextExtras: trustedMutationContextExtras(context),
@@ -131,6 +136,15 @@ function liveAdapterPathHint(platform, action) {
131
136
  }
132
137
  return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
133
138
  }
139
+ async function handleRunNetworkAction(platform, action, node, context) {
140
+ if (!getAdapterSurface(platform).observation?.network?.actions) return null;
141
+ if (action === "app.network_assert") {
142
+ const assertion = await import("../library/actions/shared/app/network-assert.mjs");
143
+ return { output: await assertion.assertNetwork({ action, node, context }) };
144
+ }
145
+ if (action !== "app.network_capture") return null;
146
+ return runNetworkCaptureAction(platform, node, context);
147
+ }
134
148
  async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
135
149
  const performanceAction = await handleRunPerformanceAction(
136
150
  platform,
@@ -368,7 +382,9 @@ function createMetaMaskAdapters(adapter, declaredCustomActions = [], preparedLiv
368
382
  return adapters;
369
383
  }
370
384
  export {
385
+ IMMEDIATE_CLOSE_ALLOWANCE_MS,
371
386
  createMetaMaskAdapters,
372
387
  createMetaMaskUiTransport,
373
- normalizeUiWaitNode
388
+ normalizeUiWaitNode,
389
+ withImmediateCloseBudget
374
390
  };
package/dist/cli.js CHANGED
@@ -1,18 +1,24 @@
1
1
  import "./host.js";
2
2
  import "./adapters/surface.js";
3
+ import fs from "node:fs";
3
4
  import {
4
5
  detectAdapter,
6
+ handleActions,
7
+ handleCall,
5
8
  handleHarness,
6
9
  handleLast,
7
10
  handleLaunch,
8
11
  handleReload,
12
+ handleRun,
9
13
  handleStop,
10
14
  parseArgs,
11
15
  targetPath
12
16
  } from "@farmslot/recipe-cli/harness";
13
17
  import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
14
18
  import { adapterRegistry } from "./adapters/registry.js";
15
- import { handleActions } from "./commands/manifest.js";
19
+ import { applyDeviceTargeting } from "./commands/device-target.js";
20
+ import { metaMaskEngine } from "./engine.js";
21
+ import { walletFixturePath } from "./paths.js";
16
22
  import { handleDoctor } from "./commands/doctor.js";
17
23
  import { handleProvision } from "./commands/provision.js";
18
24
  import { handleRuntimeHealth } from "./commands/runtime-health.js";
@@ -20,9 +26,7 @@ import { handleRuntimeLaunch } from "./commands/runtime-launch.js";
20
26
  import { handleRuntimeDecision } from "./commands/runtime-decision.js";
21
27
  import { handleResolveExtension } from "./commands/resolve-extension.js";
22
28
  import { handleEnsureReady } from "./commands/ensure-ready.js";
23
- import { handleRun } from "./commands/run.js";
24
29
  import { handleSelfTest } from "./commands/self-test.js";
25
- import { handleCall } from "./commands/call.js";
26
30
  import { handleCompletionCandidates, invalidateCompletionCache } from "./commands/completion-candidates.js";
27
31
  import { handleLogs } from "./commands/logs.js";
28
32
  import { handleDebug } from "./commands/debug.js";
@@ -45,15 +49,35 @@ const stopOptions = {
45
49
  return collector.stopped && collector.pid !== null ? [{ label: "analytics collector", pid: collector.pid }] : [];
46
50
  }
47
51
  };
52
+ const targetDevice = (command, adapter, options) => applyDeviceTargeting(command, adapter, options, { gate: true, rerun: "" });
53
+ const runOptions = {
54
+ engine: metaMaskEngine,
55
+ targetDevice,
56
+ plan: {
57
+ // The wallet fixture `fixtures set` seeds.
58
+ steps: (target) => [{
59
+ step: "fixture.file",
60
+ confidence: "static",
61
+ status: "ok",
62
+ detail: fs.existsSync(walletFixturePath(target)) ? `present: ${walletFixturePath(target)}` : `absent: ${walletFixturePath(target)} (fixtures set to seed)`
63
+ }],
64
+ launchDetail: "would launch/attach the app + heal transport (Metro/Chrome/CDP) before executing"
65
+ }
66
+ };
67
+ const callOptions = {
68
+ engine: metaMaskEngine,
69
+ targetDevice,
70
+ // A wallet action makes the most useful first example.
71
+ exampleAction: (names) => names.find((name) => /wallet|unlock/u.test(name))
72
+ };
48
73
  const COMMANDS = {
49
- actions: handleActions,
74
+ actions: (parsed) => handleActions(parsed, { catalog: metaMaskEngine }),
50
75
  doctor: handleDoctor,
51
76
  "runtime-health": handleRuntimeHealth,
52
77
  "runtime-decision": handleRuntimeDecision,
53
78
  "runtime-launch": handleRuntimeLaunch,
54
79
  "resolve-extension": handleResolveExtension,
55
80
  "ensure-ready": handleEnsureReady,
56
- run: handleRun,
57
81
  "self-test": handleSelfTest
58
82
  };
59
83
  const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
@@ -114,7 +138,8 @@ async function main(argv) {
114
138
  if (command === "status" || command === "health" || command === "home") {
115
139
  return handleStatus(parseArgs(argv.slice(1)));
116
140
  }
117
- if (command === "call") return handleCall(argv.slice(1));
141
+ if (command === "run") return handleRun(argv.slice(1), runOptions);
142
+ if (command === "call") return handleCall(argv.slice(1), callOptions);
118
143
  if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
119
144
  if (command === "provision") return handleProvision(parseArgs(argv.slice(1)));
120
145
  if (OVERLAY_COMMANDS.includes(command)) {
@@ -8,6 +8,7 @@ const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
8
8
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
9
9
  const TEST_FILE_RE = /(?:^|[./_-])(?:test|spec)\.(?:c|m)?jsx?$|(?:^|[./_-])(?:test|spec)\.tsx?$/u;
10
10
  const ESLINT_SUPPRESSION_RE = /^\s*eslint-disable(?:-line|-next-line)?\b/u;
11
+ const EXTENSION_LOCALE_RE = /^app\/_locales\//u;
11
12
  async function handleCheck(argv) {
12
13
  const { positional, options } = parseArgs(argv);
13
14
  const action = positional[0];
@@ -52,10 +53,12 @@ async function handleCheck(argv) {
52
53
  const mergeBase = resolveMergeBase(target, baseRef);
53
54
  const changedFiles = listChangedFiles(target, mergeBase, artifactsDir);
54
55
  const existingChangedFiles = changedFiles.filter((file) => fs.existsSync(path.join(target, file)));
56
+ const untracked = untrackedFiles(target);
55
57
  const suppressionCheck = checkNewEslintSuppressions({
56
58
  target,
57
59
  artifactsDir,
58
60
  mergeBase,
61
+ untracked,
59
62
  files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file))
60
63
  });
61
64
  const policyFailed = suppressionCheck.status === "fail";
@@ -97,6 +100,9 @@ async function handleCheck(argv) {
97
100
  files: existingChangedFiles.filter((file) => TEST_FILE_RE.test(file))
98
101
  })
99
102
  );
103
+ if (adapter === "extension") {
104
+ checks.push(...runExtensionDiffChecks({ target, artifactsDir, packageJson, files: changedFiles }));
105
+ }
100
106
  if (profile === "full") {
101
107
  checks.push(runTypecheck({ target, artifactsDir, packageJson }));
102
108
  } else {
@@ -187,10 +193,10 @@ function listChangedFiles(target, mergeBase, artifactsDir) {
187
193
  }
188
194
  };
189
195
  if (mergeBase) {
190
- addLines(runCommand("git", ["diff", "--name-only", "--diff-filter=ACMR", `${mergeBase}...HEAD`], target).stdout);
196
+ addLines(runCommand("git", ["diff", "--name-only", "--no-renames", "--diff-filter=ACMRD", `${mergeBase}...HEAD`], target).stdout);
191
197
  }
192
- addLines(runCommand("git", ["diff", "--name-only", "--diff-filter=ACMR", "HEAD"], target).stdout);
193
- addLines(runCommand("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR"], target).stdout);
198
+ addLines(runCommand("git", ["diff", "--name-only", "--no-renames", "--diff-filter=ACMRD", "HEAD"], target).stdout);
199
+ addLines(runCommand("git", ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACMRD"], target).stdout);
194
200
  addLines(runCommand("git", ["ls-files", "--others", "--exclude-standard"], target).stdout);
195
201
  return [...files].filter((file) => !isUnderArtifactsDir(target, artifactsDir, file)).sort();
196
202
  }
@@ -209,9 +215,7 @@ function checkNewEslintSuppressions(input) {
209
215
  if (input.files.length === 0) {
210
216
  return { id, label, status: "skip", reason: "no changed JS/TS files" };
211
217
  }
212
- const untracked = new Set(
213
- runCommand("git", ["ls-files", "--others", "--exclude-standard"], input.target).stdout.split("\n").map((file) => normalizeRelativePath(file.trim())).filter(Boolean)
214
- );
218
+ const { untracked } = input;
215
219
  const findings = [];
216
220
  const trackedFiles = input.files.filter((file) => !untracked.has(normalizeRelativePath(file)));
217
221
  if (trackedFiles.length > 0) {
@@ -240,6 +244,26 @@ function checkNewEslintSuppressions(input) {
240
244
  reason: `${findings.length} newly added eslint-disable directive(s)`
241
245
  } : { id, label, status: "pass", logPath };
242
246
  }
247
+ function untrackedFiles(target) {
248
+ return new Set(
249
+ runCommand("git", ["ls-files", "--others", "--exclude-standard"], target).stdout.split("\n").map((file) => normalizeRelativePath(file.trim())).filter(Boolean)
250
+ );
251
+ }
252
+ function runExtensionDiffChecks(input) {
253
+ const scripts = input.packageJson?.scripts && typeof input.packageJson.scripts === "object" ? input.packageJson.scripts : {};
254
+ const results = [];
255
+ if (typeof scripts["verify-locales"] === "string") {
256
+ results.push(
257
+ input.files.some((file) => EXTENSION_LOCALE_RE.test(normalizeRelativePath(file))) ? runCheckCommand("verify-locales", "Locale strings", input.target, input.artifactsDir, "yarn", ["verify-locales", "--quiet"]) : { id: "verify-locales", label: "Locale strings", status: "skip", reason: "no app/_locales changes" }
258
+ );
259
+ }
260
+ if (typeof scripts["circular-deps:check"] === "string") {
261
+ results.push(
262
+ input.files.some((file) => JS_TS_EXT_RE.test(file)) ? runCheckCommand("circular-deps", "Circular dependencies", input.target, input.artifactsDir, "yarn", ["circular-deps:check"]) : { id: "circular-deps", label: "Circular dependencies", status: "skip", reason: "no JS/TS changes" }
263
+ );
264
+ }
265
+ return results;
266
+ }
243
267
  function findAddedSuppressions(diff) {
244
268
  const findings = [];
245
269
  let file = "";
@@ -1,4 +1,4 @@
1
- import { EXIT, parseArgs, optionFlag, optionString, optionStrings, resolveAdapter, targetPath } from "@farmslot/recipe-cli/harness";
1
+ import { EXIT, listRunnableRecipes, parseArgs, optionFlag, optionString, optionStrings, resolveAdapter, resolveLibrarySources, targetPath } from "@farmslot/recipe-cli/harness";
2
2
  import { resolveActionManifest } from "../manifest.js";
3
3
  import {
4
4
  invalidateCompletionCache,
@@ -6,7 +6,7 @@ import {
6
6
  writeCompletionCandidates
7
7
  } from "../completions-cache.js";
8
8
  import { importRecipeProtocol } from "../paths.js";
9
- import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
9
+ import { metaMaskEngine } from "../engine.js";
10
10
  async function handleCompletionCandidates(argv) {
11
11
  const kind = argv[0];
12
12
  const { options } = parseArgs(argv.slice(1));
@@ -20,7 +20,7 @@ async function handleCompletionCandidates(argv) {
20
20
  let candidates;
21
21
  if (kind === "actions") {
22
22
  adapter = resolveAdapter(options).adapter;
23
- const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
23
+ const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(options, "library"));
24
24
  const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
25
25
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
26
26
  candidates = getRecipeActionManifestActionNames(manifest);
@@ -30,8 +30,8 @@ async function handleCompletionCandidates(argv) {
30
30
  const cacheKey = `recipes:${adapter}`;
31
31
  candidates = readFreshCandidates(target, cacheKey);
32
32
  if (!candidates) {
33
- const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library")).catch(() => void 0);
34
- candidates = sources ? (await listRunnableRecipes(adapter, sources)).map((recipe) => recipe.name) : [];
33
+ const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(options, "library")).catch(() => void 0);
34
+ candidates = sources ? (await listRunnableRecipes(metaMaskEngine, adapter, sources)).map((recipe) => recipe.name) : [];
35
35
  writeCompletionCandidates(target, cacheKey, candidates);
36
36
  }
37
37
  }
@@ -1,10 +1,10 @@
1
- import { optionFlag, optionString, requiredOption } from "@farmslot/recipe-cli/harness";
1
+ import { optionFlag, optionString, requiredOption, runRecipe } from "@farmslot/recipe-cli/harness";
2
2
  import { mkdtemp } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { loadActionManifest, validateManifest } from "../manifest.js";
6
6
  import { recipePath, runnerDir } from "../paths.js";
7
- import { runRecipe } from "./run-engine.js";
7
+ import { metaMaskEngine } from "../engine.js";
8
8
  async function handleSelfTest({ options }) {
9
9
  const result = await runSelfTest(options);
10
10
  if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
@@ -23,7 +23,8 @@ async function runSelfTest(options) {
23
23
  const manifestValidation = await validateManifest(manifest);
24
24
  const smokeRecipe = recipePath(`${adapter}/runner/smoke.recipe.json`);
25
25
  const artifactsDir = path.join(root, adapter);
26
- const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
26
+ const result = await runRecipe(metaMaskEngine, adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
27
+ cli: { adapter },
27
28
  platform: { skipExtensionRuntimePrepare: true }
28
29
  });
29
30
  runs.push({ adapter, manifestValidation: manifestValidation.summary, artifactsDir, result });
package/dist/engine.js ADDED
@@ -0,0 +1,57 @@
1
+ import path from "node:path";
2
+ import { resolveAdapter } from "@farmslot/recipe-cli/harness";
3
+ import { getAdapterSurface } from "./adapters/surface.js";
4
+ import { consoleClassifier } from "./console-classifier.js";
5
+ import {
6
+ authorizeTrustedMutationPlan,
7
+ fixturePolicyMutationBase,
8
+ fundingOptionsFromCli,
9
+ isFixturePolicyBase,
10
+ loadTrustedMutationBase
11
+ } from "./funding-execution-context.js";
12
+ import { resolveActionManifest, validateManifest } from "./manifest.js";
13
+ import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
14
+ import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
15
+ const metaMaskEngine = {
16
+ bundledLibrary: {
17
+ name: "metamask",
18
+ root: path.resolve(runnerDir, "library"),
19
+ actionNamespace: LIBRARY_ACTION_NAMESPACE
20
+ },
21
+ resolveActionManifest,
22
+ validateManifest,
23
+ actionCapabilities: metaMaskActionExecutionCapabilities,
24
+ createRunner: async (adapter, manifest, { trustedMutation, ...options }) => {
25
+ const { createMetaMaskRunner } = await import("./runner.js");
26
+ return trustedMutation === NO_PLAN_MUTATION ? createMetaMaskRunner(adapter, manifest, { ...options, suppressLibraryResolutionLogs: true }) : createMetaMaskRunner(adapter, manifest, { ...options, trustedMutation });
27
+ },
28
+ // Funding files from the run's command line, bound to the run's plan. Without
29
+ // them, a platform whose actions take a trusted mutation (the Extension) runs a
30
+ // plan with external mutations in the fixture-policy lane, where each action
31
+ // applies the shared order policy; any other plan runs without one.
32
+ trustedMutation: {
33
+ load: async ({ cli, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (takesTrustedMutation(cli) ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
34
+ authorize: async (base, plan) => {
35
+ if (!base || base === NO_PLAN_MUTATION) return void 0;
36
+ if (isFixturePolicyBase(base) && !plan.nodes.some((node) => node.capabilities.includes("external-mutation"))) {
37
+ return NO_PLAN_MUTATION;
38
+ }
39
+ return authorizeTrustedMutationPlan(base, plan);
40
+ }
41
+ },
42
+ console: consoleClassifier,
43
+ runnerIncludes: ["src", "adapters", "library", "bin", "dist", "package.json"]
44
+ };
45
+ const NO_PLAN_MUTATION = /* @__PURE__ */ Symbol("no plan mutation");
46
+ function takesTrustedMutation(cli) {
47
+ let adapter;
48
+ try {
49
+ ({ adapter } = resolveAdapter(cli));
50
+ } catch {
51
+ return false;
52
+ }
53
+ return getAdapterSurface(adapter).actions.trustedMutation === true;
54
+ }
55
+ export {
56
+ metaMaskEngine
57
+ };