@deeeed/metamask-harness 0.55.0 → 0.56.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 +13 -0
- package/dist/adapters/mobile/runtime-decision.js +1 -0
- package/dist/commands/prepare.js +207 -46
- package/dist/commands/update.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.56.0 - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `prepare` says what it is doing while it does it. It now opens with the whole chain — `doctor`, `status`, `launch`, `fixtures`, `verify`, or just `doctor`, `status`, `verify` on Core — one line each, so the wait is bounded before the first step runs. The running step counts up in place (`… launch: running 1:23`), each finished one keeps how long it took (`✓ launch: pass (0:47)`), and the slow steps carry a dim note saying what they are waiting on: `can take minutes on Mobile: Metro bundle and app start` for `launch`, `under a minute` for `fixtures`. Where the checkout has a previous `sandbox.json`, each step also shows what it cost last time (`last run 6:52`) — the only honest estimate the harness has. Before this, a Mobile prepare printed `✓ status: pass` and then nothing for the minutes the launch step ran, which reads as a hang. The clock on screen is the wall clock: one timestamp taken at the spawn feeds both the ticker and the `durationMs` in the record, and the repaint holds the event loop awake so a step that stays quiet for minutes — `verify` waiting on its bridge probe — cannot leave the count frozen behind the wall. Piped output is a plain log — the list, then each step as it lands, then the finished list. `--json` is untouched: stdout stays a single document.
|
|
10
|
+
- `sandbox.json` records `durationMs` on every readiness step (`0` for a skipped one), so a run that felt slow can be read back step by step instead of retimed by hand.
|
|
11
|
+
|
|
12
|
+
## 0.55.1 - 2026-09-18
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `doctor` probes the registry when the running build is newer than the cached latest, instead of reporting "ahead of registry" for up to an hour after a publish.
|
|
17
|
+
|
|
5
18
|
## 0.55.0 - 2026-09-18
|
|
6
19
|
|
|
7
20
|
### Added
|
package/dist/commands/prepare.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
READINESS_RECORD
|
|
6
6
|
} from "@farmslot/protocol";
|
|
7
|
+
import { color, stripAnsi } from "../cli-color.js";
|
|
7
8
|
import { runnerProvenance } from "../doctor.js";
|
|
8
9
|
import { detectAdapter } from "../harness.js";
|
|
9
10
|
import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
10
11
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
11
12
|
import { EXIT } from "./shared.js";
|
|
13
|
+
import { formatDuration } from "./status-watch.js";
|
|
12
14
|
const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]";
|
|
13
15
|
const REPORT_DIR = "prepare";
|
|
14
16
|
const SKIPPED_AFTER_FAILURE = "previous step failed";
|
|
@@ -26,31 +28,175 @@ function reportedError(stdout) {
|
|
|
26
28
|
return typeof message === "string" && message ? message : void 0;
|
|
27
29
|
}
|
|
28
30
|
function skippedStep(id, command, reason) {
|
|
29
|
-
return { id, command, status: "skipped", exitCode: 0, reason };
|
|
31
|
+
return { id, command, status: "skipped", exitCode: 0, reason, durationMs: 0 };
|
|
30
32
|
}
|
|
31
|
-
const STEP_MARK = {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const STEP_MARK = {
|
|
34
|
+
pending: "\xB7",
|
|
35
|
+
running: "\u2026",
|
|
36
|
+
pass: "\u2713",
|
|
37
|
+
fail: "\u2717",
|
|
38
|
+
skipped: "\u2013"
|
|
39
|
+
};
|
|
40
|
+
const STEP_STYLE = {
|
|
41
|
+
pending: "dim",
|
|
42
|
+
running: "accent",
|
|
43
|
+
pass: "ok",
|
|
44
|
+
fail: "err",
|
|
45
|
+
skipped: "dim"
|
|
46
|
+
};
|
|
47
|
+
const STEP_HINT = {
|
|
48
|
+
launch: "can take minutes on Mobile: Metro bundle and app start",
|
|
49
|
+
fixtures: "under a minute"
|
|
50
|
+
};
|
|
51
|
+
function stepChain(platform) {
|
|
52
|
+
return platform === "core" ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
|
|
34
53
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
function lastRunDurations(recordPath) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(fs.readFileSync(recordPath, "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
const steps = parsed?.steps;
|
|
62
|
+
if (!Array.isArray(steps)) return {};
|
|
63
|
+
const durations = {};
|
|
64
|
+
for (const step of steps) {
|
|
65
|
+
if (typeof step?.id === "string" && typeof step.durationMs === "number" && step.durationMs > 0) {
|
|
66
|
+
durations[step.id] = step.durationMs;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return durations;
|
|
70
|
+
}
|
|
71
|
+
function stepHints(chain, lastRun) {
|
|
72
|
+
const hints = {};
|
|
73
|
+
for (const id of chain) {
|
|
74
|
+
const previous = lastRun[id];
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (STEP_HINT[id]) parts.push(STEP_HINT[id]);
|
|
77
|
+
if (previous !== void 0) parts.push(`last run ${formatDuration(previous)}`);
|
|
78
|
+
if (parts.length > 0) hints[id] = parts.join(", ");
|
|
79
|
+
}
|
|
80
|
+
return hints;
|
|
81
|
+
}
|
|
82
|
+
function stepRowLine(row, width) {
|
|
83
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
84
|
+
const running = row.state === "pending" || row.state === "running";
|
|
85
|
+
const elapsed = row.state === "running" && row.startedAt !== void 0 ? ` ${out("dim", formatDuration(Date.now() - row.startedAt))}` : "";
|
|
86
|
+
const note = running ? row.hint : row.durationMs ? formatDuration(row.durationMs) : void 0;
|
|
87
|
+
const mark = out(STEP_STYLE[row.state], STEP_MARK[row.state]);
|
|
88
|
+
return `${mark} ${out(running ? "bold" : "dim", row.id.padEnd(width))}: ${out(STEP_STYLE[row.state], row.state)}${elapsed}${row.reason ? ` \u2014 ${row.reason}` : ""}${note ? ` ${out("dim", `(${note})`)}` : ""}`;
|
|
89
|
+
}
|
|
90
|
+
function createStepView(chain, hints) {
|
|
91
|
+
const rows = new Map(
|
|
92
|
+
chain.map((id) => [id, { id, state: "pending", hint: hints[id] }])
|
|
93
|
+
);
|
|
94
|
+
const width = Math.max(...chain.map((id) => id.length));
|
|
95
|
+
const tty = Boolean(process.stdout.isTTY);
|
|
96
|
+
const block = () => {
|
|
97
|
+
const terminalColumns = process.stdout.columns;
|
|
98
|
+
const columns = tty && typeof terminalColumns === "number" && terminalColumns > 0 ? terminalColumns : Number.POSITIVE_INFINITY;
|
|
99
|
+
return [...rows.values()].map((row) => {
|
|
100
|
+
const line = stepRowLine(row, width);
|
|
101
|
+
return stripAnsi(line).length <= columns ? line : stripAnsi(line).slice(0, columns);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
let painted = 0;
|
|
105
|
+
let lastFrame = "";
|
|
106
|
+
let ended = false;
|
|
107
|
+
let timer;
|
|
108
|
+
const repaint = () => {
|
|
109
|
+
const lines = block();
|
|
110
|
+
const frame = lines.join("\n");
|
|
111
|
+
if (frame === lastFrame) return;
|
|
112
|
+
lastFrame = frame;
|
|
113
|
+
const up = painted > 0 ? `\x1B[${painted}A` : "";
|
|
114
|
+
painted = lines.length;
|
|
115
|
+
process.stdout.write(`${up}${lines.map((line) => `\x1B[2K${line}`).join("\n")}
|
|
116
|
+
`);
|
|
117
|
+
};
|
|
118
|
+
const writeBlock = () => {
|
|
119
|
+
process.stdout.write(`${block().join("\n")}
|
|
120
|
+
`);
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
begin() {
|
|
124
|
+
if (tty) repaint();
|
|
125
|
+
else writeBlock();
|
|
126
|
+
},
|
|
127
|
+
start(id, startedAt) {
|
|
128
|
+
const row = rows.get(id);
|
|
129
|
+
if (!row) return;
|
|
130
|
+
row.state = "running";
|
|
131
|
+
row.startedAt = startedAt;
|
|
132
|
+
if (!tty) return;
|
|
133
|
+
repaint();
|
|
134
|
+
timer = setInterval(repaint, 250);
|
|
135
|
+
},
|
|
136
|
+
finish(step) {
|
|
137
|
+
clearInterval(timer);
|
|
138
|
+
timer = void 0;
|
|
139
|
+
const row = rows.get(step.id);
|
|
140
|
+
if (!row) return;
|
|
141
|
+
row.state = step.status;
|
|
142
|
+
row.startedAt = void 0;
|
|
143
|
+
row.reason = step.reason;
|
|
144
|
+
row.durationMs = step.durationMs;
|
|
145
|
+
if (tty) repaint();
|
|
146
|
+
else process.stdout.write(`${stepRowLine(row, width)}
|
|
147
|
+
`);
|
|
148
|
+
},
|
|
149
|
+
// Idempotent: the caller ends the view where the final block belongs in the
|
|
150
|
+
// output, and a `finally` ends it again on the paths that never got there.
|
|
151
|
+
// The ticker holds the event loop awake, so this must run either way.
|
|
152
|
+
end() {
|
|
153
|
+
if (ended) return;
|
|
154
|
+
ended = true;
|
|
155
|
+
clearInterval(timer);
|
|
156
|
+
timer = void 0;
|
|
157
|
+
if (!tty) writeBlock();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function spawnStep(executable, args, target) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const child = spawn(executable, [...args], {
|
|
164
|
+
cwd: target,
|
|
165
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
166
|
+
// The update nudge is an interactive courtesy; it has no place in the
|
|
167
|
+
// stream a recorded readiness step captures.
|
|
168
|
+
env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
|
|
169
|
+
});
|
|
170
|
+
let stdout = "";
|
|
171
|
+
let stderr = "";
|
|
172
|
+
child.stdout?.setEncoding("utf8");
|
|
173
|
+
child.stderr?.setEncoding("utf8");
|
|
174
|
+
child.stdout?.on("data", (chunk) => {
|
|
175
|
+
stdout += chunk;
|
|
176
|
+
});
|
|
177
|
+
child.stderr?.on("data", (chunk) => {
|
|
178
|
+
stderr += chunk;
|
|
179
|
+
});
|
|
180
|
+
child.on("error", (error) => resolve({ error, status: null, stdout, stderr }));
|
|
181
|
+
child.on("close", (status) => resolve({ status, stdout, stderr }));
|
|
43
182
|
});
|
|
183
|
+
}
|
|
184
|
+
async function runStep(executable, target, artifactsDir, id, args, { json, onSpawn }) {
|
|
185
|
+
const command = [executable, ...args].join(" ");
|
|
186
|
+
const startedAt = Date.now();
|
|
187
|
+
onSpawn?.(startedAt);
|
|
188
|
+
const result = await spawnStep(executable, args, target);
|
|
189
|
+
const durationMs = Date.now() - startedAt;
|
|
44
190
|
if (result.error) {
|
|
45
|
-
return { id, command, status: "fail", exitCode: 1, reason: result.error.message };
|
|
191
|
+
return { id, command, status: "fail", exitCode: 1, reason: result.error.message, durationMs };
|
|
46
192
|
}
|
|
47
193
|
const report = reportPathFor(id);
|
|
48
194
|
const file = path.join(artifactsDir, report);
|
|
49
195
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
-
fs.writeFileSync(file, result.stdout
|
|
196
|
+
fs.writeFileSync(file, result.stdout);
|
|
51
197
|
const exitCode = result.status ?? 1;
|
|
52
198
|
if (exitCode === 0) {
|
|
53
|
-
return { id, command, status: "pass", exitCode, reportPath: report };
|
|
199
|
+
return { id, command, status: "pass", exitCode, reportPath: report, durationMs };
|
|
54
200
|
}
|
|
55
201
|
if (!json && result.stderr) process.stderr.write(result.stderr);
|
|
56
202
|
return {
|
|
@@ -59,7 +205,8 @@ function runStep(executable, target, artifactsDir, id, args, { json }) {
|
|
|
59
205
|
status: "fail",
|
|
60
206
|
exitCode,
|
|
61
207
|
reportPath: report,
|
|
62
|
-
reason: reportedError(result.stdout) ?? `exited ${exitCode}
|
|
208
|
+
reason: reportedError(result.stdout) ?? `exited ${exitCode}`,
|
|
209
|
+
durationMs
|
|
63
210
|
};
|
|
64
211
|
}
|
|
65
212
|
function readDevices(artifactsDir) {
|
|
@@ -172,41 +319,56 @@ async function handlePrepare(argv) {
|
|
|
172
319
|
Next: ${USAGE}`);
|
|
173
320
|
}
|
|
174
321
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
322
|
+
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
323
|
+
const chain = stepChain(platform);
|
|
324
|
+
const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
|
|
325
|
+
view?.begin();
|
|
175
326
|
const steps = [];
|
|
176
327
|
const failed = () => steps.some((step) => step.status === "fail");
|
|
177
328
|
const addStep = (step) => {
|
|
178
329
|
steps.push(step);
|
|
179
|
-
|
|
330
|
+
view?.finish(step);
|
|
180
331
|
return step;
|
|
181
332
|
};
|
|
182
|
-
const run = (id, args) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
333
|
+
const run = async (id, args) => {
|
|
334
|
+
if (failed()) {
|
|
335
|
+
return addStep(skippedStep(id, [executable, ...args].join(" "), SKIPPED_AFTER_FAILURE));
|
|
336
|
+
}
|
|
337
|
+
const step = await runStep(executable, target, artifactsDir, id, args, {
|
|
338
|
+
json,
|
|
339
|
+
onSpawn: (startedAt) => view?.start(id, startedAt)
|
|
340
|
+
});
|
|
341
|
+
return addStep(step);
|
|
342
|
+
};
|
|
343
|
+
try {
|
|
344
|
+
await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
345
|
+
await run("status", ["status", "--json", ...targetArgs]);
|
|
346
|
+
if (platform === "mobile" && !mobilePlatform && !failed()) {
|
|
347
|
+
mobilePlatform = mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(readDevices(artifactsDir));
|
|
348
|
+
if (!mobilePlatform) return ambiguousMobileTarget(json);
|
|
349
|
+
}
|
|
350
|
+
if (platform === "core") {
|
|
351
|
+
const reason = "core is headless: no app surface to launch or seed";
|
|
352
|
+
addStep(skippedStep("launch", `${executable} launch`, reason));
|
|
353
|
+
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
354
|
+
} else if (platform === "mobile") {
|
|
355
|
+
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
356
|
+
await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], "--verify", "--json", ...targetArgs]);
|
|
357
|
+
await run("fixtures", [
|
|
358
|
+
"fixtures",
|
|
359
|
+
"set",
|
|
360
|
+
...mobilePlatform ? ["--platform", mobilePlatform] : [],
|
|
361
|
+
"--json",
|
|
362
|
+
...targetArgs
|
|
363
|
+
]);
|
|
364
|
+
} else {
|
|
365
|
+
await run("launch", ["launch", "--verify", "--json", ...targetArgs]);
|
|
366
|
+
await run("fixtures", ["fixtures", "set", "--json", ...targetArgs]);
|
|
367
|
+
}
|
|
368
|
+
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
369
|
+
} finally {
|
|
370
|
+
view?.end();
|
|
208
371
|
}
|
|
209
|
-
run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
210
372
|
const provenance = runnerProvenance(manifestPath(platform));
|
|
211
373
|
const record = {
|
|
212
374
|
schemaVersion: 1,
|
|
@@ -222,7 +384,6 @@ async function handlePrepare(argv) {
|
|
|
222
384
|
ready: steps.every((step) => step.status !== "fail"),
|
|
223
385
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
224
386
|
};
|
|
225
|
-
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
226
387
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
227
388
|
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
|
|
228
389
|
`);
|
package/dist/commands/update.js
CHANGED
|
@@ -206,7 +206,7 @@ function updateStatus(now = Date.now()) {
|
|
|
206
206
|
if (nudgeDisabled()) return { current, checked: false };
|
|
207
207
|
const file = cacheFile();
|
|
208
208
|
const cache = readCache(file);
|
|
209
|
-
const latest = cache?.latest && now - cache.lastCheck < DOCTOR_CACHE_FRESH_MS ? cache.latest : probeLatest(file, now);
|
|
209
|
+
const latest = cache?.latest && now - cache.lastCheck < DOCTOR_CACHE_FRESH_MS && !isNewer(current, cache.latest) ? cache.latest : probeLatest(file, now);
|
|
210
210
|
if (!latest) return { current, checked: false };
|
|
211
211
|
return { current, checked: true, latest, updateAvailable: isNewer(latest, current) };
|
|
212
212
|
}
|