@deeeed/metamask-harness 0.6.3 → 0.7.1
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 +82 -0
- package/adapters/core/inject.sh +7 -2
- package/adapters/extension/inject.mjs +14 -2
- package/adapters/extension/start-watch.sh +11 -17
- package/adapters/manifest.json +57 -9
- package/adapters/mobile/inject.sh +7 -2
- 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 +3 -3
- package/dist/commands/call.js +5 -0
- package/dist/commands/doctor.js +55 -2
- package/dist/commands/fixtures.js +5 -3
- package/dist/commands/launch/extension.js +18 -0
- package/dist/commands/launch/index.js +11 -4
- 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 +2 -0
- package/dist/commands/run.js +3 -4
- package/dist/live-adapter-contract.js +30 -9
- package/dist/mm-harness-cli.js +16 -1
- 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
|
@@ -11,11 +11,11 @@ const SPEC = {
|
|
|
11
11
|
{ name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
|
|
12
12
|
{ name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
|
|
13
13
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
14
|
-
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f"] },
|
|
14
|
+
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
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
|
-
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir"] },
|
|
18
|
-
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"] },
|
|
17
|
+
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port"] },
|
|
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
|
@@ -7,7 +7,7 @@ import { assertAdapter, runnerDir } from "../paths.js";
|
|
|
7
7
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
8
8
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
9
9
|
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
10
|
-
import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
|
|
10
|
+
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
11
11
|
import {
|
|
12
12
|
actionManifestPathOption,
|
|
13
13
|
applyRuntimeDirOption,
|
|
@@ -15,10 +15,24 @@ import {
|
|
|
15
15
|
optionString,
|
|
16
16
|
targetPath
|
|
17
17
|
} from "./parse-args.js";
|
|
18
|
+
function applyDoctorRuntimePorts(options) {
|
|
19
|
+
const cdpPort = optionString(options, "cdpPort");
|
|
20
|
+
if (cdpPort) {
|
|
21
|
+
process.env.CDP_PORT = cdpPort;
|
|
22
|
+
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
23
|
+
}
|
|
24
|
+
const watcherPort = optionString(options, "watcherPort") ?? optionString(options, "port") ?? optionString(options, "metroPort");
|
|
25
|
+
if (watcherPort) {
|
|
26
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
27
|
+
process.env.METRO_PORT = watcherPort;
|
|
28
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
async function handleDoctor({ options }) {
|
|
19
32
|
applyRuntimeDirOption(options);
|
|
20
33
|
const target = targetPath(options);
|
|
21
34
|
const json = optionFlag(options, "json");
|
|
35
|
+
const expectLive = optionFlag(options, "expectLive");
|
|
22
36
|
const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
|
|
23
37
|
const adapter = explicitAdapter ?? detectAdapter(target);
|
|
24
38
|
if (!adapter) {
|
|
@@ -40,11 +54,14 @@ async function handleDoctor({ options }) {
|
|
|
40
54
|
try {
|
|
41
55
|
const surface = getAdapterSurface(adapter);
|
|
42
56
|
surface.resolveSlotPorts(target);
|
|
57
|
+
applyDoctorRuntimePorts(options);
|
|
43
58
|
runtime = await surface.runtimeStatus(target);
|
|
44
59
|
} catch {
|
|
45
60
|
}
|
|
46
61
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
47
|
-
|
|
62
|
+
const capture = captureHelperHealth();
|
|
63
|
+
if (expectLive) return emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, json);
|
|
64
|
+
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
|
|
48
65
|
else {
|
|
49
66
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
50
67
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -67,9 +84,45 @@ async function handleDoctor({ options }) {
|
|
|
67
84
|
);
|
|
68
85
|
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
69
86
|
}
|
|
87
|
+
if (capture) {
|
|
88
|
+
console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
|
|
89
|
+
if (capture.status !== "pass") {
|
|
90
|
+
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
91
|
+
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
70
94
|
}
|
|
71
95
|
return result.status === "pass" ? 0 : 1;
|
|
72
96
|
}
|
|
97
|
+
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, json) {
|
|
98
|
+
const live = runtime?.decision === "ready";
|
|
99
|
+
if (json) {
|
|
100
|
+
console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
|
|
101
|
+
return live ? EXIT.ok : EXIT.runtime;
|
|
102
|
+
}
|
|
103
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
104
|
+
if (live) {
|
|
105
|
+
console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
|
|
106
|
+
return EXIT.ok;
|
|
107
|
+
}
|
|
108
|
+
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
109
|
+
console.error(`${out("err", "not-live")} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
110
|
+
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
111
|
+
console.error(` Next: ${getAdapterSurface(adapter).hints.relaunch}`);
|
|
112
|
+
return EXIT.runtime;
|
|
113
|
+
}
|
|
114
|
+
function captureHelperHealth() {
|
|
115
|
+
if (process.platform !== "darwin") return null;
|
|
116
|
+
const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
117
|
+
try {
|
|
118
|
+
const out = execFileSync(bin, ["doctor", "--json"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
|
|
119
|
+
const parsed = JSON.parse(out);
|
|
120
|
+
const failing = (parsed.checks ?? []).filter((c) => c.required === true && c.ok === false).map((c) => c.name ?? c.id ?? "check");
|
|
121
|
+
return { status: parsed.ok === true && failing.length === 0 ? "pass" : "warn", failing };
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
73
126
|
function detectOrphanMetros(target, livePort) {
|
|
74
127
|
try {
|
|
75
128
|
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",
|
|
@@ -32,9 +32,7 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
|
32
32
|
"sidepanel",
|
|
33
33
|
"fullscreen",
|
|
34
34
|
"runway",
|
|
35
|
-
"json"
|
|
36
|
-
"jsonStream",
|
|
37
|
-
"yes"
|
|
35
|
+
"json"
|
|
38
36
|
]);
|
|
39
37
|
async function handleLaunch(argv) {
|
|
40
38
|
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
@@ -111,6 +109,15 @@ async function handleLaunch(argv) {
|
|
|
111
109
|
if (json) liveArgs.push("--json");
|
|
112
110
|
return handleHarness(liveArgs);
|
|
113
111
|
}
|
|
112
|
+
if (adapter === "extension") {
|
|
113
|
+
const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
|
|
114
|
+
if (willBuild) {
|
|
115
|
+
const depsBlock = extensionDepsBlock(target);
|
|
116
|
+
if (depsBlock) {
|
|
117
|
+
return usageOut(json, "launch", depsBlock.message, depsBlock.userAction);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
114
121
|
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
115
122
|
if (attempt.status === 0) {
|
|
116
123
|
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,
|