@deeeed/metamask-harness 0.17.5 → 0.19.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 +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- package/library/flows/perps.flows.json +0 -64
package/dist/manifest.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
|
|
4
|
+
import {
|
|
5
|
+
actionSourceIntegrity,
|
|
6
|
+
metaMaskActionExecutionCapabilities
|
|
7
|
+
} from "./recipe-security.js";
|
|
3
8
|
function loadMetaMaskMobileActionManifest() {
|
|
4
9
|
return asActionManifest(readJson(manifestPath("mobile")));
|
|
5
10
|
}
|
|
@@ -15,6 +20,130 @@ function loadActionManifest(adapter, overridePath) {
|
|
|
15
20
|
if (adapter === "core") return loadMetaMaskCoreActionManifest();
|
|
16
21
|
return loadMetaMaskExtensionActionManifest();
|
|
17
22
|
}
|
|
23
|
+
async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
|
|
24
|
+
if (overridePath) {
|
|
25
|
+
const manifestPath2 = path.resolve(overridePath);
|
|
26
|
+
const implementationRoot = path.resolve(
|
|
27
|
+
taskActionRoot ?? defaultImplementationRoot(manifestPath2)
|
|
28
|
+
);
|
|
29
|
+
const manifest = withMetaMaskExecutionCapabilities(loadActionManifest(adapter, manifestPath2));
|
|
30
|
+
await validateManifest(manifest);
|
|
31
|
+
const integrity = await actionSourceIntegrity(manifestPath2, "task", implementationRoot);
|
|
32
|
+
return {
|
|
33
|
+
manifest,
|
|
34
|
+
actionSources: describeManifestSources(manifest, {
|
|
35
|
+
name: "task",
|
|
36
|
+
tier: "task",
|
|
37
|
+
manifestPath: manifestPath2,
|
|
38
|
+
implementationRoot,
|
|
39
|
+
trust: "unknown",
|
|
40
|
+
...integrity
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const canonicalPath = manifestPath(adapter);
|
|
45
|
+
const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }];
|
|
46
|
+
const manifests = [];
|
|
47
|
+
for (const source of sources) {
|
|
48
|
+
const file = path.join(source.root, "manifests", `${adapter}.action-manifest.json`);
|
|
49
|
+
if (!fs.existsSync(file)) continue;
|
|
50
|
+
const manifest = asActionManifest(readJson(file));
|
|
51
|
+
await validateManifest(manifest);
|
|
52
|
+
manifests.push({ source, file, manifest });
|
|
53
|
+
}
|
|
54
|
+
if (!manifests.some((entry) => path.resolve(entry.file) === path.resolve(canonicalPath))) {
|
|
55
|
+
const manifest = loadActionManifest(adapter);
|
|
56
|
+
await validateManifest(manifest);
|
|
57
|
+
manifests.push({ source: { name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }, file: canonicalPath, manifest });
|
|
58
|
+
}
|
|
59
|
+
const canonical = manifests.find((entry) => entry.source.name === "metamask") ?? manifests.at(-1);
|
|
60
|
+
if (!canonical) throw new Error(`No ${adapter} action manifest could be resolved.`);
|
|
61
|
+
const base = cloneManifest(canonical.manifest);
|
|
62
|
+
const official = new Set(stringArray(recordOf(base).supported_official_actions));
|
|
63
|
+
const custom = /* @__PURE__ */ new Map();
|
|
64
|
+
const customMetadata = /* @__PURE__ */ new Map();
|
|
65
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
66
|
+
const canonicalImplementationRoot = path.join(canonical.source.root, "actions");
|
|
67
|
+
const canonicalIntegrity = await actionSourceIntegrity(
|
|
68
|
+
canonical.file,
|
|
69
|
+
"canonical",
|
|
70
|
+
canonicalImplementationRoot
|
|
71
|
+
);
|
|
72
|
+
const actionSources = describeManifestSources(base, {
|
|
73
|
+
name: "metamask",
|
|
74
|
+
tier: "canonical",
|
|
75
|
+
manifestPath: canonical.file,
|
|
76
|
+
implementationRoot: canonicalImplementationRoot,
|
|
77
|
+
trust: "trusted",
|
|
78
|
+
...canonicalIntegrity
|
|
79
|
+
});
|
|
80
|
+
for (const entry of manifests) {
|
|
81
|
+
const sourceName = entry.source.name ?? path.basename(entry.source.root);
|
|
82
|
+
const tier = sourceName === "metamask" ? "canonical" : sourceName === "personal" ? "personal" : "team";
|
|
83
|
+
const implementationRoot = path.join(entry.source.root, "actions");
|
|
84
|
+
const integrity = await actionSourceIntegrity(entry.file, tier, implementationRoot);
|
|
85
|
+
const sourceInfo = {
|
|
86
|
+
name: sourceName,
|
|
87
|
+
tier,
|
|
88
|
+
manifestPath: entry.file,
|
|
89
|
+
implementationRoot,
|
|
90
|
+
trust: entry.source.provenance?.trust ?? (tier === "canonical" ? "trusted" : "unknown"),
|
|
91
|
+
...integrity
|
|
92
|
+
};
|
|
93
|
+
const manifestRecord = recordOf(entry.manifest);
|
|
94
|
+
const metadata = recordOf(manifestRecord.action_metadata);
|
|
95
|
+
for (const action of customActions(manifestRecord.custom_actions)) {
|
|
96
|
+
if (official.has(action.name)) {
|
|
97
|
+
throw new Error(`Library action ${action.name} from ${entry.file} conflicts with an official action.`);
|
|
98
|
+
}
|
|
99
|
+
if (custom.has(action.name)) {
|
|
100
|
+
const existing = actionSources.get(action.name);
|
|
101
|
+
if (existing && existing.name !== sourceName) {
|
|
102
|
+
existing.shadows = [...existing.shadows ?? [], sourceName];
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
custom.set(action.name, action.value);
|
|
107
|
+
customMetadata.set(action.name, metadata[action.name]);
|
|
108
|
+
actionSources.set(action.name, { ...sourceInfo });
|
|
109
|
+
}
|
|
110
|
+
for (const binding of objectArray(manifestRecord.native_bindings)) {
|
|
111
|
+
const action = typeof binding.action === "string" ? binding.action : void 0;
|
|
112
|
+
if (action && !bindings.has(action)) bindings.set(action, binding);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const baseRecord = recordOf(base);
|
|
116
|
+
baseRecord.custom_actions = [...custom.values()];
|
|
117
|
+
const baseMetadata = recordOf(baseRecord.action_metadata);
|
|
118
|
+
for (const [name, metadata] of customMetadata) {
|
|
119
|
+
if (metadata !== void 0) baseMetadata[name] = metadata;
|
|
120
|
+
}
|
|
121
|
+
baseRecord.action_metadata = baseMetadata;
|
|
122
|
+
baseRecord.native_bindings = [...bindings.values()];
|
|
123
|
+
return { manifest: withMetaMaskExecutionCapabilities(base), actionSources };
|
|
124
|
+
}
|
|
125
|
+
function defaultImplementationRoot(selectedManifestPath) {
|
|
126
|
+
const manifestDirectory = path.dirname(selectedManifestPath);
|
|
127
|
+
return path.basename(manifestDirectory) === "manifests" ? path.join(path.dirname(manifestDirectory), "actions") : path.join(manifestDirectory, "actions");
|
|
128
|
+
}
|
|
129
|
+
function withMetaMaskExecutionCapabilities(manifest) {
|
|
130
|
+
const result = cloneManifest(manifest);
|
|
131
|
+
const record = recordOf(result);
|
|
132
|
+
if (!Array.isArray(record.custom_actions)) return result;
|
|
133
|
+
record.custom_actions = record.custom_actions.map((entry) => {
|
|
134
|
+
const name = typeof entry === "string" ? entry : recordOf(entry).name;
|
|
135
|
+
if (typeof name !== "string") return entry;
|
|
136
|
+
const capabilities = metaMaskActionExecutionCapabilities(name);
|
|
137
|
+
if (capabilities.length === 0) return entry;
|
|
138
|
+
const declaredValue = typeof entry === "string" ? void 0 : recordOf(entry).execution_capabilities;
|
|
139
|
+
const declared = Array.isArray(declaredValue) ? declaredValue.filter(
|
|
140
|
+
(capability) => typeof capability === "string"
|
|
141
|
+
) : [];
|
|
142
|
+
const executionCapabilities = [.../* @__PURE__ */ new Set([...declared, ...capabilities])];
|
|
143
|
+
return typeof entry === "string" ? { name, execution_capabilities: executionCapabilities } : { ...recordOf(entry), execution_capabilities: executionCapabilities };
|
|
144
|
+
});
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
18
147
|
async function validateManifest(manifest) {
|
|
19
148
|
const { validateRecipeActionManifestDocument } = await importRecipeProtocol();
|
|
20
149
|
const result = validateRecipeActionManifestDocument(manifest);
|
|
@@ -28,10 +157,41 @@ async function validateManifest(manifest) {
|
|
|
28
157
|
function asActionManifest(value) {
|
|
29
158
|
return value;
|
|
30
159
|
}
|
|
160
|
+
function cloneManifest(value) {
|
|
161
|
+
return JSON.parse(JSON.stringify(value));
|
|
162
|
+
}
|
|
163
|
+
function recordOf(value) {
|
|
164
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
165
|
+
}
|
|
166
|
+
function stringArray(value) {
|
|
167
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
168
|
+
}
|
|
169
|
+
function objectArray(value) {
|
|
170
|
+
return Array.isArray(value) ? value.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
171
|
+
}
|
|
172
|
+
function customActions(value) {
|
|
173
|
+
if (!Array.isArray(value)) return [];
|
|
174
|
+
return value.flatMap((entry) => {
|
|
175
|
+
if (typeof entry === "string") return [{ name: entry, value: entry }];
|
|
176
|
+
const record = recordOf(entry);
|
|
177
|
+
return typeof record.name === "string" ? [{ name: record.name, value: entry }] : [];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function describeManifestSources(manifest, customSource) {
|
|
181
|
+
const record = recordOf(manifest);
|
|
182
|
+
const sources = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const name of stringArray(record.supported_official_actions)) {
|
|
184
|
+
sources.set(name, { name: "official", tier: "official", manifestPath: customSource.manifestPath });
|
|
185
|
+
}
|
|
186
|
+
for (const action of customActions(record.custom_actions)) sources.set(action.name, { ...customSource });
|
|
187
|
+
return sources;
|
|
188
|
+
}
|
|
31
189
|
export {
|
|
32
190
|
loadActionManifest,
|
|
33
191
|
loadMetaMaskCoreActionManifest,
|
|
34
192
|
loadMetaMaskExtensionActionManifest,
|
|
35
193
|
loadMetaMaskMobileActionManifest,
|
|
36
|
-
|
|
194
|
+
resolveActionManifest,
|
|
195
|
+
validateManifest,
|
|
196
|
+
withMetaMaskExecutionCapabilities
|
|
37
197
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -4,10 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { color } from "./cli-color.js";
|
|
7
|
+
import { withCommandJournal } from "./command-journal.js";
|
|
8
|
+
import { JsonStreamWriter } from "./json-stream.js";
|
|
7
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
8
10
|
import { handleCallHelp } from "./commands/call.js";
|
|
9
11
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
10
12
|
import { detectAdapter } from "./harness.js";
|
|
13
|
+
import {
|
|
14
|
+
PUBLIC_COMMAND_CONTRACTS,
|
|
15
|
+
publicCommandNames,
|
|
16
|
+
validatePublicInvocation
|
|
17
|
+
} from "./command-contract.js";
|
|
11
18
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
19
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
13
20
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -69,13 +76,16 @@ Example:
|
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
name: "actions",
|
|
72
|
-
summary: "
|
|
79
|
+
summary: "Discover typed single operations and their fields.",
|
|
73
80
|
example: "mm-harness actions --adapter mobile",
|
|
74
|
-
helpText: `mm-harness actions [flags]
|
|
81
|
+
helpText: `mm-harness actions [query] [flags]
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
Discover typed single operations and their field schemas for this checkout.
|
|
77
84
|
|
|
85
|
+
query Search names, categories, fields, and descriptions (typo-tolerant)
|
|
78
86
|
--action <name> Describe one action; fuzzy-resolves like call (short or full name)
|
|
87
|
+
--categories List compact action categories and counts
|
|
88
|
+
--category <name> List only one category (for example ui, wallet, or perps)
|
|
79
89
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
80
90
|
--target <path> Checkout path (default: cwd)
|
|
81
91
|
--raw Dump raw action registry JSON
|
|
@@ -83,6 +93,9 @@ Example:
|
|
|
83
93
|
|
|
84
94
|
Example:
|
|
85
95
|
mm-harness actions --adapter mobile
|
|
96
|
+
mm-harness actions positions --adapter mobile
|
|
97
|
+
mm-harness actions --adapter mobile --categories --json
|
|
98
|
+
mm-harness actions --adapter mobile --category ui --json
|
|
86
99
|
mm-harness actions --adapter mobile --action assert_orders
|
|
87
100
|
mm-harness actions --adapter extension --raw`
|
|
88
101
|
},
|
|
@@ -121,7 +134,7 @@ Example:
|
|
|
121
134
|
Actions differ per adapter \u2014 list this checkout's with:
|
|
122
135
|
mm-harness actions.
|
|
123
136
|
|
|
124
|
-
--list List
|
|
137
|
+
--list List actions accepted by call for this adapter; no <action> needed
|
|
125
138
|
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
126
139
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
|
|
127
140
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
@@ -137,46 +150,61 @@ Example (real actions; run mm-harness actions for this checkout's full set):
|
|
|
137
150
|
mm-harness call navigate page=perps --adapter mobile
|
|
138
151
|
mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
|
|
139
152
|
},
|
|
140
|
-
{
|
|
141
|
-
name: "flows",
|
|
142
|
-
summary: "Browse the reusable flow library (bare = list; `flows promote` publishes a flow up a tier). Resolves across libraries \u2014 personal > team > canonical, highest tier wins.",
|
|
143
|
-
example: "mm-harness flows",
|
|
144
|
-
helpText: `mm-harness flows [flags]
|
|
145
|
-
|
|
146
|
-
Browse the reusable flow library. Flows resolve across libraries by precedence
|
|
147
|
-
(personal > team > canonical); the highest-tier copy wins and shadows lower ones.
|
|
148
|
-
Flow resolution is adapter-global, so there is no --adapter flag here.
|
|
149
|
-
|
|
150
|
-
--library <name=path> Add/override a library source (repeatable)
|
|
151
|
-
--target <path> Checkout path (default: cwd)
|
|
152
|
-
--json Machine-readable output
|
|
153
|
-
|
|
154
|
-
Example:
|
|
155
|
-
mm-harness flows`
|
|
156
|
-
},
|
|
157
153
|
{
|
|
158
154
|
name: "run",
|
|
159
|
-
summary: "
|
|
155
|
+
summary: "Execute a complete proof and write its evidence.",
|
|
160
156
|
example: "mm-harness run recipe.json",
|
|
161
|
-
helpText: `mm-harness run <recipe.json> [flags]
|
|
157
|
+
helpText: `mm-harness run <recipe|recipe.json> [key=value ...] [flags]
|
|
162
158
|
|
|
163
|
-
Validate
|
|
159
|
+
Validate and execute a complete proof, then write summary, trace, diagnostics,
|
|
160
|
+
and artifacts. Inside a checkout, the adapter, target, ports, and artifact
|
|
161
|
+
directory are normally detected automatically.
|
|
164
162
|
|
|
165
|
-
|
|
163
|
+
Discovery:
|
|
164
|
+
--list List complete recipes accepted by run for this adapter; no <recipe> needed
|
|
165
|
+
--describe Show one recipe's parameters, composed actions/recipes, and runnable command
|
|
166
166
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
167
|
+
|
|
168
|
+
Common overrides:
|
|
167
169
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
|
|
168
170
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
171
|
+
--platform <ios|android> Mobile platform preference; use --adapter for the product adapter
|
|
169
172
|
--target <path> Checkout path (default: cwd)
|
|
170
|
-
--artifacts-dir <dir>
|
|
171
|
-
--action-manifest <path> Override the action manifest
|
|
173
|
+
--artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
|
|
172
174
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
173
175
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
174
176
|
--json Machine-readable output
|
|
177
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
175
178
|
--record-video=full-run Record a video of the run
|
|
176
179
|
|
|
180
|
+
Advanced integrations:
|
|
181
|
+
runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
|
|
182
|
+
--validation-runtime-dir, --launch-existing-dist
|
|
183
|
+
managed trust boundary (not needed for normal runs; Farmslot supplies it):
|
|
184
|
+
--source-trust, --source-kind, --source-name, --source-digest,
|
|
185
|
+
--approve-plan (binds approval to the exact reviewed plan)
|
|
186
|
+
schema: --action-manifest
|
|
187
|
+
|
|
177
188
|
Example:
|
|
178
|
-
mm-harness run
|
|
179
|
-
mm-harness run
|
|
189
|
+
mm-harness run wallet.smoke --describe
|
|
190
|
+
mm-harness run perps.clean-market-testnet market=BTC --plan
|
|
191
|
+
mm-harness run recipe.json market=ETH --adapter extension`
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "last",
|
|
195
|
+
summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
|
|
196
|
+
example: "mm-harness last --json",
|
|
197
|
+
helpText: `mm-harness last [flags]
|
|
198
|
+
|
|
199
|
+
Read the atomic per-checkout resumability journal. Discovery commands do not
|
|
200
|
+
replace it, and an interrupted process remains recorded as verdict=running.
|
|
201
|
+
|
|
202
|
+
--target <path> Checkout path (default: cwd)
|
|
203
|
+
--runtime-dir <dir> Runtime dir containing last-command.json
|
|
204
|
+
--json Machine-readable envelope
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
mm-harness last --json`
|
|
180
208
|
},
|
|
181
209
|
{
|
|
182
210
|
name: "doctor",
|
|
@@ -375,6 +403,7 @@ Example:
|
|
|
375
403
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
376
404
|
--target <path> Checkout path (default: cwd)
|
|
377
405
|
--json Machine-readable summary (recovered[] / mutations[] / phase)
|
|
406
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
378
407
|
|
|
379
408
|
Example:
|
|
380
409
|
mm-harness launch ios
|
|
@@ -393,8 +422,8 @@ Example:
|
|
|
393
422
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
394
423
|
--window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
|
|
395
424
|
--events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
|
|
396
|
-
--source <label> Log source per adapter \u2014 mobile: metro|app
|
|
397
|
-
extension: webpack|watcher|rebuild
|
|
425
|
+
--source <label> Log source per adapter \u2014 mobile: metro|app;
|
|
426
|
+
extension: extension|dapp|webpack|watcher|rebuild.
|
|
398
427
|
Core is headless (teaching error).
|
|
399
428
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
400
429
|
--target <path> Checkout path (default: cwd)
|
|
@@ -448,9 +477,10 @@ Example:
|
|
|
448
477
|
name: "fixtures",
|
|
449
478
|
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
|
|
450
479
|
example: "mm-harness fixtures set",
|
|
451
|
-
helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
|
|
480
|
+
helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
|
|
452
481
|
|
|
453
482
|
Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
|
|
483
|
+
With no action, show the current fixture status and safe next command.
|
|
454
484
|
init Create it from --from <path>, or explicitly choose --dev for a
|
|
455
485
|
disposable public test wallet that must never hold real funds.
|
|
456
486
|
sync Refresh the wallet fixture files on the target.
|
|
@@ -473,6 +503,7 @@ Example:
|
|
|
473
503
|
--cdp-port <port> finalize: CDP port of the running extension
|
|
474
504
|
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
475
505
|
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
506
|
+
--action-manifest <path> Extension set: override the wallet action manifest
|
|
476
507
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
477
508
|
--target <path> Checkout path (default: cwd)
|
|
478
509
|
--device <udid|serial|name> Mobile only: target this device for sync/set
|
|
@@ -487,21 +518,28 @@ Example:
|
|
|
487
518
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
488
519
|
}
|
|
489
520
|
];
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
|
|
502
|
-
identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
|
|
521
|
+
const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
|
|
522
|
+
REAL.map((command) => [command.name, command.example])
|
|
523
|
+
);
|
|
524
|
+
function assertPublicContractMatchesSurface() {
|
|
525
|
+
const registered = REAL.map((command) => command.name).sort();
|
|
526
|
+
const contracted = publicCommandNames().sort();
|
|
527
|
+
if (registered.join("\n") !== contracted.join("\n")) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
|
|
530
|
+
);
|
|
503
531
|
}
|
|
504
|
-
|
|
532
|
+
for (const command of REAL) {
|
|
533
|
+
const registeredAliases = [...command.aliases ?? []].sort();
|
|
534
|
+
const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
|
|
535
|
+
if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
assertPublicContractMatchesSurface();
|
|
505
543
|
const HELP_GROUPS = [
|
|
506
544
|
{
|
|
507
545
|
title: "DAILY LOOP",
|
|
@@ -510,13 +548,13 @@ const HELP_GROUPS = [
|
|
|
510
548
|
},
|
|
511
549
|
{
|
|
512
550
|
title: "DISCOVER",
|
|
513
|
-
blurb: "
|
|
514
|
-
commands: ["actions", "call"
|
|
551
|
+
blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
|
|
552
|
+
commands: ["actions", "call"]
|
|
515
553
|
},
|
|
516
554
|
{
|
|
517
555
|
title: "PROVE",
|
|
518
556
|
blurb: "run recipes and inspect readiness",
|
|
519
|
-
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
557
|
+
commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
|
|
520
558
|
},
|
|
521
559
|
{
|
|
522
560
|
title: "RUNTIME OVERLAY",
|
|
@@ -604,7 +642,7 @@ function groupedHelp() {
|
|
|
604
642
|
lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
|
|
605
643
|
lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
|
|
606
644
|
lines.push("");
|
|
607
|
-
lines.push("See
|
|
645
|
+
lines.push("See README.md for the workflow and docs/RECIPES.md for proof authoring.");
|
|
608
646
|
return `${lines.join("\n")}
|
|
609
647
|
`;
|
|
610
648
|
}
|
|
@@ -616,11 +654,6 @@ async function delegate(argv) {
|
|
|
616
654
|
return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
|
|
617
655
|
}
|
|
618
656
|
}
|
|
619
|
-
function translateActionsRaw(argv) {
|
|
620
|
-
const rest = argv.slice(1).filter((arg) => arg !== "--raw");
|
|
621
|
-
const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
|
|
622
|
-
return ["manifest", ...withJson];
|
|
623
|
-
}
|
|
624
657
|
const pkgVersion = (() => {
|
|
625
658
|
try {
|
|
626
659
|
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
|
|
@@ -638,8 +671,7 @@ for (const command of REAL) {
|
|
|
638
671
|
if (command.name === "update") {
|
|
639
672
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
640
673
|
}
|
|
641
|
-
|
|
642
|
-
process.exit(await delegate(argv));
|
|
674
|
+
process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
|
|
643
675
|
});
|
|
644
676
|
}
|
|
645
677
|
const HIDDEN = [
|
|
@@ -661,24 +693,9 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
661
693
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
662
694
|
process.exit(result.status ?? 1);
|
|
663
695
|
});
|
|
664
|
-
for (const retired of RETIRED) {
|
|
665
|
-
program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
|
|
666
|
-
if (retired.name === "live") {
|
|
667
|
-
const adapterIdx = rawArgv.indexOf("--adapter");
|
|
668
|
-
const aliasIdx = rawArgv.indexOf("-a");
|
|
669
|
-
const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
|
|
670
|
-
if (adapterVal === "mobile" || adapterVal === "android") {
|
|
671
|
-
process.exit(await delegate(rawArgv));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
console.error(retired.message);
|
|
676
|
-
process.exit(2);
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
696
|
const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
|
|
680
697
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
681
|
-
|
|
698
|
+
maybeNudge();
|
|
682
699
|
}
|
|
683
700
|
function hasPassthroughHelp(argv) {
|
|
684
701
|
const divider = argv.indexOf("--");
|
|
@@ -692,10 +709,66 @@ function isCallActionHelp(argv) {
|
|
|
692
709
|
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
693
710
|
return scope.includes("--help") || scope.includes("-h");
|
|
694
711
|
}
|
|
712
|
+
function jsonRequestedBeforePassthrough(argv) {
|
|
713
|
+
const divider = argv.indexOf("--");
|
|
714
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
715
|
+
return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
716
|
+
}
|
|
717
|
+
function jsonStreamRequestedBeforePassthrough(argv) {
|
|
718
|
+
const divider = argv.indexOf("--");
|
|
719
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
720
|
+
return scope.includes("--json-stream");
|
|
721
|
+
}
|
|
722
|
+
function emitUsageError(error, json, jsonStream) {
|
|
723
|
+
if (jsonStream) {
|
|
724
|
+
const stream = new JsonStreamWriter(error.command, true);
|
|
725
|
+
stream.error({ code: error.code, message: error.message, userAction: error.userAction });
|
|
726
|
+
stream.complete("fail", 2);
|
|
727
|
+
} else if (json) {
|
|
728
|
+
process.stdout.write(
|
|
729
|
+
`${JSON.stringify(
|
|
730
|
+
{
|
|
731
|
+
schemaVersion: 1,
|
|
732
|
+
command: error.command,
|
|
733
|
+
status: "fail",
|
|
734
|
+
error: {
|
|
735
|
+
code: error.code,
|
|
736
|
+
message: error.message,
|
|
737
|
+
userAction: error.userAction
|
|
738
|
+
},
|
|
739
|
+
exitCode: 2
|
|
740
|
+
},
|
|
741
|
+
null,
|
|
742
|
+
2
|
|
743
|
+
)}
|
|
744
|
+
`
|
|
745
|
+
);
|
|
746
|
+
} else {
|
|
747
|
+
const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
|
|
748
|
+
process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
|
|
749
|
+
Next: ${error.userAction}
|
|
750
|
+
`);
|
|
751
|
+
}
|
|
752
|
+
process.exit(2);
|
|
753
|
+
}
|
|
695
754
|
if (rawArgv.length === 0) {
|
|
696
755
|
process.stdout.write(groupedHelp());
|
|
697
756
|
process.exit(0);
|
|
698
757
|
}
|
|
758
|
+
const preflightBypass = /* @__PURE__ */ new Set([
|
|
759
|
+
...HIDDEN,
|
|
760
|
+
"completions"
|
|
761
|
+
]);
|
|
762
|
+
if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
763
|
+
const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
|
|
764
|
+
if (usageError) {
|
|
765
|
+
emitUsageError(
|
|
766
|
+
usageError,
|
|
767
|
+
jsonRequestedBeforePassthrough(rawArgv),
|
|
768
|
+
jsonStreamRequestedBeforePassthrough(rawArgv)
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
699
772
|
if (hasPassthroughHelp(rawArgv)) {
|
|
700
773
|
process.exit(await delegate(rawArgv));
|
|
701
774
|
}
|
package/dist/paths.js
CHANGED
|
@@ -47,10 +47,7 @@ function recipeWatchLogCandidates() {
|
|
|
47
47
|
}
|
|
48
48
|
function resolveLocalProtocolRoot() {
|
|
49
49
|
const candidates = [
|
|
50
|
-
// Orchestrator-neutral name is primary; FARMSLOT_ROOT stays a back-compat alias
|
|
51
|
-
// for one release (the installer injects METAMASK_RUNNER_PROTOCOL_ROOT).
|
|
52
50
|
process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
|
|
53
|
-
process.env.FARMSLOT_ROOT,
|
|
54
51
|
readConfiguredProtocolRoot(),
|
|
55
52
|
findProtocolRoot(runnerDir),
|
|
56
53
|
findProtocolRoot(process.cwd())
|
|
@@ -62,7 +59,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
|
|
|
62
59
|
const root = resolveLocalProtocolRoot();
|
|
63
60
|
if (!root) {
|
|
64
61
|
throw new Error(
|
|
65
|
-
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT
|
|
62
|
+
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
|
|
66
63
|
);
|
|
67
64
|
}
|
|
68
65
|
return root;
|
|
@@ -152,7 +149,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
|
|
|
152
149
|
const root = resolveLocalProtocolRoot();
|
|
153
150
|
if (!root) {
|
|
154
151
|
throw new Error(
|
|
155
|
-
`${packageName} is not installed. Install @farmslot/* packages normally, or set
|
|
152
|
+
`${packageName} is not installed. Install @farmslot/* packages normally, or set METAMASK_RUNNER_PROTOCOL_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
|
|
156
153
|
);
|
|
157
154
|
}
|
|
158
155
|
return import(pathToFileURL(path.join(root, localSourceEntry)).href);
|