@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.
- package/CHANGELOG.md +26 -0
- package/adapters/manifest.json +14 -30
- package/adapters/terminal/launch.mjs +17 -13
- package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
- package/adapters/terminal/lib/readiness.mjs +20 -12
- package/adapters/terminal/policy.mjs +35 -0
- package/adapters/terminal/verify.mjs +10 -3
- package/adapters/terminal/wallet-host.mjs +43 -225
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +112 -5
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +3 -0
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +8 -3
- package/dist/adapters/terminal/surface.js +2 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +31 -6
- package/dist/commands/check.js +30 -6
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/self-test.js +4 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/mm-harness-cli.js +3 -3
- package/dist/performance-observation.js +1 -3
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +2 -1
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/terminal/app/launch.mjs +3 -2
- package/library/actions/terminal/platform/hud.mjs +2 -1
- package/library/actions/terminal/platform/page.mjs +2 -1
- package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
- package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
- package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +4 -4
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- 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
|
|
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("
|
|
150
|
+
backend: async (target, env) => (await import("./network-observer.js")).createMobileNetworkBackend(target, env)
|
|
131
151
|
},
|
|
132
152
|
performance: {
|
|
133
|
-
|
|
134
|
-
|
|
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: () => ({
|
|
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("
|
|
43
|
+
backend: async (_target, env, artifactsDir) => (await import("../extension/network-observer.js")).createExtensionNetworkBackend(env, artifactsDir)
|
|
44
44
|
},
|
|
45
45
|
performance: {
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
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 === "
|
|
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)) {
|
package/dist/commands/check.js
CHANGED
|
@@ -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=
|
|
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=
|
|
193
|
-
addLines(runCommand("git", ["diff", "--cached", "--name-only", "--diff-filter=
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
+
};
|