@deeeed/metamask-harness 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -0,0 +1,183 @@
1
+ import fs from "node:fs";
2
+ import { mkdtemp } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { loadActionManifest } from "../manifest.js";
6
+ import { importRecipeProtocol } from "../paths.js";
7
+ import { recipeRunning } from "../heal-bounds.js";
8
+ import { EXIT } from "./shared.js";
9
+ import {
10
+ parseArgs,
11
+ optionFlag,
12
+ optionString,
13
+ resolveAdapter,
14
+ runtimeOptionsFromCli,
15
+ usageError
16
+ } from "./parse-args.js";
17
+ import {
18
+ emitHealViolation,
19
+ executeWithHealBounds,
20
+ prepareHeal,
21
+ resolveMetaMaskLibrarySources,
22
+ runRecipe,
23
+ synthesizeOneNodeRecipe,
24
+ validateRecipeAdapterAware
25
+ } from "./run-engine.js";
26
+ async function handleCall(argv) {
27
+ if (argv.length > 0 && argv[0].startsWith("--")) {
28
+ const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
29
+ console.error(message);
30
+ return EXIT.usage;
31
+ }
32
+ const { action: shortName, args, rest } = parseCallArgs(argv);
33
+ const { options } = parseArgs(rest, "call");
34
+ const json = optionFlag(options, "json");
35
+ if (!shortName) {
36
+ let example = "mm-harness call <action>";
37
+ let discovery = "mm-harness actions";
38
+ try {
39
+ const { adapter: adapter2 } = resolveAdapter(options);
40
+ const manifest2 = loadActionManifest(adapter2, optionString(options, "actionManifest"));
41
+ const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
42
+ const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
43
+ example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
44
+ discovery = `mm-harness actions --adapter ${adapter2}`;
45
+ } catch {
46
+ }
47
+ const message = `call requires <action>. Example: ${example}`;
48
+ const userAction = `${example} # see the vocabulary: ${discovery}`;
49
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", error: { code: "USAGE", message, userAction } }, null, 2));
50
+ else console.error(`${message}
51
+ See the vocabulary: ${discovery}`);
52
+ return EXIT.usage;
53
+ }
54
+ const { adapter, target } = resolveAdapter(options);
55
+ if (recipeRunning(target)) {
56
+ const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
57
+ if (json) {
58
+ console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
59
+ } else {
60
+ console.error(`\u2717 mm-harness call: ${msg}`);
61
+ }
62
+ return EXIT.bounded;
63
+ }
64
+ const actionManifestOverride = optionString(options, "actionManifest");
65
+ const manifest = loadActionManifest(adapter, actionManifestOverride);
66
+ const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
67
+ const names = getRecipeActionManifestActionNames(manifest);
68
+ const resolution = resolveActionName(shortName, names);
69
+ if (resolution.status === "unknown") {
70
+ const message = `\u2717 call: unknown action "${shortName}" for the ${adapter} adapter.
71
+ See the vocabulary: mm-harness actions --adapter ${adapter} --json`;
72
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
73
+ else console.error(message);
74
+ return EXIT.usage;
75
+ }
76
+ if (resolution.status === "ambiguous") {
77
+ const message = `\u2717 call: "${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
78
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates } }, null, 2));
79
+ else console.error(message);
80
+ return EXIT.usage;
81
+ }
82
+ const resolvedAction = resolution.resolved;
83
+ const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
84
+ const validation = await validateRecipeAdapterAware(recipe, manifest);
85
+ if (validation.status === "invalid") {
86
+ const message = `\u2717 call ${resolvedAction}: recipe validation failed`;
87
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message } }, null, 2));
88
+ else {
89
+ console.error(message);
90
+ for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
91
+ }
92
+ return EXIT.validation;
93
+ }
94
+ const prepared = await prepareHeal(adapter, target, options, json);
95
+ if (typeof prepared === "number") return prepared;
96
+ const { state, heal } = prepared;
97
+ const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
98
+ const recipeFile = path.join(scratch, "call.recipe.json");
99
+ fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
100
+ `);
101
+ const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
102
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
103
+ const callRuntimeOptions = {
104
+ ...runtimeOptionsFromCli(options),
105
+ ...librarySources ? { librarySources } : {},
106
+ stdoutIsMachineContract: json
107
+ };
108
+ const { result, violation } = await executeWithHealBounds(
109
+ () => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
110
+ adapter,
111
+ target,
112
+ heal,
113
+ state
114
+ );
115
+ if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
116
+ if (json) {
117
+ console.log(
118
+ JSON.stringify(
119
+ {
120
+ schemaVersion: 1,
121
+ command: "call",
122
+ adapter,
123
+ action: shortName,
124
+ resolvedAction,
125
+ args,
126
+ status: result.status,
127
+ summaryPath: result.summaryPath,
128
+ tracePath: result.tracePath,
129
+ artifactManifestPath: result.artifactManifestPath,
130
+ recovered: state.recovered,
131
+ mutations: state.mutations,
132
+ exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
133
+ },
134
+ null,
135
+ 2
136
+ )
137
+ );
138
+ } else {
139
+ console.log(`call ${resolvedAction}: ${result.status}
140
+ Artifacts: ${result.artifactManifestPath}`);
141
+ }
142
+ return result.status === "pass" ? EXIT.ok : EXIT.runtime;
143
+ }
144
+ function parseCallArgs(argv) {
145
+ const args = {};
146
+ const rest = [];
147
+ let action;
148
+ for (let i = 0; i < argv.length; i += 1) {
149
+ const arg = argv[i];
150
+ if (arg === "--arg" || arg.startsWith("--arg=")) {
151
+ const pair = arg === "--arg" ? argv[i += 1] : arg.slice("--arg=".length);
152
+ if (pair === void 0) throw usageError("--arg requires k=v.");
153
+ const eq = pair.indexOf("=");
154
+ if (eq === -1) throw usageError(`--arg must be k=v: ${pair}`);
155
+ args[pair.slice(0, eq)] = pair.slice(eq + 1);
156
+ continue;
157
+ }
158
+ if (!arg.startsWith("--") && action === void 0) {
159
+ action = arg;
160
+ continue;
161
+ }
162
+ rest.push(arg);
163
+ }
164
+ return { action, args, rest };
165
+ }
166
+ function resolveActionName(shortName, names) {
167
+ if (names.includes(shortName)) return { status: "ok", resolved: shortName, candidates: [shortName] };
168
+ const finalSegment = (name) => name.split(".").pop() ?? name;
169
+ const exactSegment = names.filter((name) => finalSegment(name) === shortName);
170
+ const tier = exactSegment.length > 0 ? exactSegment : names.filter((name) => finalSegment(name).includes(shortName));
171
+ if (tier.length === 1) return { status: "ok", resolved: tier[0], candidates: tier };
172
+ if (tier.length > 1) return { status: "ambiguous", resolved: "", candidates: tier };
173
+ return { status: "unknown", resolved: "", candidates: [] };
174
+ }
175
+ function pickCallExampleAction(names) {
176
+ const walletish = names.find((name) => /wallet|unlock/u.test(name));
177
+ if (walletish) return walletish;
178
+ if (names.includes("command")) return "command";
179
+ return names[0] ?? "command";
180
+ }
181
+ export {
182
+ handleCall
183
+ };
@@ -0,0 +1,58 @@
1
+ import { loadActionManifest } from "../manifest.js";
2
+ import {
3
+ invalidateCompletionCache,
4
+ readFreshCandidates,
5
+ writeCompletionCandidates
6
+ } from "../completions-cache.js";
7
+ import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
8
+ import { EXIT } from "./shared.js";
9
+ import {
10
+ parseArgs,
11
+ optionFlag,
12
+ optionString,
13
+ resolveAdapter,
14
+ targetPath
15
+ } from "./parse-args.js";
16
+ import { resolveMetaMaskLibrarySources } from "./run-engine.js";
17
+ async function handleCompletionCandidates(argv) {
18
+ const kind = argv[0];
19
+ const { options } = parseArgs(argv.slice(1), "completion-candidates");
20
+ const json = optionFlag(options, "json");
21
+ if (kind !== "actions" && kind !== "flows") {
22
+ console.error("completion-candidates requires <actions|flows>.");
23
+ return EXIT.usage;
24
+ }
25
+ const target = targetPath(options);
26
+ let adapter;
27
+ let candidates;
28
+ if (kind === "actions") {
29
+ adapter = resolveAdapter(options).adapter;
30
+ candidates = readFreshCandidates(target, "actions");
31
+ if (!candidates) {
32
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
33
+ const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
34
+ candidates = getRecipeActionManifestActionNames(manifest);
35
+ writeCompletionCandidates(target, "actions", candidates);
36
+ }
37
+ } else {
38
+ candidates = readFreshCandidates(target, "flows");
39
+ if (!candidates) {
40
+ const sources = await resolveMetaMaskLibrarySources(void 0).catch(() => void 0);
41
+ if (!sources) {
42
+ candidates = [];
43
+ } else {
44
+ const harness = await importRecipeHarness();
45
+ const resolution = await harness.loadRecipeLibraries(sources);
46
+ candidates = [...resolution.flows.keys()].sort();
47
+ writeCompletionCandidates(target, "flows", candidates);
48
+ }
49
+ }
50
+ }
51
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));
52
+ else for (const candidate of candidates) console.log(candidate);
53
+ return EXIT.ok;
54
+ }
55
+ export {
56
+ handleCompletionCandidates,
57
+ invalidateCompletionCache
58
+ };
@@ -0,0 +1,101 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { color } from "../cli-color.js";
4
+ import { createDoctorReport, renderRuntimeContext } from "../doctor.js";
5
+ import { detectAdapter } from "../harness.js";
6
+ import { assertAdapter, runnerDir } from "../paths.js";
7
+ import { getAdapterSurface } from "../adapters/surface.js";
8
+ import { loadActionManifest, validateManifest } from "../manifest.js";
9
+ import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
10
+ import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
11
+ import {
12
+ actionManifestPathOption,
13
+ applyRuntimeDirOption,
14
+ optionFlag,
15
+ optionString,
16
+ targetPath
17
+ } from "./parse-args.js";
18
+ async function handleDoctor({ options }) {
19
+ applyRuntimeDirOption(options);
20
+ const target = targetPath(options);
21
+ const json = optionFlag(options, "json");
22
+ const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
23
+ const adapter = explicitAdapter ?? detectAdapter(target);
24
+ if (!adapter) {
25
+ return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
26
+ }
27
+ assertAdapter(adapter);
28
+ const actionManifestPath = actionManifestPathOption(options, adapter);
29
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
30
+ const manifestValidation = await validateManifest(manifest);
31
+ if (optionFlag(options, "fix")) {
32
+ const { fixed, failed } = await runDoctorFix(adapter, target, manifestValidation, json);
33
+ const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
34
+ if (json) console.log(JSON.stringify({ ...result2, fixed, failed }, null, 2));
35
+ else console.log(`${result2.status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
36
+ return failed.length === 0 ? 0 : 1;
37
+ }
38
+ const result = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
39
+ let runtime;
40
+ try {
41
+ const surface = getAdapterSurface(adapter);
42
+ surface.resolveSlotPorts(target);
43
+ runtime = await surface.runtimeStatus(target);
44
+ } catch {
45
+ }
46
+ const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target) : [];
47
+ if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros }, null, 2));
48
+ else {
49
+ const out = (style, text) => color(style, text, { stream: process.stdout });
50
+ const stateStyle = (value, good) => value === good ? "ok" : "warn";
51
+ console.log(`${out(result.status === "pass" ? "ok" : "err", result.status)} ${out("bold", adapter)} ${result.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
52
+ if (runtime) {
53
+ const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
54
+ const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
55
+ const depsStyle = provisionedDepsPending ? "info" : stateStyle(runtime.deps, "current");
56
+ const devServerStyle = provisionedDepsPending ? "info" : void 0;
57
+ const devServer = runtime.devServer ? ` ${runtime.devServer.label}=${out(devServerStyle ?? stateStyle(runtime.devServer.status, "up"), runtime.devServer.status)}` : "";
58
+ console.log(
59
+ `${out("label", "runtime:")} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out("dim", `(${runtime.reasonCode})`)}` : ""} deps=${out(depsStyle, runtime.deps ?? "unknown")}${devServer}`
60
+ );
61
+ for (const reason of runtime.reasons) console.log(` ${out("dim", reason)}`);
62
+ }
63
+ console.log(renderRuntimeContext(result.runtimeContext));
64
+ if (orphanMetros.length > 0) {
65
+ console.log(
66
+ `${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
67
+ );
68
+ console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
69
+ }
70
+ }
71
+ return result.status === "pass" ? 0 : 1;
72
+ }
73
+ function detectOrphanMetros(target) {
74
+ try {
75
+ const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
76
+ const out = execFileSync("bash", ["-c", `. "$1"; detect_checkout_metros "$2"`, "mm-harness-doctor", script, target], {
77
+ encoding: "utf8",
78
+ timeout: 5e3
79
+ });
80
+ return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+ async function runDoctorFix(adapter, target, manifestValidation, json) {
86
+ const fixed = [];
87
+ const failed = [];
88
+ if (recipeRunning(target)) {
89
+ failed.push("recipe-running");
90
+ return { fixed, failed };
91
+ }
92
+ if (Number(manifestValidation.summary?.errors ?? 0) > 0) failed.push("manifest");
93
+ const state = newHealState();
94
+ const ensured = await ensureOverlay(adapter, target, "infra-only", state, json);
95
+ if (!ensured.ok) failed.push("overlay");
96
+ else if (state.mutations.length > 0) fixed.push("overlay");
97
+ return { fixed, failed };
98
+ }
99
+ export {
100
+ handleDoctor
101
+ };
@@ -0,0 +1,24 @@
1
+ import { ensureExtensionReady } from "../adapters/extension/ensure-ready.js";
2
+ import {
3
+ adapterOption,
4
+ optionFlag,
5
+ optionString,
6
+ parsePort,
7
+ targetPath
8
+ } from "./parse-args.js";
9
+ async function handleEnsureReady({ options }) {
10
+ const adapter = adapterOption(options);
11
+ if (adapter !== "extension") throw new Error("ensure-ready currently applies to the extension adapter.");
12
+ const target = targetPath(options);
13
+ const cdpPort = parsePort(
14
+ optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
15
+ "ensure-ready requires --cdp-port <port>."
16
+ );
17
+ const result = await ensureExtensionReady(target, { cdpPort });
18
+ if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
19
+ else console.log(`${result.ready ? "READY" : "NOT-READY"} ${result.reasonCode} \u2014 id=${result.extensionId} homeTabs ${result.homeTabs.before}\u2192${result.homeTabs.after} (closed ${result.homeTabs.closed})`);
20
+ return result.ready ? 0 : 1;
21
+ }
22
+ export {
23
+ handleEnsureReady
24
+ };
@@ -0,0 +1,62 @@
1
+ import path from "node:path";
2
+ import { color } from "../cli-color.js";
3
+ import { importRecipeHarnessCli } from "../paths.js";
4
+ import { EXIT } from "./shared.js";
5
+ import { usageError } from "./parse-args.js";
6
+ import { resolveMetaMaskLibrarySources } from "./run-engine.js";
7
+ const FLOWS_SUBCOMMANDS = ["list", "promote"];
8
+ function stripTargetFlag(argv) {
9
+ const out = [];
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const arg = argv[i];
12
+ if (arg === "--target") {
13
+ i += 1;
14
+ continue;
15
+ }
16
+ if (arg.startsWith("--target=")) continue;
17
+ out.push(arg);
18
+ }
19
+ return out;
20
+ }
21
+ function serializeLibrarySources(sources) {
22
+ return sources.map((source) => source.name ? `${source.name}=${source.root}` : source.root).join(":");
23
+ }
24
+ function flowSourceTier(source, index, total) {
25
+ if (index === total - 1 && source.name === "metamask") return "canonical";
26
+ if (source.name === "personal") return "personal";
27
+ return "team";
28
+ }
29
+ function restoreEnv(key, value) {
30
+ if (value === void 0) delete process.env[key];
31
+ else process.env[key] = value;
32
+ }
33
+ async function handleFlows(argv) {
34
+ const sources = await resolveMetaMaskLibrarySources(void 0);
35
+ if (!sources) {
36
+ throw usageError(
37
+ "flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; flows resolve personal > team > canonical). Next: update the dependency and run yarn install."
38
+ );
39
+ }
40
+ const forwarded = stripTargetFlag(argv);
41
+ const jsonMode = forwarded.includes("--json");
42
+ if (!jsonMode) {
43
+ console.error(color("label", "flows resolve by precedence (highest wins; local shadows canonical):"));
44
+ sources.forEach((source, index) => {
45
+ const name = source.name ?? path.basename(source.root);
46
+ console.error(` ${index + 1}. ${color("cmd", name)} ${color("dim", `[${flowSourceTier(source, index, sources.length)}]`)}`);
47
+ });
48
+ }
49
+ const withSubcommand = FLOWS_SUBCOMMANDS.includes(forwarded[0]) ? forwarded : ["list", ...forwarded];
50
+ const { runRecipeHarnessCli } = await importRecipeHarnessCli();
51
+ const previousLibraryPath = process.env.RECIPE_LIBRARY_PATH;
52
+ process.env.RECIPE_LIBRARY_PATH = serializeLibrarySources(sources);
53
+ try {
54
+ await runRecipeHarnessCli(["flows", ...withSubcommand]);
55
+ } finally {
56
+ restoreEnv("RECIPE_LIBRARY_PATH", previousLibraryPath);
57
+ }
58
+ return EXIT.ok;
59
+ }
60
+ export {
61
+ handleFlows
62
+ };
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
4
+ import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
5
+ import { spawnScriptStreaming } from "../shared.js";
6
+ async function launchExtension(target, tier, wantWatch) {
7
+ if (!wantWatch && tier === "quick") {
8
+ return extensionRebuild(target);
9
+ }
10
+ const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
11
+ const watchArgs = ["--target", target];
12
+ if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
13
+ console.error(`\u2192 extension ${wantWatch ? "watch" : "build"} \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
14
+ return spawnScriptStreaming(startWatchSh, watchArgs, target);
15
+ }
16
+ async function extensionRebuild(target) {
17
+ const runtimeDirRel = recipeRuntimeDir();
18
+ const runtimeAbs = path.join(target, runtimeDirRel);
19
+ const rebuildLog = path.join(runtimeAbs, "rebuild.log");
20
+ stopExtensionWatcher(target);
21
+ fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
22
+ fs.writeFileSync(rebuildLog, "");
23
+ const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
24
+ const liveArgs = ["--target", target, "--start-watch"];
25
+ if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
26
+ if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
27
+ console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
28
+ const result = await spawnScriptStreaming(liveScript, liveArgs, target);
29
+ if (result.output) {
30
+ try {
31
+ fs.appendFileSync(rebuildLog, result.output);
32
+ } catch {
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ export {
38
+ extensionRebuild,
39
+ launchExtension
40
+ };
@@ -1,12 +1,10 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { color } from "../cli-color.js";
5
- import { handleHarness } from "../harness.js";
6
- import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from "../paths.js";
7
- import { prepareMobile } from "../adapters/mobile/prepare.js";
8
- import { getAdapterSurface } from "../adapters/surface.js";
9
- import { stopExtensionWatcher } from "../adapters/slot-ports.js";
4
+ import { color } from "../../cli-color.js";
5
+ import { handleHarness } from "../../harness.js";
6
+ import { recipeRuntimePath, runnerDir } from "../../paths.js";
7
+ import { getAdapterSurface } from "../../adapters/surface.js";
10
8
  import {
11
9
  ADAPTER_DETECT_NEXT,
12
10
  EXIT,
@@ -14,18 +12,19 @@ import {
14
12
  parseFlags,
15
13
  resolveAdapter,
16
14
  spawnScript,
17
- spawnScriptStreaming,
18
15
  str,
19
16
  targetOf,
20
17
  usageOut
21
- } from "./shared.js";
18
+ } from "../shared.js";
22
19
  import {
23
20
  RECOVERY_CODE,
24
21
  checkHealBounds,
25
22
  ensureOverlay,
26
23
  newHealState,
27
24
  parseHeal
28
- } from "../heal-bounds.js";
25
+ } from "../../heal-bounds.js";
26
+ import { launchExtension } from "./extension.js";
27
+ import { launchMobile } from "./mobile.js";
29
28
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
30
29
  "build",
31
30
  "watch",
@@ -84,6 +83,9 @@ async function handleLaunch(argv) {
84
83
  }
85
84
  }
86
85
  applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
86
+ if (adapter === "extension" && typeof options["url"] === "string" && options["url"]) {
87
+ process.env.EXTENSION_START_URL = options["url"];
88
+ }
87
89
  const tier = wantVerify ? "verify" : wantBuild ? "build" : "quick";
88
90
  const state = newHealState();
89
91
  if (tier === "quick" && nativeInputsChanged(target, adapter)) {
@@ -107,8 +109,7 @@ async function handleLaunch(argv) {
107
109
  const liveArgs = ["live", "--adapter", adapter, "--target", target];
108
110
  if (adapter === "mobile" && mobileTarget) liveArgs.push("--platform", mobileTarget);
109
111
  if (json) liveArgs.push("--json");
110
- const code = await handleHarness(liveArgs);
111
- return code;
112
+ return handleHarness(liveArgs);
112
113
  }
113
114
  let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
114
115
  if (attempt.status === 0) {
@@ -194,47 +195,16 @@ function nativeInputsChanged(target, adapter) {
194
195
  }
195
196
  async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json) {
196
197
  if (adapter === "mobile") {
197
- const platform = mobileTarget ?? "ios";
198
- const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
199
- const preflightMode = tier === "build" ? "auto" : "fast";
200
- return prepareMobile(target, { platform, json, watcherPort, preflightMode });
201
- }
202
- if (!wantWatch && tier === "quick") {
203
- return extensionRebuild(target, json);
204
- }
205
- const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
206
- const watchArgs = ["--target", target];
207
- if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
208
- console.error(`\u2192 extension ${wantWatch ? "watch" : "build"} \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
209
- return spawnScriptStreaming(startWatchSh, watchArgs, target);
210
- }
211
- async function extensionRebuild(target, json) {
212
- const runtimeDirRel = recipeRuntimeDir();
213
- const runtimeAbs = path.join(target, runtimeDirRel);
214
- const rebuildLog = path.join(runtimeAbs, "rebuild.log");
215
- stopExtensionWatcher(target);
216
- fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
217
- fs.writeFileSync(rebuildLog, "");
218
- const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
219
- const liveArgs = ["--target", target, "--start-watch"];
220
- if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
221
- if (process.env.WATCHER_PORT) liveArgs.push("--watcher-port", process.env.WATCHER_PORT);
222
- console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
223
- const result = await spawnScriptStreaming(liveScript, liveArgs, target);
224
- if (result.output) {
225
- try {
226
- fs.appendFileSync(rebuildLog, result.output);
227
- } catch {
228
- }
198
+ return launchMobile(target, mobileTarget, tier, json);
229
199
  }
230
- return result;
200
+ return launchExtension(target, tier, wantWatch);
231
201
  }
232
202
  function finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state) {
233
203
  if (adapter === "extension" && displayMode === "sidepanel") {
234
204
  const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
235
205
  const sidepanelArgs = ["open"];
236
206
  if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
237
- const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
207
+ const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json, { REPO: target });
238
208
  if (sidepanel.status !== 0) {
239
209
  return launchFail(json, adapter, mobileTarget, tier, state, {
240
210
  code: "SIDEPANEL_OPEN_FAILED",
@@ -300,8 +270,6 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
300
270
  message: failure.message,
301
271
  retryable: failure.recoverable,
302
272
  userAction: failure.userAction ?? null,
303
- // The classification note is in `message`; the REAL failure text lives
304
- // here verbatim so it is never lost.
305
273
  originalError: failure.originalError ?? null
306
274
  }
307
275
  },
@@ -0,0 +1,10 @@
1
+ import { prepareMobile } from "../../adapters/mobile/prepare.js";
2
+ async function launchMobile(target, mobileTarget, tier, json) {
3
+ const platform = mobileTarget ?? "ios";
4
+ const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
5
+ const preflightMode = tier === "build" ? "auto" : "fast";
6
+ return prepareMobile(target, { platform, json, watcherPort, preflightMode });
7
+ }
8
+ export {
9
+ launchMobile
10
+ };
@@ -0,0 +1,72 @@
1
+ import { loadActionManifest, validateManifest } from "../manifest.js";
2
+ import {
3
+ actionManifestPathOption,
4
+ isRecord,
5
+ optionFlag,
6
+ optionString,
7
+ resolveAdapter
8
+ } from "./parse-args.js";
9
+ async function handleManifest({ options }) {
10
+ const { adapter } = resolveAdapter(options);
11
+ const actionManifestPath = actionManifestPathOption(options, adapter);
12
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
13
+ await validateManifest(manifest);
14
+ if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
15
+ else console.log(actionManifestPath);
16
+ return 0;
17
+ }
18
+ async function handleActions({ options }) {
19
+ const { adapter } = resolveAdapter(options);
20
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
21
+ await validateManifest(manifest);
22
+ const action = optionString(options, "action");
23
+ const actions = describeManifestActions(manifest, action);
24
+ if (optionFlag(options, "json")) {
25
+ console.log(JSON.stringify({ adapter, actions }, null, 2));
26
+ } else {
27
+ for (const entry of actions) {
28
+ const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
29
+ console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
30
+ }
31
+ }
32
+ return 0;
33
+ }
34
+ function describeManifestActions(manifest, filterAction) {
35
+ const manifestRecord = isRecord(manifest) ? manifest : {};
36
+ const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
37
+ const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
38
+ const custom = Array.isArray(manifestRecord.custom_actions) ? manifestRecord.custom_actions.flatMap((entry) => {
39
+ if (typeof entry === "string") return [{ name: entry, metadata: metadata[entry] }];
40
+ if (isRecord(entry) && typeof entry.name === "string") {
41
+ const entryMetadata = { ...entry };
42
+ const metadataOverride = metadata[entry.name];
43
+ if (isRecord(metadataOverride)) Object.assign(entryMetadata, metadataOverride);
44
+ return [{ name: entry.name, metadata: entryMetadata }];
45
+ }
46
+ return [];
47
+ }) : [];
48
+ const entries = [
49
+ ...official.map((name) => describeManifestAction(name, "official", metadata[name])),
50
+ ...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
51
+ ].filter((entry) => !filterAction || entry.name === filterAction);
52
+ if (filterAction && entries.length === 0) throw new Error(`Action not found in manifest: ${filterAction}`);
53
+ return entries;
54
+ }
55
+ function describeManifestAction(name, kind, metadata) {
56
+ const record = isRecord(metadata) ? metadata : {};
57
+ const schema = record.schema;
58
+ const schemaRecord = isRecord(schema) ? schema : {};
59
+ const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).sort() : [];
60
+ return {
61
+ name,
62
+ kind,
63
+ description: typeof record.description === "string" ? record.description : "",
64
+ fields: properties,
65
+ schema,
66
+ examples: record.examples
67
+ };
68
+ }
69
+ export {
70
+ handleActions,
71
+ handleManifest
72
+ };