@deeeed/metamask-harness 0.17.5 → 0.18.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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- 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 +1 -1
- 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 +90 -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 +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- 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/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 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- 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 +2 -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 +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- 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/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- 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/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/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)
|
|
@@ -139,11 +152,11 @@ Example (real actions; run mm-harness actions for this checkout's full set):
|
|
|
139
152
|
},
|
|
140
153
|
{
|
|
141
154
|
name: "flows",
|
|
142
|
-
summary: "Browse
|
|
155
|
+
summary: "Browse reusable parameterized action sequences.",
|
|
143
156
|
example: "mm-harness flows",
|
|
144
|
-
helpText: `mm-harness flows [flags]
|
|
157
|
+
helpText: `mm-harness flows [list|describe <ref>|promote] [flags]
|
|
145
158
|
|
|
146
|
-
Browse
|
|
159
|
+
Browse reusable parameterized action sequences. Flows resolve by precedence
|
|
147
160
|
(personal > team > canonical); the highest-tier copy wins and shadows lower ones.
|
|
148
161
|
Flow resolution is adapter-global, so there is no --adapter flag here.
|
|
149
162
|
|
|
@@ -152,31 +165,55 @@ Example (real actions; run mm-harness actions for this checkout's full set):
|
|
|
152
165
|
--json Machine-readable output
|
|
153
166
|
|
|
154
167
|
Example:
|
|
155
|
-
mm-harness flows
|
|
168
|
+
mm-harness flows
|
|
169
|
+
mm-harness flows describe perps.clean_market_testnet
|
|
170
|
+
mm-harness flows describe perps.clean_market_testnet --json`
|
|
156
171
|
},
|
|
157
172
|
{
|
|
158
173
|
name: "run",
|
|
159
|
-
summary: "
|
|
174
|
+
summary: "Execute a complete proof and write its evidence.",
|
|
160
175
|
example: "mm-harness run recipe.json",
|
|
161
176
|
helpText: `mm-harness run <recipe.json> [flags]
|
|
162
177
|
|
|
163
|
-
Validate
|
|
178
|
+
Validate and execute a complete proof, then write summary, trace, diagnostics,
|
|
179
|
+
and artifacts. A recipe may use actions directly or call reusable flows.
|
|
164
180
|
|
|
165
|
-
--list List
|
|
181
|
+
--list List complete recipes accepted by run for this adapter; no <recipe> needed
|
|
182
|
+
--describe Show one recipe's metadata, declared inputs, composed actions/flows, and next command
|
|
166
183
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
167
184
|
--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.
|
|
185
|
+
--cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
186
|
+
--launch-existing-dist Extension: reuse/launch the existing dist without rebuilding
|
|
168
187
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
169
188
|
--target <path> Checkout path (default: cwd)
|
|
170
|
-
--artifacts-dir <dir>
|
|
189
|
+
--artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
|
|
171
190
|
--action-manifest <path> Override the action manifest
|
|
172
191
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
173
192
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
174
193
|
--json Machine-readable output
|
|
194
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
175
195
|
--record-video=full-run Record a video of the run
|
|
176
196
|
|
|
177
197
|
Example:
|
|
198
|
+
mm-harness run wallet.smoke --describe
|
|
178
199
|
mm-harness run recipe.json --plan --adapter mobile
|
|
179
|
-
mm-harness run recipe.json --adapter extension
|
|
200
|
+
mm-harness run recipe.json --adapter extension`
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "last",
|
|
204
|
+
summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
|
|
205
|
+
example: "mm-harness last --json",
|
|
206
|
+
helpText: `mm-harness last [flags]
|
|
207
|
+
|
|
208
|
+
Read the atomic per-checkout resumability journal. Discovery commands do not
|
|
209
|
+
replace it, and an interrupted process remains recorded as verdict=running.
|
|
210
|
+
|
|
211
|
+
--target <path> Checkout path (default: cwd)
|
|
212
|
+
--runtime-dir <dir> Runtime dir containing last-command.json
|
|
213
|
+
--json Machine-readable envelope
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
mm-harness last --json`
|
|
180
217
|
},
|
|
181
218
|
{
|
|
182
219
|
name: "doctor",
|
|
@@ -375,6 +412,7 @@ Example:
|
|
|
375
412
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
376
413
|
--target <path> Checkout path (default: cwd)
|
|
377
414
|
--json Machine-readable summary (recovered[] / mutations[] / phase)
|
|
415
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
378
416
|
|
|
379
417
|
Example:
|
|
380
418
|
mm-harness launch ios
|
|
@@ -393,8 +431,8 @@ Example:
|
|
|
393
431
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
394
432
|
--window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
|
|
395
433
|
--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
|
|
434
|
+
--source <label> Log source per adapter \u2014 mobile: metro|app;
|
|
435
|
+
extension: extension|dapp|webpack|watcher|rebuild.
|
|
398
436
|
Core is headless (teaching error).
|
|
399
437
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
400
438
|
--target <path> Checkout path (default: cwd)
|
|
@@ -473,6 +511,7 @@ Example:
|
|
|
473
511
|
--cdp-port <port> finalize: CDP port of the running extension
|
|
474
512
|
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
475
513
|
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
514
|
+
--action-manifest <path> Extension set: override the wallet action manifest
|
|
476
515
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
477
516
|
--target <path> Checkout path (default: cwd)
|
|
478
517
|
--device <udid|serial|name> Mobile only: target this device for sync/set
|
|
@@ -487,21 +526,28 @@ Example:
|
|
|
487
526
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
488
527
|
}
|
|
489
528
|
];
|
|
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.`
|
|
529
|
+
const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
|
|
530
|
+
REAL.map((command) => [command.name, command.example])
|
|
531
|
+
);
|
|
532
|
+
function assertPublicContractMatchesSurface() {
|
|
533
|
+
const registered = REAL.map((command) => command.name).sort();
|
|
534
|
+
const contracted = publicCommandNames().sort();
|
|
535
|
+
if (registered.join("\n") !== contracted.join("\n")) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
|
|
538
|
+
);
|
|
503
539
|
}
|
|
504
|
-
|
|
540
|
+
for (const command of REAL) {
|
|
541
|
+
const registeredAliases = [...command.aliases ?? []].sort();
|
|
542
|
+
const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
|
|
543
|
+
if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
assertPublicContractMatchesSurface();
|
|
505
551
|
const HELP_GROUPS = [
|
|
506
552
|
{
|
|
507
553
|
title: "DAILY LOOP",
|
|
@@ -516,7 +562,7 @@ const HELP_GROUPS = [
|
|
|
516
562
|
{
|
|
517
563
|
title: "PROVE",
|
|
518
564
|
blurb: "run recipes and inspect readiness",
|
|
519
|
-
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
565
|
+
commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
|
|
520
566
|
},
|
|
521
567
|
{
|
|
522
568
|
title: "RUNTIME OVERLAY",
|
|
@@ -604,7 +650,7 @@ function groupedHelp() {
|
|
|
604
650
|
lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
|
|
605
651
|
lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
|
|
606
652
|
lines.push("");
|
|
607
|
-
lines.push("See
|
|
653
|
+
lines.push("See README.md for the workflow and docs/RECIPES.md for proof authoring.");
|
|
608
654
|
return `${lines.join("\n")}
|
|
609
655
|
`;
|
|
610
656
|
}
|
|
@@ -616,11 +662,6 @@ async function delegate(argv) {
|
|
|
616
662
|
return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
|
|
617
663
|
}
|
|
618
664
|
}
|
|
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
665
|
const pkgVersion = (() => {
|
|
625
666
|
try {
|
|
626
667
|
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
|
|
@@ -638,8 +679,7 @@ for (const command of REAL) {
|
|
|
638
679
|
if (command.name === "update") {
|
|
639
680
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
640
681
|
}
|
|
641
|
-
|
|
642
|
-
process.exit(await delegate(argv));
|
|
682
|
+
process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
|
|
643
683
|
});
|
|
644
684
|
}
|
|
645
685
|
const HIDDEN = [
|
|
@@ -661,24 +701,9 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
661
701
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
662
702
|
process.exit(result.status ?? 1);
|
|
663
703
|
});
|
|
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
704
|
const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
|
|
680
705
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
681
|
-
|
|
706
|
+
maybeNudge();
|
|
682
707
|
}
|
|
683
708
|
function hasPassthroughHelp(argv) {
|
|
684
709
|
const divider = argv.indexOf("--");
|
|
@@ -692,10 +717,66 @@ function isCallActionHelp(argv) {
|
|
|
692
717
|
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
693
718
|
return scope.includes("--help") || scope.includes("-h");
|
|
694
719
|
}
|
|
720
|
+
function jsonRequestedBeforePassthrough(argv) {
|
|
721
|
+
const divider = argv.indexOf("--");
|
|
722
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
723
|
+
return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
724
|
+
}
|
|
725
|
+
function jsonStreamRequestedBeforePassthrough(argv) {
|
|
726
|
+
const divider = argv.indexOf("--");
|
|
727
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
728
|
+
return scope.includes("--json-stream");
|
|
729
|
+
}
|
|
730
|
+
function emitUsageError(error, json, jsonStream) {
|
|
731
|
+
if (jsonStream) {
|
|
732
|
+
const stream = new JsonStreamWriter(error.command, true);
|
|
733
|
+
stream.error({ code: error.code, message: error.message, userAction: error.userAction });
|
|
734
|
+
stream.complete("fail", 2);
|
|
735
|
+
} else if (json) {
|
|
736
|
+
process.stdout.write(
|
|
737
|
+
`${JSON.stringify(
|
|
738
|
+
{
|
|
739
|
+
schemaVersion: 1,
|
|
740
|
+
command: error.command,
|
|
741
|
+
status: "fail",
|
|
742
|
+
error: {
|
|
743
|
+
code: error.code,
|
|
744
|
+
message: error.message,
|
|
745
|
+
userAction: error.userAction
|
|
746
|
+
},
|
|
747
|
+
exitCode: 2
|
|
748
|
+
},
|
|
749
|
+
null,
|
|
750
|
+
2
|
|
751
|
+
)}
|
|
752
|
+
`
|
|
753
|
+
);
|
|
754
|
+
} else {
|
|
755
|
+
const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
|
|
756
|
+
process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
|
|
757
|
+
Next: ${error.userAction}
|
|
758
|
+
`);
|
|
759
|
+
}
|
|
760
|
+
process.exit(2);
|
|
761
|
+
}
|
|
695
762
|
if (rawArgv.length === 0) {
|
|
696
763
|
process.stdout.write(groupedHelp());
|
|
697
764
|
process.exit(0);
|
|
698
765
|
}
|
|
766
|
+
const preflightBypass = /* @__PURE__ */ new Set([
|
|
767
|
+
...HIDDEN,
|
|
768
|
+
"completions"
|
|
769
|
+
]);
|
|
770
|
+
if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
771
|
+
const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
|
|
772
|
+
if (usageError) {
|
|
773
|
+
emitUsageError(
|
|
774
|
+
usageError,
|
|
775
|
+
jsonRequestedBeforePassthrough(rawArgv),
|
|
776
|
+
jsonStreamRequestedBeforePassthrough(rawArgv)
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
699
780
|
if (hasPassthroughHelp(rawArgv)) {
|
|
700
781
|
process.exit(await delegate(rawArgv));
|
|
701
782
|
}
|
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);
|