@deeeed/metamask-harness 0.6.3 → 0.7.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 +72 -0
- package/adapters/extension/inject.mjs +5 -0
- package/adapters/extension/start-watch.sh +11 -17
- package/adapters/manifest.json +57 -9
- package/adapters/mobile/lib/tmux-viewer.sh +31 -10
- package/adapters/mobile/open-device.sh +14 -1
- package/adapters/mobile/stop-metro.sh +1 -1
- package/adapters/mobile/yarn-setup.sh +15 -1
- package/adapters/shared/activate-repo-ruby.sh +124 -0
- package/adapters/shared/open-log-window.sh +55 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
- package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
- package/adapters/shared/resolve-farmslot-ports.sh +6 -104
- package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
- package/adapters/shared/resolve-slot-ports.mjs +20 -0
- package/adapters/shared/resolve-slot-ports.sh +110 -0
- package/adapters/shared/tmux-session.sh +35 -0
- package/dist/adapters/mobile/provision.js +25 -2
- package/dist/adapters/{resolve-farmslot-ports.js → resolve-slot-ports.js} +4 -2
- package/dist/adapters/slot-ports.js +8 -10
- package/dist/cli-commands.js +1 -1
- package/dist/commands/call.js +5 -0
- package/dist/commands/doctor.js +21 -1
- package/dist/commands/fixtures.js +5 -3
- package/dist/commands/launch/extension.js +18 -0
- package/dist/commands/launch/index.js +10 -1
- package/dist/commands/list-executables.js +48 -0
- package/dist/commands/logs.js +25 -2
- package/dist/commands/manifest.js +27 -7
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +3 -4
- package/dist/live-adapter-contract.js +30 -9
- package/dist/paths.js +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tmux-session — resolve the run-owned tmux session a log-tail window belongs in.
|
|
3
|
+
#
|
|
4
|
+
# Responsibility split: the orchestrator (farmslot) NAMES the session; the harness
|
|
5
|
+
# only POPULATES windows inside it. The name is therefore never guessed from the
|
|
6
|
+
# slot number — a renamed pool or a manual run would land the window (and its
|
|
7
|
+
# tail -F) in the wrong place. Resolution ladder, first hit wins:
|
|
8
|
+
# 1. RECIPE_TMUX_SESSION — the orchestrator's explicit hand-off.
|
|
9
|
+
# 2. agentic-runtime.json `session` — the prepared checkout's recorded session.
|
|
10
|
+
# 3. the current session — but ONLY inside a tmux client (never the
|
|
11
|
+
# last-attached session reported outside one, which would be a foreign leak).
|
|
12
|
+
# Prints the resolved name (empty when none). Callers still gate on has-session
|
|
13
|
+
# before creating a window.
|
|
14
|
+
|
|
15
|
+
# shellcheck disable=SC2329 # sourced by leaves and by contract tests.
|
|
16
|
+
resolve_run_tmux_session() {
|
|
17
|
+
local runtime_dir="${1:-}" ctx="" session=""
|
|
18
|
+
if [ -n "${RECIPE_TMUX_SESSION:-}" ]; then
|
|
19
|
+
printf '%s\n' "$RECIPE_TMUX_SESSION"
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
ctx="${RECIPE_RUNTIME_CONTEXT:-}"
|
|
23
|
+
if [ -z "$ctx" ] && [ -n "$runtime_dir" ]; then ctx="$runtime_dir/agentic-runtime.json"; fi
|
|
24
|
+
if [ -n "$ctx" ] && [ -f "$ctx" ] && command -v node >/dev/null 2>&1; then
|
|
25
|
+
session="$(RUN_TMUX_CTX="$ctx" node -e 'try{const d=JSON.parse(require("fs").readFileSync(process.env.RUN_TMUX_CTX,"utf8"));const s=d&&d.session;if(s!==undefined&&s!==null&&s!=="")process.stdout.write(String(s));}catch{}' 2>/dev/null || true)"
|
|
26
|
+
if [ -n "$session" ]; then
|
|
27
|
+
printf '%s\n' "$session"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
if [ -n "${TMUX:-}" ]; then
|
|
32
|
+
tmux display-message -p '#S' 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { recipeRuntimeDir } from "../../paths.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveSlotPortsByRepo } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
8
8
|
const RUNWAY_IOS_METADATA = {
|
|
9
9
|
artifactName: "ios-app-main-dev-expo",
|
|
10
10
|
workflow: "expo-dev-build.yml",
|
|
@@ -13,7 +13,7 @@ const RUNWAY_IOS_METADATA = {
|
|
|
13
13
|
fallbackRepo: "MetaMask/metamask-mobile"
|
|
14
14
|
};
|
|
15
15
|
function resolvePoolIdentity(target) {
|
|
16
|
-
const out =
|
|
16
|
+
const out = resolveSlotPortsByRepo(target);
|
|
17
17
|
const id = {};
|
|
18
18
|
if (!out) return id;
|
|
19
19
|
for (const line of String(out).split("\n")) {
|
|
@@ -95,6 +95,7 @@ async function provisionRunwayMobile(target, options) {
|
|
|
95
95
|
log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
|
|
96
96
|
bootSimulator(sim.udid ?? sim.name, options);
|
|
97
97
|
execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
98
|
+
preapproveDeepLinkScheme(sim.udid ?? sim.name, RUNWAY_IOS_METADATA.bundleId, options);
|
|
98
99
|
const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
|
|
99
100
|
return {
|
|
100
101
|
schemaVersion: 1,
|
|
@@ -412,6 +413,28 @@ function bootSimulator(device, options) {
|
|
|
412
413
|
log(options, `runway: bootstatus wait for ${device} did not confirm; continuing to install`);
|
|
413
414
|
}
|
|
414
415
|
}
|
|
416
|
+
function preapproveDeepLinkScheme(device, bundleId, options) {
|
|
417
|
+
const scheme = process.env.IOS_DEV_CLIENT_SCHEME ?? "expo-metamask";
|
|
418
|
+
try {
|
|
419
|
+
execFileSync(
|
|
420
|
+
"xcrun",
|
|
421
|
+
[
|
|
422
|
+
"simctl",
|
|
423
|
+
"spawn",
|
|
424
|
+
device,
|
|
425
|
+
"defaults",
|
|
426
|
+
"write",
|
|
427
|
+
"com.apple.launchservices.schemeapproval",
|
|
428
|
+
`com.apple.CoreSimulator.CoreSimulatorBridge-->${scheme}`,
|
|
429
|
+
"-string",
|
|
430
|
+
bundleId
|
|
431
|
+
],
|
|
432
|
+
{ stdio: ["ignore", "ignore", "ignore"], timeout: 15e3 }
|
|
433
|
+
);
|
|
434
|
+
log(options, `runway: pre-approved deep-link scheme ${scheme} \u2192 ${bundleId}`);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
415
438
|
function ensureSimulator(name, runtime, deviceType) {
|
|
416
439
|
const existing = findSimulator(name);
|
|
417
440
|
if (existing) return { name, udid: existing, created: false, runtime, deviceType };
|
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
realRepoPath,
|
|
5
5
|
resolveDefaultExtensionPorts,
|
|
6
6
|
resolveExtensionRuntimePorts,
|
|
7
|
+
resolveSlotPortsByRepo,
|
|
7
8
|
resolveFarmslotPortsByRepo,
|
|
8
9
|
resolveMobileRuntimeContext,
|
|
9
10
|
resolveMobileRuntimePorts,
|
|
10
11
|
resolveMobileSlotDefaults
|
|
11
|
-
} from "../../adapters/shared/resolve-
|
|
12
|
+
} from "../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
12
13
|
export {
|
|
13
14
|
formatKvLines,
|
|
14
15
|
inferSlotSuffix,
|
|
@@ -18,5 +19,6 @@ export {
|
|
|
18
19
|
resolveFarmslotPortsByRepo,
|
|
19
20
|
resolveMobileRuntimeContext,
|
|
20
21
|
resolveMobileRuntimePorts,
|
|
21
|
-
resolveMobileSlotDefaults
|
|
22
|
+
resolveMobileSlotDefaults,
|
|
23
|
+
resolveSlotPortsByRepo
|
|
22
24
|
};
|
|
@@ -5,10 +5,10 @@ import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.j
|
|
|
5
5
|
import { recipeRuntimeDir } from "../paths.js";
|
|
6
6
|
import {
|
|
7
7
|
resolveDefaultExtensionPorts,
|
|
8
|
-
|
|
8
|
+
resolveSlotPortsByRepo,
|
|
9
9
|
resolveMobileRuntimeContext,
|
|
10
10
|
resolveMobileSlotDefaults
|
|
11
|
-
} from "./resolve-
|
|
11
|
+
} from "./resolve-slot-ports.js";
|
|
12
12
|
function applyKVLines(output, overwrite) {
|
|
13
13
|
for (const line of output.split("\n")) {
|
|
14
14
|
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
@@ -41,7 +41,7 @@ function applyKVLines(output, overwrite) {
|
|
|
41
41
|
function resolveMobileSlotPorts(target) {
|
|
42
42
|
const ctxOut = resolveMobileRuntimeContext(target);
|
|
43
43
|
if (ctxOut?.trim()) applyKVLines(ctxOut, true);
|
|
44
|
-
const poolOut =
|
|
44
|
+
const poolOut = resolveSlotPortsByRepo(target);
|
|
45
45
|
if (poolOut?.trim()) applyKVLines(poolOut, false);
|
|
46
46
|
const defOut = resolveMobileSlotDefaults(target);
|
|
47
47
|
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
@@ -58,14 +58,12 @@ function resolveExtensionSlotPorts(target) {
|
|
|
58
58
|
process.env["WATCHER_PORT"] = dev;
|
|
59
59
|
process.env["RECIPE_WATCHER_PORT"] = dev;
|
|
60
60
|
}
|
|
61
|
-
if (process.env["CDP_PORT"])
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
if (!process.env["CDP_PORT"] || !process.env["WATCHER_PORT"]) {
|
|
62
|
+
const poolOut = resolveSlotPortsByRepo(target);
|
|
63
|
+
if (poolOut?.trim()) applyKVLines(poolOut, false);
|
|
64
|
+
const defOut = resolveDefaultExtensionPorts(target);
|
|
65
|
+
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
66
66
|
}
|
|
67
|
-
const defOut = resolveDefaultExtensionPorts(target);
|
|
68
|
-
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
69
67
|
}
|
|
70
68
|
function stopExtensionWatcher(target) {
|
|
71
69
|
const runtimeAbs = path.join(target, recipeRuntimeDir());
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,7 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
16
16
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
17
17
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir"] },
|
|
18
|
-
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"] },
|
|
18
|
+
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
|
|
19
19
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
20
20
|
{ name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
|
|
21
21
|
{ name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
|
package/dist/commands/call.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
runtimeOptionsFromCli,
|
|
15
15
|
usageError
|
|
16
16
|
} from "./parse-args.js";
|
|
17
|
+
import { handleListExecutables } from "./list-executables.js";
|
|
17
18
|
import {
|
|
18
19
|
emitHealViolation,
|
|
19
20
|
executeWithHealBounds,
|
|
@@ -24,6 +25,10 @@ import {
|
|
|
24
25
|
validateRecipeAdapterAware
|
|
25
26
|
} from "./run-engine.js";
|
|
26
27
|
async function handleCall(argv) {
|
|
28
|
+
if (argv.includes("--list")) {
|
|
29
|
+
const { options: options2 } = parseArgs(argv, "call");
|
|
30
|
+
return handleListExecutables("call", options2);
|
|
31
|
+
}
|
|
27
32
|
if (argv.length > 0 && argv[0].startsWith("--")) {
|
|
28
33
|
const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
|
|
29
34
|
console.error(message);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -44,7 +44,8 @@ async function handleDoctor({ options }) {
|
|
|
44
44
|
} catch {
|
|
45
45
|
}
|
|
46
46
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
47
|
-
|
|
47
|
+
const capture = captureHelperHealth();
|
|
48
|
+
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
|
|
48
49
|
else {
|
|
49
50
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
50
51
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -67,9 +68,28 @@ async function handleDoctor({ options }) {
|
|
|
67
68
|
);
|
|
68
69
|
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
69
70
|
}
|
|
71
|
+
if (capture) {
|
|
72
|
+
console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
|
|
73
|
+
if (capture.status !== "pass") {
|
|
74
|
+
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
75
|
+
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
return result.status === "pass" ? 0 : 1;
|
|
72
80
|
}
|
|
81
|
+
function captureHelperHealth() {
|
|
82
|
+
if (process.platform !== "darwin") return null;
|
|
83
|
+
const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
84
|
+
try {
|
|
85
|
+
const out = execFileSync(bin, ["doctor", "--json"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
|
|
86
|
+
const parsed = JSON.parse(out);
|
|
87
|
+
const failing = (parsed.checks ?? []).filter((c) => c.required === true && c.ok === false).map((c) => c.name ?? c.id ?? "check");
|
|
88
|
+
return { status: parsed.ok === true && failing.length === 0 ? "pass" : "warn", failing };
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
73
93
|
function detectOrphanMetros(target, livePort) {
|
|
74
94
|
try {
|
|
75
95
|
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from "./shared.js";
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
6
6
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
7
7
|
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
8
8
|
"CDP not reachable",
|
|
@@ -49,13 +49,15 @@ async function handleFixtures(argv, deps) {
|
|
|
49
49
|
return exitCode;
|
|
50
50
|
}
|
|
51
51
|
const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
52
|
+
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
53
|
+
`);
|
|
52
54
|
let status;
|
|
53
55
|
if (adapter === "mobile") {
|
|
54
56
|
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
55
57
|
const previousAppRoot = process.env.APP_ROOT;
|
|
56
58
|
process.env.APP_ROOT = target;
|
|
57
59
|
try {
|
|
58
|
-
let result =
|
|
60
|
+
let result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
|
|
59
61
|
if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env["RECIPE_SETUP_WALLET_RETRIED"]) {
|
|
60
62
|
process.env["RECIPE_SETUP_WALLET_RETRIED"] = "1";
|
|
61
63
|
if (!json) process.stderr.write(" setup-wallet: recoverable failure \u2014 restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n");
|
|
@@ -63,7 +65,7 @@ async function handleFixtures(argv, deps) {
|
|
|
63
65
|
const platform = str(options, "platform") ?? process.env["MOBILE_PLATFORM"] ?? "ios";
|
|
64
66
|
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: "auto", clearMetro: true });
|
|
65
67
|
if (relaunchResult.status === 0) {
|
|
66
|
-
result =
|
|
68
|
+
result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
status = result.status === 0 ? "pass" : "fail";
|
|
@@ -2,10 +2,26 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
5
6
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
6
7
|
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
7
8
|
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
8
9
|
import { spawnScriptStreaming } from "../shared.js";
|
|
10
|
+
function extensionDepsBlock(target) {
|
|
11
|
+
if (!fs.existsSync(path.join(target, "package.json"))) return null;
|
|
12
|
+
const deps = depsCheck(target);
|
|
13
|
+
const userAction = "yarn install --immutable # then re-run: mm-harness launch";
|
|
14
|
+
if (deps.status === "missing") {
|
|
15
|
+
return { message: "extension dependencies are not installed (no yarn install-state markers).", userAction };
|
|
16
|
+
}
|
|
17
|
+
if (deps.status === "stale") {
|
|
18
|
+
return {
|
|
19
|
+
message: "extension dependencies are stale (package.json/yarn.lock changed since the last install) \u2014 webpack would crash mid-build.",
|
|
20
|
+
userAction
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
9
25
|
async function launchExtension(target, tier, wantWatch) {
|
|
10
26
|
if (wantWatch) {
|
|
11
27
|
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
@@ -90,7 +106,9 @@ async function extensionRebuild(target) {
|
|
|
90
106
|
return result;
|
|
91
107
|
}
|
|
92
108
|
export {
|
|
109
|
+
extensionDepsBlock,
|
|
93
110
|
extensionReattach,
|
|
94
111
|
extensionRebuild,
|
|
112
|
+
extensionRuntimeReusable,
|
|
95
113
|
launchExtension
|
|
96
114
|
};
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
newHealState,
|
|
24
24
|
parseHeal
|
|
25
25
|
} from "../../heal-bounds.js";
|
|
26
|
-
import { launchExtension } from "./extension.js";
|
|
26
|
+
import { extensionDepsBlock, extensionRuntimeReusable, launchExtension } from "./extension.js";
|
|
27
27
|
import { launchMobile } from "./mobile.js";
|
|
28
28
|
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
29
29
|
"build",
|
|
@@ -111,6 +111,15 @@ async function handleLaunch(argv) {
|
|
|
111
111
|
if (json) liveArgs.push("--json");
|
|
112
112
|
return handleHarness(liveArgs);
|
|
113
113
|
}
|
|
114
|
+
if (adapter === "extension") {
|
|
115
|
+
const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
|
|
116
|
+
if (willBuild) {
|
|
117
|
+
const depsBlock = extensionDepsBlock(target);
|
|
118
|
+
if (depsBlock) {
|
|
119
|
+
return usageOut(json, "launch", depsBlock.message, depsBlock.userAction);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
114
123
|
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
115
124
|
if (attempt.status === 0) {
|
|
116
125
|
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { loadActionManifest } from "../manifest.js";
|
|
2
|
+
import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
|
|
3
|
+
import { EXIT } from "./shared.js";
|
|
4
|
+
import { optionFlag, optionString, resolveAdapter } from "./parse-args.js";
|
|
5
|
+
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
6
|
+
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
7
|
+
async function listFlowIds() {
|
|
8
|
+
try {
|
|
9
|
+
const sources = await resolveMetaMaskLibrarySources(void 0);
|
|
10
|
+
if (!sources || sources.length === 0) return [];
|
|
11
|
+
const harness = await importRecipeHarness();
|
|
12
|
+
const resolution = await harness.loadRecipeLibraries(sources);
|
|
13
|
+
return [...resolution.flows.keys()].sort();
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function handleListExecutables(command, options) {
|
|
19
|
+
const json = optionFlag(options, "json");
|
|
20
|
+
const { adapter } = resolveAdapter(options);
|
|
21
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
22
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
23
|
+
const names = getRecipeActionManifestActionNames(manifest).slice().sort();
|
|
24
|
+
const shortCounts = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const name of names) shortCounts.set(finalSegment(name), (shortCounts.get(finalSegment(name)) ?? 0) + 1);
|
|
26
|
+
const actions = names.map((name) => {
|
|
27
|
+
const short = finalSegment(name);
|
|
28
|
+
return { name, short: shortCounts.get(short) === 1 ? short : null };
|
|
29
|
+
});
|
|
30
|
+
const flows = await listFlowIds();
|
|
31
|
+
if (json) {
|
|
32
|
+
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions, flows }, null, 2));
|
|
33
|
+
return EXIT.ok;
|
|
34
|
+
}
|
|
35
|
+
console.log(`invocable for ${adapter}:`);
|
|
36
|
+
console.log(` actions (mm-harness call <name> \u2014 short or full):`);
|
|
37
|
+
for (const entry of actions) {
|
|
38
|
+
console.log(entry.short ? ` ${entry.short} (${entry.name})` : ` ${entry.name} (full name only \u2014 ambiguous short)`);
|
|
39
|
+
}
|
|
40
|
+
if (flows.length > 0) {
|
|
41
|
+
console.log(` flows (mm-harness run <flow>):`);
|
|
42
|
+
for (const flow of flows) console.log(` ${flow}`);
|
|
43
|
+
}
|
|
44
|
+
return EXIT.ok;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
handleListExecutables
|
|
48
|
+
};
|
package/dist/commands/logs.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnInherit, str, targetOf, usageOut } from "./shared.js";
|
|
6
|
-
const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json"]);
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "./shared.js";
|
|
6
|
+
const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json", "window"]);
|
|
7
7
|
async function handleLogs(argv) {
|
|
8
8
|
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
9
9
|
const json = flag(options, "json");
|
|
@@ -43,6 +43,29 @@ async function handleLogs(argv) {
|
|
|
43
43
|
surface.hints.launch
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
+
if (flag(options, "window")) {
|
|
47
|
+
surface.resolveSlotPorts(target);
|
|
48
|
+
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
|
|
49
|
+
const windowName = adapter === "mobile" ? `metro-${port}` : `webpack-${port}`;
|
|
50
|
+
const devLog = logSources[0].path;
|
|
51
|
+
const leaf = path.join(runnerDir, "adapters/shared/open-log-window.sh");
|
|
52
|
+
const result = spawnScript(
|
|
53
|
+
leaf,
|
|
54
|
+
["--window", windowName, "--log", devLog, "--runtime-dir", path.dirname(devLog)],
|
|
55
|
+
target,
|
|
56
|
+
json
|
|
57
|
+
);
|
|
58
|
+
if (json) {
|
|
59
|
+
console.log(
|
|
60
|
+
JSON.stringify(
|
|
61
|
+
{ schemaVersion: 1, command: "logs", action: "window", adapter, window: windowName, logFile: devLog, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
62
|
+
null,
|
|
63
|
+
2
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
68
|
+
}
|
|
46
69
|
if (json) {
|
|
47
70
|
console.log(JSON.stringify({ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: EXIT.ok }, null, 2));
|
|
48
71
|
return EXIT.ok;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
2
|
+
import { EXIT } from "./shared.js";
|
|
2
3
|
import {
|
|
3
4
|
actionManifestPathOption,
|
|
4
5
|
isRecord,
|
|
@@ -19,9 +20,22 @@ async function handleActions({ options }) {
|
|
|
19
20
|
const { adapter } = resolveAdapter(options);
|
|
20
21
|
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
21
22
|
await validateManifest(manifest);
|
|
23
|
+
const json = optionFlag(options, "json");
|
|
22
24
|
const action = optionString(options, "action");
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const all = describeManifestActions(manifest);
|
|
26
|
+
const actions = action ? fuzzyResolveActions(all, action) : all;
|
|
27
|
+
if (action && actions.length === 0) {
|
|
28
|
+
const message = `no action matches "${action}" for the ${adapter} adapter.`;
|
|
29
|
+
const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
|
|
30
|
+
if (json) {
|
|
31
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
32
|
+
} else {
|
|
33
|
+
console.error(`\u2717 mm-harness actions: ${message}
|
|
34
|
+
Next: ${userAction}`);
|
|
35
|
+
}
|
|
36
|
+
return EXIT.usage;
|
|
37
|
+
}
|
|
38
|
+
if (json) {
|
|
25
39
|
console.log(JSON.stringify({ adapter, actions }, null, 2));
|
|
26
40
|
} else {
|
|
27
41
|
for (const entry of actions) {
|
|
@@ -31,7 +45,15 @@ async function handleActions({ options }) {
|
|
|
31
45
|
}
|
|
32
46
|
return 0;
|
|
33
47
|
}
|
|
34
|
-
function
|
|
48
|
+
function fuzzyResolveActions(entries, query) {
|
|
49
|
+
const exactFull = entries.filter((e) => e.name === query);
|
|
50
|
+
if (exactFull.length > 0) return exactFull;
|
|
51
|
+
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
52
|
+
const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
|
|
53
|
+
if (exactSegment.length > 0) return exactSegment;
|
|
54
|
+
return entries.filter((e) => finalSegment(e.name).includes(query));
|
|
55
|
+
}
|
|
56
|
+
function describeManifestActions(manifest) {
|
|
35
57
|
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
36
58
|
const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
|
|
37
59
|
const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
|
|
@@ -45,12 +67,10 @@ function describeManifestActions(manifest, filterAction) {
|
|
|
45
67
|
}
|
|
46
68
|
return [];
|
|
47
69
|
}) : [];
|
|
48
|
-
|
|
70
|
+
return [
|
|
49
71
|
...official.map((name) => describeManifestAction(name, "official", metadata[name])),
|
|
50
72
|
...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
|
|
51
|
-
]
|
|
52
|
-
if (filterAction && entries.length === 0) throw new Error(`Action not found in manifest: ${filterAction}`);
|
|
53
|
-
return entries;
|
|
73
|
+
];
|
|
54
74
|
}
|
|
55
75
|
function describeManifestAction(name, kind, metadata) {
|
|
56
76
|
const record = isRecord(metadata) ? metadata : {};
|
package/dist/commands/run.js
CHANGED
|
@@ -3,13 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import { walletFixturePath } from "../paths.js";
|
|
4
4
|
import { EXIT } from "./shared.js";
|
|
5
5
|
import {
|
|
6
|
-
adapterOption,
|
|
7
6
|
optionFlag,
|
|
8
7
|
optionString,
|
|
9
8
|
requiredOption,
|
|
10
9
|
resolveAdapter,
|
|
11
10
|
runtimeOptionsFromCli,
|
|
12
|
-
targetPath,
|
|
13
11
|
usageError
|
|
14
12
|
} from "./parse-args.js";
|
|
15
13
|
import {
|
|
@@ -21,13 +19,14 @@ import {
|
|
|
21
19
|
runRecipe,
|
|
22
20
|
validateRunRecipeStatic
|
|
23
21
|
} from "./run-engine.js";
|
|
22
|
+
import { handleListExecutables } from "./list-executables.js";
|
|
24
23
|
async function handleRun({ positional, options }) {
|
|
24
|
+
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
25
25
|
const targetRecipe = positional[0];
|
|
26
26
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
27
27
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
28
|
-
const adapter =
|
|
28
|
+
const { adapter, target } = resolveAdapter(options);
|
|
29
29
|
const json = optionFlag(options, "json");
|
|
30
|
-
const target = targetPath(options);
|
|
31
30
|
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
32
31
|
const prepared = await prepareHeal(adapter, target, options, json);
|
|
33
32
|
if (typeof prepared === "number") return prepared;
|
|
@@ -119,18 +119,39 @@ function runProcess(command, args, options) {
|
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
function commandFor(file) {
|
|
122
|
+
function commandFor(file, projectRoot, platform) {
|
|
123
123
|
if (file.endsWith(".sh")) return { command: "bash", args: [file] };
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const needsTsx = platform === "core" || importsSourceTypescript(file);
|
|
125
|
+
if (!needsTsx && (file.endsWith(".mjs") || file.endsWith(".js"))) {
|
|
126
|
+
return { command: process.execPath, args: [file] };
|
|
127
|
+
}
|
|
128
|
+
const tsxBin = resolveTsxBin(projectRoot);
|
|
129
|
+
if (!tsxBin) {
|
|
130
|
+
const where = projectRoot ?? "the checkout";
|
|
131
|
+
const error = new Error(
|
|
132
|
+
`this action runs TypeScript from the checkout but no tsx runtime was found.
|
|
133
|
+
Next: run 'yarn install' in ${where} (tsx is a dev dependency of the checkout); if package imports still fail, run 'yarn build' there`
|
|
134
|
+
);
|
|
135
|
+
error.exitCode = 2;
|
|
136
|
+
throw error;
|
|
126
137
|
}
|
|
127
|
-
const localTsx = path.join(runnerDir, "node_modules/.bin/tsx");
|
|
128
|
-
const tsxBin = process.env.TSX_BIN || (existsSync(localTsx) ? localTsx : path.join(
|
|
129
|
-
resolveRequiredLocalProtocolRoot("TypeScript live adapter execution"),
|
|
130
|
-
"node_modules/.bin/tsx"
|
|
131
|
-
));
|
|
132
138
|
return { command: tsxBin, args: [file] };
|
|
133
139
|
}
|
|
140
|
+
function resolveTsxBin(projectRoot) {
|
|
141
|
+
if (process.env.TSX_BIN) return process.env.TSX_BIN;
|
|
142
|
+
const candidates = [];
|
|
143
|
+
if (projectRoot) candidates.push(path.join(projectRoot, "node_modules/.bin/tsx"));
|
|
144
|
+
candidates.push(path.join(runnerDir, "node_modules/.bin/tsx"));
|
|
145
|
+
for (const candidate of candidates) {
|
|
146
|
+
if (existsSync(candidate)) return candidate;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const protocolTsx = path.join(resolveRequiredLocalProtocolRoot("TypeScript live adapter execution"), "node_modules/.bin/tsx");
|
|
150
|
+
if (existsSync(protocolTsx)) return protocolTsx;
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
134
155
|
function importsSourceTypescript(file) {
|
|
135
156
|
return importsSourceTypescriptFrom(file, /* @__PURE__ */ new Set());
|
|
136
157
|
}
|
|
@@ -201,7 +222,7 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
|
201
222
|
};
|
|
202
223
|
await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
|
|
203
224
|
`);
|
|
204
|
-
const command = commandFor(script);
|
|
225
|
+
const command = commandFor(script, context.projectRoot, platform);
|
|
205
226
|
const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
|
|
206
227
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
207
228
|
cwd: context.projectRoot,
|
package/dist/paths.js
CHANGED
|
@@ -47,6 +47,9 @@ 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
|
+
process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
|
|
50
53
|
process.env.FARMSLOT_ROOT,
|
|
51
54
|
readConfiguredProtocolRoot(),
|
|
52
55
|
findProtocolRoot(runnerDir),
|
|
@@ -59,7 +62,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
|
|
|
59
62
|
const root = resolveLocalProtocolRoot();
|
|
60
63
|
if (!root) {
|
|
61
64
|
throw new Error(
|
|
62
|
-
`${reason} requires a local protocol/runtime checkout. Set FARMSLOT_ROOT or create .farmslot-root for this dev-only path.`
|
|
65
|
+
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT (or the legacy FARMSLOT_ROOT), or create .farmslot-root for this dev-only path.`
|
|
63
66
|
);
|
|
64
67
|
}
|
|
65
68
|
return root;
|