@deeeed/metamask-harness 0.6.0 → 0.6.2
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 +21 -0
- package/adapters/mobile/start-metro.sh +6 -0
- package/adapters/shared/reap-checkout-metros.sh +36 -31
- package/dist/commands/debug.js +1 -0
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/logs.js +15 -17
- package/dist/commands/shared.js +22 -0
- package/dist/mm-harness-cli.js +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.2 - 2026-07-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`-v` / `--version`** prints the mm-harness version.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **`logs` now streams live and shows the file path** — it captured output via spawnSync (never shown for a `tail -F` that doesn't exit), so `logs`/`logs --full` looked dead. New spawnInherit hands the terminal to the follow; the resolved log path + `tail -f`/`less +F` hints print upfront so you can page it yourself. Honors the test override seam.
|
|
10
|
+
- **`debug` resolves the checkout's own port** (slot context → pool → formula) like launch/stop/doctor — bare `mm-harness debug` no longer 'fetch failed' against the wrong port.
|
|
11
|
+
|
|
12
|
+
## 0.6.1 - 2026-07-06
|
|
13
|
+
|
|
14
|
+
Fresh-install fixes found live on a published-0.6.0 install.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Mobile `launch` no longer dies on empty `METAMASK_BUILD_TYPE`** — the quick-launch runs `expo start` directly (bypassing scripts/build.sh, which passes the build type as an arg); the fixture `.js.env` shipped `METAMASK_BUILD_TYPE=""`, which Metro's transform rejects (`Invalid METAMASK_BUILD_TYPE`). start-metro now defaults to the main dev client when unset, so already-installed slots launch without re-syncing fixtures.
|
|
18
|
+
- **`doctor` no longer false-flags orphan Metros** — the detector matched any argv containing "metro" (caught the `tail -F metro.log` viewer) and reported the live, valid bundler as leaked. Now it matches only `expo start` bundlers and excludes the one serving the current port.
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
- Reap-checkout-metros test hardened against a SIGTERM timing flake (polls for exit).
|
|
22
|
+
|
|
3
23
|
## 0.6.0 - 2026-07-06
|
|
4
24
|
|
|
5
25
|
Minor release: CLI hardening (decomposed, single-parser, dist-only) plus a
|
|
@@ -31,6 +51,7 @@ independently cross-reviewed.
|
|
|
31
51
|
`ensure_unlocked` fails teaching `fixtures set` on an un-onboarded wallet instead
|
|
32
52
|
of vacuously passing (mobile + extension); `provision --json` usage errors emit the
|
|
33
53
|
standard envelope; deps-not-ready teaches a command that works on a bare checkout.
|
|
54
|
+
|
|
34
55
|
## 0.5.1 - 2026-07-05
|
|
35
56
|
|
|
36
57
|
Fresh-install hotfixes found in live validation of 0.5.0.
|
|
@@ -117,6 +117,12 @@ printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FIL
|
|
|
117
117
|
# shellcheck disable=SC1091
|
|
118
118
|
[ ! -f .env.local ] || . ./.env.local
|
|
119
119
|
set +a
|
|
120
|
+
# The quick-launch runs `expo start` directly, bypassing scripts/build.sh which
|
|
121
|
+
# passes the build type as an argument (start:ios = build.sh ios main dev). The
|
|
122
|
+
# fixture .js.env ships METAMASK_BUILD_TYPE="" (empty), which Metro's transform
|
|
123
|
+
# rejects. Default to the main dev client (what runway installs / launch targets)
|
|
124
|
+
# so already-installed slots work without re-syncing fixtures.
|
|
125
|
+
[ -n "${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
|
|
120
126
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
121
127
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
122
128
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# reap-checkout-metros.sh — kill
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# reap-checkout-metros.sh — find/kill Metro (Expo) bundlers bound to a checkout.
|
|
3
|
+
# The harness starts Metro exactly one way: `yarn expo start`. Match ONLY that
|
|
4
|
+
# signature — matching bare "metro" also caught the `tail -F …/metro.log` viewer
|
|
5
|
+
# and other noise, producing false orphans.
|
|
6
6
|
#
|
|
7
|
-
# reap_checkout_metros
|
|
8
|
-
#
|
|
7
|
+
# reap_checkout_metros <checkout-abs-path> [keep-pid]
|
|
8
|
+
# detect_checkout_metros <checkout-abs-path> [live-port]
|
|
9
|
+
# reap kills every bundler for the checkout (stop wants a clean slate), sparing
|
|
10
|
+
# keep-pid. detect lists orphans for doctor and EXCLUDES the bundler serving
|
|
11
|
+
# live-port (the currently-running valid Metro is not a leak).
|
|
12
|
+
|
|
13
|
+
_checkout_metro_pids() {
|
|
14
|
+
# pids of `expo start` bundlers whose argv references this checkout (boundary
|
|
15
|
+
# anchored so /repos/mm-1 never matches /repos/mm-10).
|
|
16
|
+
local repo="$1" pid args
|
|
17
|
+
while IFS= read -r pid; do
|
|
18
|
+
[ -n "$pid" ] || continue
|
|
19
|
+
args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
|
20
|
+
case "$args" in *"$repo/"*|*"$repo "*) : ;; *) continue ;; esac
|
|
21
|
+
case "$args" in *"expo start"*) printf '%s\n' "$pid" ;; esac
|
|
22
|
+
done <<LIST
|
|
23
|
+
$(pgrep -f "expo start" 2>/dev/null)
|
|
24
|
+
LIST
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
reap_checkout_metros() {
|
|
10
|
-
local repo="$1" keep="${2:-}" reaped=""
|
|
28
|
+
local repo="$1" keep="${2:-}" reaped="" pid
|
|
11
29
|
[ -n "$repo" ] || return 0
|
|
12
|
-
# Match the bundler's argv: `expo start` / metro under this checkout. ps argv
|
|
13
|
-
# embeds the cwd-launched command; expo/metro run with the repo on the path.
|
|
14
|
-
local pid args
|
|
15
30
|
while IFS= read -r pid; do
|
|
16
31
|
[ -n "$pid" ] || continue
|
|
17
32
|
[ "$pid" = "$keep" ] && continue
|
|
18
|
-
|
|
19
|
-
# Anchor to a path boundary so /repos/mm-1 does not match /repos/mm-10.
|
|
20
|
-
case "$args" in
|
|
21
|
-
*"$repo/"*|*"$repo "*) : ;; # argv references this checkout
|
|
22
|
-
*) continue ;;
|
|
23
|
-
esac
|
|
24
|
-
case "$args" in
|
|
25
|
-
*"expo start"*|*"metro"*|*"react-native/cli"*|*"@react-native-community/cli"*)
|
|
26
|
-
kill "$pid" 2>/dev/null && reaped="$reaped $pid" ;;
|
|
27
|
-
esac
|
|
33
|
+
kill "$pid" 2>/dev/null && reaped="$reaped $pid"
|
|
28
34
|
done <<LIST
|
|
29
|
-
$(
|
|
35
|
+
$(_checkout_metro_pids "$repo")
|
|
30
36
|
LIST
|
|
31
37
|
if [ -n "$reaped" ]; then
|
|
32
38
|
printf 'Reaped leaked Metro bundler(s) for this checkout:%s\n' "$reaped" >&2
|
|
@@ -34,20 +40,19 @@ LIST
|
|
|
34
40
|
return 0
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
# detect_checkout_metros <checkout-abs-path> — print pids of every metro/expo
|
|
38
|
-
# bundler bound to the checkout, one per line, WITHOUT killing. Powers doctor's
|
|
39
|
-
# orphan visibility so leaked bundlers are surfaced without a launch.
|
|
40
43
|
detect_checkout_metros() {
|
|
41
|
-
local repo="$1" pid args
|
|
44
|
+
local repo="$1" live_port="${2:-}" pid args
|
|
42
45
|
[ -n "$repo" ] || return 0
|
|
43
46
|
while IFS= read -r pid; do
|
|
44
47
|
[ -n "$pid" ] || continue
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
# A bundler serving the currently-managed port is the LIVE valid Metro, not
|
|
49
|
+
# an orphan — exclude it so doctor never flags the running server.
|
|
50
|
+
if [ -n "$live_port" ]; then
|
|
51
|
+
args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
|
52
|
+
case "$args" in *"--port $live_port"*|*"--port=$live_port"*) continue ;; esac
|
|
53
|
+
fi
|
|
54
|
+
printf '%s\n' "$pid"
|
|
50
55
|
done <<LIST
|
|
51
|
-
$(
|
|
56
|
+
$(_checkout_metro_pids "$repo")
|
|
52
57
|
LIST
|
|
53
58
|
}
|
package/dist/commands/debug.js
CHANGED
|
@@ -11,6 +11,7 @@ async function handleDebug(argv) {
|
|
|
11
11
|
if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
12
12
|
const surface = getAdapterSurface(adapter);
|
|
13
13
|
if (surface.headless) return usageOut(json, "debug", "core is headless; there is no debug console.", surface.hints.relaunch);
|
|
14
|
+
surface.resolveSlotPorts(target);
|
|
14
15
|
const worker = flag(options, "worker");
|
|
15
16
|
const devMenu = flag(options, "devMenu");
|
|
16
17
|
if (adapter === "mobile" && worker) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -43,7 +43,7 @@ async function handleDoctor({ options }) {
|
|
|
43
43
|
runtime = await surface.runtimeStatus(target);
|
|
44
44
|
} catch {
|
|
45
45
|
}
|
|
46
|
-
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target) : [];
|
|
46
|
+
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
47
47
|
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros }, null, 2));
|
|
48
48
|
else {
|
|
49
49
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -70,10 +70,10 @@ async function handleDoctor({ options }) {
|
|
|
70
70
|
}
|
|
71
71
|
return result.status === "pass" ? 0 : 1;
|
|
72
72
|
}
|
|
73
|
-
function detectOrphanMetros(target) {
|
|
73
|
+
function detectOrphanMetros(target, livePort) {
|
|
74
74
|
try {
|
|
75
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], {
|
|
76
|
+
const out = execFileSync("bash", ["-c", `. "$1"; detect_checkout_metros "$2" "$3"`, "mm-harness-doctor", script, target, livePort ?? ""], {
|
|
77
77
|
encoding: "utf8",
|
|
78
78
|
timeout: 5e3
|
|
79
79
|
});
|
package/dist/commands/logs.js
CHANGED
|
@@ -2,7 +2,7 @@ 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,
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnInherit, str, targetOf, usageOut } from "./shared.js";
|
|
6
6
|
const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json"]);
|
|
7
7
|
async function handleLogs(argv) {
|
|
8
8
|
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
@@ -43,30 +43,28 @@ async function handleLogs(argv) {
|
|
|
43
43
|
surface.hints.launch
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
+
if (json) {
|
|
47
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: EXIT.ok }, null, 2));
|
|
48
|
+
return EXIT.ok;
|
|
49
|
+
}
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
`${source} log: ${logFile}
|
|
52
|
+
tail it yourself: tail -f ${logFile} \xB7 page it: less +F ${logFile}
|
|
53
|
+
following below (Ctrl-C to stop) \u2026
|
|
54
|
+
`
|
|
55
|
+
);
|
|
46
56
|
const full = flag(options, "full");
|
|
47
57
|
if (full) {
|
|
48
|
-
|
|
49
|
-
return result2.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
58
|
+
return spawnInherit("tail", ["-n", "+1", "-F", logFile], target) === 0 ? EXIT.ok : EXIT.runtime;
|
|
50
59
|
}
|
|
51
60
|
const logTui = path.join(runnerDir, "adapters/shared/log-tui.mjs");
|
|
52
61
|
const eventCount = process.env.RECIPE_LOG_EVENTS ?? "20";
|
|
53
62
|
const uiMode = process.env.RECIPE_LOG_UI_MODE ?? "compact";
|
|
54
|
-
|
|
63
|
+
return spawnInherit(
|
|
55
64
|
process.execPath,
|
|
56
65
|
[logTui, "tail", "--log", logFile, "--events", eventCount, "--follow", "--mode", uiMode],
|
|
57
|
-
target
|
|
58
|
-
|
|
59
|
-
);
|
|
60
|
-
if (json) {
|
|
61
|
-
console.log(
|
|
62
|
-
JSON.stringify(
|
|
63
|
-
{ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
64
|
-
null,
|
|
65
|
-
2
|
|
66
|
-
)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
66
|
+
target
|
|
67
|
+
) === 0 ? EXIT.ok : EXIT.runtime;
|
|
70
68
|
}
|
|
71
69
|
export {
|
|
72
70
|
handleLogs
|
package/dist/commands/shared.js
CHANGED
|
@@ -85,6 +85,27 @@ function spawnScript(script, args, cwd, json, env) {
|
|
|
85
85
|
if (!json && output) process.stderr.write(output);
|
|
86
86
|
return { status: result.status ?? 1, output };
|
|
87
87
|
}
|
|
88
|
+
function spawnInherit(script, args, cwd) {
|
|
89
|
+
const isNodeScript = script === process.execPath && args.length > 0;
|
|
90
|
+
const stem = (isNodeScript ? path.basename(args[0]) : path.basename(script)).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
91
|
+
const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
|
|
92
|
+
const bin = override ?? script;
|
|
93
|
+
const directArgs = override !== void 0 && isNodeScript ? args.slice(1) : args;
|
|
94
|
+
const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
|
|
95
|
+
const result = spawnSync(invokeBin, spawnArgs, { cwd, stdio: "inherit", env: process.env });
|
|
96
|
+
if (result.error) {
|
|
97
|
+
const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
|
|
98
|
+
const code = result.error.code ?? "ESPAWN";
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
`leaf could not start: ${leaf} (${code})
|
|
101
|
+
Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the leaf is missing or not executable
|
|
102
|
+
`
|
|
103
|
+
);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
if (result.status !== null) return result.status;
|
|
107
|
+
return result.signal === "SIGINT" || result.signal === "SIGTERM" ? 0 : 1;
|
|
108
|
+
}
|
|
88
109
|
function spawnScriptStreaming(script, args, cwd, env) {
|
|
89
110
|
const isNodeScript = script === process.execPath && args.length > 0;
|
|
90
111
|
const stem = isNodeScript ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase() : path.basename(script).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
@@ -149,6 +170,7 @@ export {
|
|
|
149
170
|
flag,
|
|
150
171
|
parseFlags,
|
|
151
172
|
resolveAdapter,
|
|
173
|
+
spawnInherit,
|
|
152
174
|
spawnScript,
|
|
153
175
|
spawnScriptStreaming,
|
|
154
176
|
str,
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -469,8 +469,15 @@ function translateActionsRaw(argv) {
|
|
|
469
469
|
const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
|
|
470
470
|
return ["manifest", ...withJson];
|
|
471
471
|
}
|
|
472
|
+
const pkgVersion = (() => {
|
|
473
|
+
try {
|
|
474
|
+
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
|
|
475
|
+
} catch {
|
|
476
|
+
return "unknown";
|
|
477
|
+
}
|
|
478
|
+
})();
|
|
472
479
|
const program = new Command();
|
|
473
|
-
program.name("mm-harness").description("the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay").helpOption("-h, --help", "Show grouped help").showHelpAfterError("(run `mm-harness --help` for the full surface)").configureHelp({ formatHelp: () => groupedHelp() });
|
|
480
|
+
program.name("mm-harness").description("the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay").version(pkgVersion, "-v, --version", "Print the mm-harness version").helpOption("-h, --help", "Show grouped help").showHelpAfterError("(run `mm-harness --help` for the full surface)").configureHelp({ formatHelp: () => groupedHelp() });
|
|
474
481
|
for (const command of REAL) {
|
|
475
482
|
program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help").configureHelp({ formatHelp: () => `${command.helpText}
|
|
476
483
|
` }).argument("[args...]").action(async () => {
|