@deeeed/metamask-harness 0.41.0 → 0.41.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 +6 -0
- package/adapters/manifest.json +17 -1
- package/adapters/mobile/coalesce-metro-log.cjs +24 -0
- package/adapters/mobile/launch-metro.cjs +9 -8
- package/adapters/mobile/metro-log-generation.cjs +106 -0
- package/adapters/mobile/start-metro.sh +17 -7
- package/adapters/mobile/stop-metro.sh +5 -6
- package/adapters/shared/reap-checkout-metros.sh +17 -0
- package/dist/adapters/mobile/runtime-decision.js +0 -21
- package/dist/commands/launch/index.js +25 -5
- package/docs/QA.md +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.41.1 - 2026-08-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Rotate each harness-owned Metro start into a run-scoped evidence generation, bound retained archives, coalesce repetitive bundle progress, and keep bundle-error recovery state inside one launch invocation.
|
|
10
|
+
|
|
5
11
|
## 0.41.0 - 2026-08-19
|
|
6
12
|
|
|
7
13
|
### Added
|
package/adapters/manifest.json
CHANGED
|
@@ -39,9 +39,25 @@
|
|
|
39
39
|
"entry": "adapters/mobile/launch-metro.cjs",
|
|
40
40
|
"kind": "node",
|
|
41
41
|
"purpose": "Launch Metro detached from the invoking process group so the runtime survives command cleanup.",
|
|
42
|
-
"inputs": "--target --port --log --pid-file [--workers] [--clear]",
|
|
42
|
+
"inputs": "--target --port --log --pid-file --build-env --runner [--workers] [--clear]",
|
|
43
43
|
"outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"id": "mobile/metro-log-generation",
|
|
47
|
+
"entry": "adapters/mobile/metro-log-generation.cjs",
|
|
48
|
+
"kind": "module",
|
|
49
|
+
"purpose": "Rotate one Metro generation, retain bounded archives, and publish launch evidence.",
|
|
50
|
+
"inputs": "runtime directory, Metro port, rotation reason; env MM_HARNESS_METRO_LOG_ARCHIVE_COUNT, MM_HARNESS_METRO_LOG_ARCHIVE_BYTES",
|
|
51
|
+
"outputs": "metro.log, archived logs, metro-generation.json"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "mobile/coalesce-metro-log",
|
|
55
|
+
"entry": "adapters/mobile/coalesce-metro-log.cjs",
|
|
56
|
+
"kind": "module",
|
|
57
|
+
"purpose": "Coalesce repetitive Metro percentage progress while preserving diagnostics.",
|
|
58
|
+
"inputs": "Metro output on stdin",
|
|
59
|
+
"outputs": "errors, completion, and timestamped meaningful progress on stdout"
|
|
60
|
+
},
|
|
45
61
|
{
|
|
46
62
|
"id": "mobile/metro-config",
|
|
47
63
|
"entry": "adapters/mobile/metro-config.cjs",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const readline = require('node:readline');
|
|
5
|
+
|
|
6
|
+
const progressPattern = /^\s*(?:iOS|Android).*?(\d{1,3}(?:\.\d+)?)%/u;
|
|
7
|
+
let lastPercent = null;
|
|
8
|
+
let lastProgressAt = 0;
|
|
9
|
+
|
|
10
|
+
const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
|
|
11
|
+
lines.on('line', (line) => {
|
|
12
|
+
const progress = progressPattern.exec(line);
|
|
13
|
+
if (!progress) {
|
|
14
|
+
process.stdout.write(`${line}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const percent = Math.floor(Number(progress[1]));
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (percent !== lastPercent || now - lastProgressAt >= 15_000) {
|
|
20
|
+
process.stdout.write(`${line} [metro-progress ${new Date(now).toISOString()}]\n`);
|
|
21
|
+
lastPercent = percent;
|
|
22
|
+
lastProgressAt = now;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -26,33 +26,34 @@ function parseArgs(argv) {
|
|
|
26
26
|
async function main() {
|
|
27
27
|
const args = parseArgs(process.argv.slice(2));
|
|
28
28
|
if (args.help) {
|
|
29
|
-
console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> --build-env <path> [--workers <n>] [--clear]');
|
|
29
|
+
console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> --build-env <path> --runner <path> [--workers <n>] [--clear]');
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
for (const required of ['target', 'port', 'log', 'pid-file', 'build-env']) {
|
|
32
|
+
for (const required of ['target', 'port', 'log', 'pid-file', 'build-env', 'runner']) {
|
|
33
33
|
if (!args[required]) throw new Error(`--${required} is required`);
|
|
34
34
|
}
|
|
35
35
|
const target = path.resolve(args.target);
|
|
36
36
|
const log = path.resolve(args.log);
|
|
37
37
|
const pidFile = path.resolve(args['pid-file']);
|
|
38
|
+
const runner = path.resolve(args.runner);
|
|
38
39
|
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
39
40
|
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const runnerStat = fs.lstatSync(runner);
|
|
42
|
+
if (!runnerStat.isFile()) throw new Error('--runner must be a regular file');
|
|
43
|
+
fs.accessSync(runner, fs.constants.X_OK);
|
|
42
44
|
const env = { ...process.env };
|
|
43
45
|
env.BASH_ENV = args['build-env'];
|
|
44
46
|
if (args.workers) env.METRO_MAX_WORKERS = String(args.workers);
|
|
45
|
-
const child = spawn(
|
|
47
|
+
const child = spawn(runner, [], {
|
|
46
48
|
cwd: target,
|
|
47
49
|
detached: true,
|
|
48
50
|
env,
|
|
49
|
-
stdio:
|
|
51
|
+
stdio: 'ignore',
|
|
50
52
|
});
|
|
51
53
|
await new Promise((resolve, reject) => {
|
|
52
54
|
child.once('spawn', resolve);
|
|
53
55
|
child.once('error', reject);
|
|
54
56
|
});
|
|
55
|
-
fs.closeSync(logFd);
|
|
56
57
|
fs.writeFileSync(pidFile, `${String(child.pid)}\n`);
|
|
57
58
|
fs.writeFileSync(
|
|
58
59
|
path.join(path.dirname(pidFile), 'metro-launch.json'),
|
|
@@ -64,7 +65,7 @@ async function main() {
|
|
|
64
65
|
target,
|
|
65
66
|
port: Number(args.port),
|
|
66
67
|
clear: Boolean(args.clear),
|
|
67
|
-
command:
|
|
68
|
+
command: runner,
|
|
68
69
|
}, null, 2)}\n`,
|
|
69
70
|
);
|
|
70
71
|
child.unref();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const generationIdPattern = /^[A-Za-z0-9-]+$/u;
|
|
9
|
+
|
|
10
|
+
function archiveGenerationId(name) {
|
|
11
|
+
if (!name.startsWith('metro.') || !name.endsWith('.log')) return null;
|
|
12
|
+
const generationId = name.slice('metro.'.length, -'.log'.length);
|
|
13
|
+
return generationIdPattern.test(generationId) ? generationId : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function positiveInteger(name, fallback) {
|
|
17
|
+
const raw = process.env[name];
|
|
18
|
+
if (raw === undefined || raw === '') return fallback;
|
|
19
|
+
if (!/^[1-9][0-9]*$/u.test(raw)) throw new Error(`${name} must be a positive integer`);
|
|
20
|
+
return Number(raw);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function main() {
|
|
24
|
+
const runtimeArg = process.argv[2];
|
|
25
|
+
const port = process.argv[3];
|
|
26
|
+
const reason = process.argv[4];
|
|
27
|
+
if (!runtimeArg || !/^[1-9][0-9]*$/u.test(port ?? '') || !reason) {
|
|
28
|
+
throw new Error('usage: metro-log-generation.cjs <runtime-dir> <port> <reason>');
|
|
29
|
+
}
|
|
30
|
+
const runtimeDir = fs.realpathSync(runtimeArg);
|
|
31
|
+
const activeLog = path.join(runtimeDir, 'metro.log');
|
|
32
|
+
const evidenceFile = path.join(runtimeDir, 'metro-generation.json');
|
|
33
|
+
const maxArchives = positiveInteger('MM_HARNESS_METRO_LOG_ARCHIVE_COUNT', 4);
|
|
34
|
+
const maxArchiveBytes = positiveInteger('MM_HARNESS_METRO_LOG_ARCHIVE_BYTES', 8 * 1024 * 1024);
|
|
35
|
+
const generationId = `${new Date().toISOString().replace(/[-:.]/gu, '')}-${port}-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
36
|
+
if (!generationIdPattern.test(generationId)) throw new Error('generated Metro log identity is invalid');
|
|
37
|
+
let rotatedLog = null;
|
|
38
|
+
|
|
39
|
+
rotatedLog = path.join(runtimeDir, `metro.${generationId}.log`);
|
|
40
|
+
try {
|
|
41
|
+
fs.renameSync(activeLog, rotatedLog);
|
|
42
|
+
const stat = fs.lstatSync(rotatedLog);
|
|
43
|
+
if (!stat.isFile()) {
|
|
44
|
+
fs.renameSync(rotatedLog, activeLog);
|
|
45
|
+
throw new Error('metro.log must be a regular file');
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error && error.code === 'ENOENT') rotatedLog = null;
|
|
49
|
+
else throw error;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
fs.closeSync(fs.openSync(activeLog, 'wx', 0o600));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error && error.code === 'EEXIST') throw new Error('another Metro generation recreated metro.log; refusing to truncate it');
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const archives = fs.readdirSync(runtimeDir)
|
|
59
|
+
.filter((name) => archiveGenerationId(name) !== null)
|
|
60
|
+
.map((name) => {
|
|
61
|
+
const file = path.join(runtimeDir, name);
|
|
62
|
+
const stat = fs.lstatSync(file);
|
|
63
|
+
if (!stat.isFile()) return null;
|
|
64
|
+
return { file, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.file.localeCompare(left.file));
|
|
68
|
+
|
|
69
|
+
let retainedBytes = 0;
|
|
70
|
+
let retainedCount = 0;
|
|
71
|
+
const retained = [];
|
|
72
|
+
const removed = [];
|
|
73
|
+
for (const archive of archives) {
|
|
74
|
+
if (retainedCount < maxArchives && retainedBytes + archive.size <= maxArchiveBytes) {
|
|
75
|
+
retained.push(archive.file);
|
|
76
|
+
retainedCount += 1;
|
|
77
|
+
retainedBytes += archive.size;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
fs.unlinkSync(archive.file);
|
|
81
|
+
removed.push({ path: archive.file, size: archive.size, reason: retainedCount >= maxArchives ? 'count-limit' : 'size-limit' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const evidence = {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
generationId,
|
|
87
|
+
port: Number(port),
|
|
88
|
+
startedAt: new Date().toISOString(),
|
|
89
|
+
currentLog: activeLog,
|
|
90
|
+
rotatedLog: rotatedLog && fs.existsSync(rotatedLog) ? rotatedLog : null,
|
|
91
|
+
archivedLogs: retained,
|
|
92
|
+
rotationReason: reason,
|
|
93
|
+
retention: { maxArchives, maxArchiveBytes, retainedBytes, removed },
|
|
94
|
+
};
|
|
95
|
+
const temporary = `${evidenceFile}.${process.pid}.tmp`;
|
|
96
|
+
fs.writeFileSync(temporary, `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
97
|
+
fs.renameSync(temporary, evidenceFile);
|
|
98
|
+
process.stdout.write(`${JSON.stringify(evidence)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
main();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`metro-log-generation: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
@@ -68,6 +68,8 @@ mkdir -p "$LOG_DIR"
|
|
|
68
68
|
LOG_FILE="$LOG_DIR/metro.log"
|
|
69
69
|
PID_FILE="$LOG_DIR/metro.pid"
|
|
70
70
|
METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
|
|
71
|
+
METRO_LOG_GENERATION="$SCRIPT_DIR/metro-log-generation.cjs"
|
|
72
|
+
METRO_LOG_COALESCER="$SCRIPT_DIR/coalesce-metro-log.cjs"
|
|
71
73
|
|
|
72
74
|
# --- helpers ------------------------------------------------------------------
|
|
73
75
|
|
|
@@ -172,23 +174,21 @@ write_metro_runner() {
|
|
|
172
174
|
#!/usr/bin/env bash
|
|
173
175
|
set -uo pipefail
|
|
174
176
|
cd "$(printf '%q' "$TARGET")"
|
|
175
|
-
: > "$(printf '%q' "$LOG_FILE")"
|
|
176
177
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
177
178
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
178
179
|
$worker_env_line
|
|
179
|
-
BASH_ENV=$(printf '%q' "$METRO_BUILD_ENV_FILE") command yarn $(printf '%q' "$watcher_script") 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
180
|
+
BASH_ENV=$(printf '%q' "$METRO_BUILD_ENV_FILE") command yarn $(printf '%q' "$watcher_script") 2>&1 | node $(printf '%q' "$METRO_LOG_COALESCER") | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
180
181
|
EOF
|
|
181
182
|
chmod +x "$runner"
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
start_metro_tmux() {
|
|
186
|
+
local runner="$1"
|
|
185
187
|
command -v tmux >/dev/null 2>&1 || return 1
|
|
186
|
-
local session window
|
|
188
|
+
local session window
|
|
187
189
|
session="$(resolve_run_tmux_session "$LOG_DIR")"
|
|
188
190
|
{ [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 1
|
|
189
191
|
window="metro-${PORT}"
|
|
190
|
-
runner="$LOG_DIR/run-metro-${PORT}.sh"
|
|
191
|
-
write_metro_runner "$runner"
|
|
192
192
|
tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
|
|
193
193
|
tmux new-window -d -t "$session" -n "$window" "exec $(printf '%q' "$runner")" || return 1
|
|
194
194
|
printf '%s:%s\n' "$session" "$window" > "$LOG_DIR/metro.tmux"
|
|
@@ -249,6 +249,14 @@ else
|
|
|
249
249
|
fi
|
|
250
250
|
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
|
|
251
251
|
|
|
252
|
+
[ -f "$METRO_LOG_GENERATION" ] && [ -f "$METRO_LOG_COALESCER" ] || {
|
|
253
|
+
printf 'start-metro: Metro log generation helpers are missing; reinstall the runner\n' >&2
|
|
254
|
+
exit 1
|
|
255
|
+
}
|
|
256
|
+
ROTATION_REASON="metro-start"
|
|
257
|
+
[ "$CLEAR" = true ] && ROTATION_REASON="clear-cache-restart"
|
|
258
|
+
node "$METRO_LOG_GENERATION" "$LOG_DIR" "$PORT" "$ROTATION_REASON" >/dev/null || exit 1
|
|
259
|
+
|
|
252
260
|
write_metro_build_env || {
|
|
253
261
|
printf 'start-metro: could not stage the scoped Metro environment\n' >&2
|
|
254
262
|
exit 1
|
|
@@ -256,13 +264,14 @@ write_metro_build_env || {
|
|
|
256
264
|
|
|
257
265
|
STARTED_METRO_PID=""
|
|
258
266
|
STARTED_METRO_TMUX=""
|
|
259
|
-
|
|
267
|
+
METRO_RUNNER="$LOG_DIR/run-metro-${PORT}.sh"
|
|
268
|
+
write_metro_runner "$METRO_RUNNER"
|
|
269
|
+
if start_metro_tmux "$METRO_RUNNER"; then
|
|
260
270
|
STARTED_METRO_TMUX="$(cat "$LOG_DIR/metro.tmux" 2>/dev/null || true)"
|
|
261
271
|
else
|
|
262
272
|
# Metro runs detached, writing to the log. The tmux window is a read-only tail.
|
|
263
273
|
(
|
|
264
274
|
cd "$TARGET"
|
|
265
|
-
: > "$LOG_FILE"
|
|
266
275
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
267
276
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
268
277
|
if [ -n "$METRO_WORKERS" ]; then
|
|
@@ -276,6 +285,7 @@ else
|
|
|
276
285
|
--log "$LOG_FILE"
|
|
277
286
|
--pid-file "$PID_FILE"
|
|
278
287
|
--build-env "$METRO_BUILD_ENV_FILE"
|
|
288
|
+
--runner "$METRO_RUNNER"
|
|
279
289
|
)
|
|
280
290
|
[ -z "$METRO_WORKERS" ] || launcher_args+=(--workers "$METRO_WORKERS")
|
|
281
291
|
[ "$CLEAR" = true ] && launcher_args+=(--clear)
|
|
@@ -83,15 +83,14 @@ if [ -n "$fwd_pid" ]; then
|
|
|
83
83
|
esac
|
|
84
84
|
fi
|
|
85
85
|
rm -f "$FWD_PID_FILE"
|
|
86
|
-
pkill -f "console-forwarder.cjs --port
|
|
86
|
+
pkill -f "console-forwarder.cjs --port $PORT --out $TARGET/" 2>/dev/null
|
|
87
87
|
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# Guarded: the reap lib may be absent in a minimal install; skip rather than abort.
|
|
88
|
+
# Reap a detached bundler for this exact port when its listener disappeared
|
|
89
|
+
# before the normal stop path. Other ports in the same checkout are separate
|
|
90
|
+
# runtime generations and must remain untouched.
|
|
92
91
|
if [ -f "$SCRIPT_DIR/../shared/reap-checkout-metros.sh" ]; then
|
|
93
92
|
. "$SCRIPT_DIR/../shared/reap-checkout-metros.sh"
|
|
94
|
-
|
|
93
|
+
reap_checkout_metros_on_port "$TARGET" "$PORT" || true
|
|
95
94
|
fi
|
|
96
95
|
|
|
97
96
|
# Close the read-only log-tail window start-metro opened, if it is still there.
|
|
@@ -40,6 +40,23 @@ LIST
|
|
|
40
40
|
return 0
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
reap_checkout_metros_on_port() {
|
|
44
|
+
local repo="$1" port="$2" reaped="" pid args
|
|
45
|
+
[ -n "$repo" ] && [ -n "$port" ] || return 0
|
|
46
|
+
while IFS= read -r pid; do
|
|
47
|
+
[ -n "$pid" ] || continue
|
|
48
|
+
args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
|
49
|
+
case " $args " in *" --port $port "*|*" --port=$port "*) ;; *) continue ;; esac
|
|
50
|
+
kill "$pid" 2>/dev/null && reaped="$reaped $pid"
|
|
51
|
+
done <<LIST
|
|
52
|
+
$(_checkout_metro_pids "$repo")
|
|
53
|
+
LIST
|
|
54
|
+
if [ -n "$reaped" ]; then
|
|
55
|
+
printf 'Reaped leaked Metro bundler(s) for checkout port %s:%s\n' "$port" "$reaped" >&2
|
|
56
|
+
fi
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
detect_checkout_metros() {
|
|
44
61
|
local repo="$1" live_port="${2:-}" pid args
|
|
45
62
|
[ -n "$repo" ] || return 0
|
|
@@ -2,13 +2,11 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
clearDecisionState,
|
|
6
5
|
depsCheck,
|
|
7
6
|
recordDepsBaseline
|
|
8
7
|
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
9
8
|
import {
|
|
10
9
|
analyzeBundleLog,
|
|
11
|
-
evaluatePersistentBundleError,
|
|
12
10
|
supersededErrorCapture
|
|
13
11
|
} from "@farmslot/recipe-harness/runtime/log-analysis";
|
|
14
12
|
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
@@ -160,9 +158,6 @@ async function computeMobileReadiness(resolved, options, fast) {
|
|
|
160
158
|
}
|
|
161
159
|
const metroLog = metroLogCheck(resolved, options.metroLog);
|
|
162
160
|
const metro = options.watcherPort ? await probeMetroPackager(options.watcherPort) : { status: "skipped" };
|
|
163
|
-
if (metroLog.status === "ok") {
|
|
164
|
-
clearDecisionState(resolved, "bundle-error-state.json");
|
|
165
|
-
}
|
|
166
161
|
const checks = { deps, metroLog, metro };
|
|
167
162
|
if (deps.status === "missing") {
|
|
168
163
|
return {
|
|
@@ -229,22 +224,6 @@ async function computeMobileReadiness(resolved, options, fast) {
|
|
|
229
224
|
const depsSatisfied = deps.status === "current";
|
|
230
225
|
if (depsSatisfied && (metroLog.reason === "stale-bundle-error" || metroLog.reason === "bundle-error")) {
|
|
231
226
|
const excerpt = metroLog.excerpt ?? "";
|
|
232
|
-
const persistent = evaluatePersistentBundleError(resolved, excerpt);
|
|
233
|
-
if (persistent.blocked) {
|
|
234
|
-
return {
|
|
235
|
-
schemaVersion: 1,
|
|
236
|
-
adapter: "mobile",
|
|
237
|
-
target: resolved,
|
|
238
|
-
decision: "blocked",
|
|
239
|
-
reasonCode: "bundle-error-persistent",
|
|
240
|
-
reasons: [
|
|
241
|
-
"Metro bundle keeps failing with the same error after a cache-cleared relaunch was already suggested; fix the bundle error in app code before retrying recipe up.",
|
|
242
|
-
...excerpt ? [excerpt] : []
|
|
243
|
-
],
|
|
244
|
-
checks,
|
|
245
|
-
actions: []
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
227
|
return {
|
|
249
228
|
schemaVersion: 1,
|
|
250
229
|
adapter: "mobile",
|
|
@@ -53,6 +53,21 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
|
53
53
|
"jsonStream"
|
|
54
54
|
]);
|
|
55
55
|
const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
|
|
56
|
+
function metroLaunchEvidence(adapter, target) {
|
|
57
|
+
if (adapter !== "mobile") return void 0;
|
|
58
|
+
const evidencePath = recipeRuntimePath(target, "metro-generation.json");
|
|
59
|
+
let descriptor;
|
|
60
|
+
try {
|
|
61
|
+
descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
62
|
+
const stat = fs.fstatSync(descriptor);
|
|
63
|
+
if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
|
|
64
|
+
return JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
} finally {
|
|
68
|
+
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
56
71
|
async function handleLaunch(argv) {
|
|
57
72
|
const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
58
73
|
const json = flag(options, "json");
|
|
@@ -627,14 +642,15 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
|
|
|
627
642
|
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
628
643
|
tier,
|
|
629
644
|
recovered: state.recovered,
|
|
630
|
-
mutations: state.mutations
|
|
645
|
+
mutations: state.mutations,
|
|
646
|
+
metro: metroLaunchEvidence(adapter, target)
|
|
631
647
|
});
|
|
632
648
|
}
|
|
633
649
|
return exitCode;
|
|
634
650
|
}
|
|
635
|
-
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
|
|
651
|
+
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state);
|
|
636
652
|
}
|
|
637
|
-
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
|
|
653
|
+
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state) {
|
|
638
654
|
if (stream.enabled) {
|
|
639
655
|
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
640
656
|
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
@@ -643,7 +659,8 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
|
|
|
643
659
|
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
644
660
|
tier,
|
|
645
661
|
recovered: state.recovered,
|
|
646
|
-
mutations: state.mutations
|
|
662
|
+
mutations: state.mutations,
|
|
663
|
+
metro: metroLaunchEvidence(adapter, target)
|
|
647
664
|
});
|
|
648
665
|
} else if (json) {
|
|
649
666
|
console.log(
|
|
@@ -658,6 +675,7 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
|
|
|
658
675
|
phase: "launch",
|
|
659
676
|
recovered: state.recovered,
|
|
660
677
|
mutations: state.mutations,
|
|
678
|
+
metro: metroLaunchEvidence(adapter, target),
|
|
661
679
|
exitCode: EXIT.ok
|
|
662
680
|
},
|
|
663
681
|
null,
|
|
@@ -706,7 +724,8 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
|
|
|
706
724
|
recovered: state.recovered,
|
|
707
725
|
mutations: state.mutations,
|
|
708
726
|
recoverable: failure.recoverable,
|
|
709
|
-
attemptedRecoveries: state.attemptedRecoveries
|
|
727
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
728
|
+
metro: metroLaunchEvidence(adapter, target)
|
|
710
729
|
});
|
|
711
730
|
} else if (json) {
|
|
712
731
|
console.log(
|
|
@@ -723,6 +742,7 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
|
|
|
723
742
|
mutations: state.mutations,
|
|
724
743
|
recoverable: failure.recoverable,
|
|
725
744
|
attemptedRecoveries: state.attemptedRecoveries,
|
|
745
|
+
metro: metroLaunchEvidence(adapter, target),
|
|
726
746
|
exitCode: failure.exitCode,
|
|
727
747
|
error: {
|
|
728
748
|
code: failure.code,
|
package/docs/QA.md
CHANGED
|
@@ -124,6 +124,8 @@ mm-harness debug
|
|
|
124
124
|
- [ ] Status matches verified product routes: `Login`/`LockScreen` are locked;
|
|
125
125
|
`WalletView` is unlocked.
|
|
126
126
|
- [ ] App and Metro logs are separate.
|
|
127
|
+
- [ ] A restarted Metro rotates the previous `metro.log`, records its generation and retention actions in launch JSON, and classifies bundle failures only from the current log.
|
|
128
|
+
- [ ] `mm-harness logs --full` preserves errors, completion, and meaningful timestamped bundle progress without repeated same-percentage module counts.
|
|
127
129
|
- [ ] A JS edit rebuilds through Metro and appears after reload without a native
|
|
128
130
|
rebuild; revert restores a clean tree.
|
|
129
131
|
|