@deeeed/metamask-harness 0.54.1 → 0.55.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
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.55.0 - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `doctor` reports two more setup answers, also carried in its `--json`: `update` says whether this harness is the published latest (`0.54.1 is latest`, `0.54.1 → 0.55.0 available`, or `not checked` when the registry is unreachable or `MM_HARNESS_NO_UPDATE_CHECK=1`), and `skills` counts the `mms-*` skill directories the checkout has installed (`none installed` when there are none). Both are advisory — neither joins `requiredChecks`, changes `status`/`ready`, or can fail doctor, because the harness does its job without either. The registry check answers from the existing update cache, so it costs nothing on a checkout that already looked today. A setup skill now reads one verdict from `doctor` instead of running checks of its own; which skill names are expected or retired stays with the installer and that skill, so the harness carries no list of them.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The checkout view's fixture block is labelled `last fixtures set:`, because it reports the result of the last `fixtures set` this checkout ran, not the state of the wallet fixture file — which `doctor` already reports as `fixture.status`.
|
|
14
|
+
- The mobile deps note — raised when the manifests are newer than the install markers and no baseline is recorded — says what was observed, why it is treated as fine, and the one case that calls for action, with the install command it already teaches for a blocked preflight. It is set off from the command's own output by a blank line and a `mm-harness note (deps):` label, so a reader can tell the harness is talking.
|
|
15
|
+
|
|
5
16
|
## 0.54.1 - 2026-09-18
|
|
6
17
|
|
|
7
18
|
### Changed
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
supersededErrorCapture
|
|
12
12
|
} from "@farmslot/recipe-harness/runtime/log-analysis";
|
|
13
13
|
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
14
|
+
import { color } from "../../cli-color.js";
|
|
14
15
|
import { recipeRuntimePath } from "../../paths.js";
|
|
15
16
|
import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
16
17
|
import { mobileProductMarkers } from "./deps-markers.js";
|
|
@@ -147,14 +148,16 @@ const launchActions = (target, clearMetro = false) => {
|
|
|
147
148
|
actions.push({ id: "wait-for-bridge", cwd: target });
|
|
148
149
|
return actions;
|
|
149
150
|
};
|
|
151
|
+
function depsInstallCommand(resolved) {
|
|
152
|
+
return fs.existsSync(path.join(resolved, "node_modules")) ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
|
|
153
|
+
}
|
|
150
154
|
async function decideMobileReadiness(target, options = {}) {
|
|
151
155
|
const resolved = path.resolve(target);
|
|
152
156
|
if (options.record) recordDepsBaseline(resolved);
|
|
153
157
|
const fast = options.preflightMode === "fast";
|
|
154
158
|
const report = await computeMobileReadiness(resolved, options, fast);
|
|
155
159
|
if (fast && report.decision === "install") {
|
|
156
|
-
const
|
|
157
|
-
const standalone = hasNodeModules ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
|
|
160
|
+
const standalone = depsInstallCommand(resolved);
|
|
158
161
|
return {
|
|
159
162
|
...report,
|
|
160
163
|
decision: "blocked",
|
|
@@ -184,6 +187,15 @@ async function decideMobileReadiness(target, options = {}) {
|
|
|
184
187
|
actions: report.decision === "ready" ? launchActions(resolved) : report.actions
|
|
185
188
|
};
|
|
186
189
|
}
|
|
190
|
+
function writeDepsNote(resolved) {
|
|
191
|
+
const stream = process.stderr;
|
|
192
|
+
const body = `package.json/yarn.lock are newer than the node_modules install markers and no install baseline is recorded yet, so the installed node_modules are used as they are. Pulling commits refreshes those timestamps without changing dependencies, and a required-dependency presence check still runs, so this on its own is not evidence that anything drifted. Act only if you know dependencies changed: run ${depsInstallCommand(resolved)} once \u2014 that refreshes the markers, and \`mm-harness prepare\` records a baseline when it runs the install, so this note stops.`;
|
|
193
|
+
stream.write(
|
|
194
|
+
`
|
|
195
|
+
${color("warn", "mm-harness note (deps):", { stream })} ${color("dim", body, { stream })}
|
|
196
|
+
`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
187
199
|
async function computeMobileReadiness(resolved, options, fast) {
|
|
188
200
|
const rawDeps = depsCheck(resolved, {
|
|
189
201
|
productMarkers: mobileProductMarkers(options.platform)
|
|
@@ -191,9 +203,7 @@ async function computeMobileReadiness(resolved, options, fast) {
|
|
|
191
203
|
let deps = rawDeps;
|
|
192
204
|
if (rawDeps.status === "stale" && !rawDeps.hasBaseline) {
|
|
193
205
|
deps = { installed: rawDeps.installed, status: "current", hasBaseline: false };
|
|
194
|
-
|
|
195
|
-
"[runtime-decision] manifest is newer than install markers (no recorded baseline); trusting installed node_modules \u2014 run yarn setup manually if deps truly changed.\n"
|
|
196
|
-
);
|
|
206
|
+
writeDepsNote(resolved);
|
|
197
207
|
}
|
|
198
208
|
if (deps.status === "current") {
|
|
199
209
|
const absent = missingRequiredDeps(resolved);
|
package/dist/checkout-view.js
CHANGED
|
@@ -47,7 +47,7 @@ function renderCheckoutView(view) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
if (view.fixtures) {
|
|
50
|
-
lines.push(`${out("label", "fixtures:")}`);
|
|
50
|
+
lines.push(`${out("label", "last fixtures set:")}`);
|
|
51
51
|
for (const [key, value] of Object.entries(view.fixtures)) {
|
|
52
52
|
const isSet = value !== null && value !== "";
|
|
53
53
|
lines.push(` ${key.padEnd(22)} ${isSet ? out("ok", String(value)) : out("dim", "(unset)")}`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
renderMobileLiveBlock
|
|
22
22
|
} from "./mobile-device-view.js";
|
|
23
23
|
import { farmslotReadyIndicator } from "./farmslot-ready.js";
|
|
24
|
+
import { updateStatus } from "./update.js";
|
|
24
25
|
import { ensureRuntimeContext } from "../runtime-context.js";
|
|
25
26
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
26
27
|
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
@@ -206,12 +207,14 @@ async function handleDoctor({ options }) {
|
|
|
206
207
|
discovery
|
|
207
208
|
);
|
|
208
209
|
}
|
|
210
|
+
const skills = skillsView(target);
|
|
211
|
+
const update = updateStatus();
|
|
209
212
|
const doctorUserAction = doctorResult.checks.find(
|
|
210
213
|
(check) => check.required && check.status === "fail" && check.userAction
|
|
211
214
|
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
212
215
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
213
216
|
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
214
|
-
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
217
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, skills, update, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
215
218
|
else {
|
|
216
219
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
217
220
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -246,6 +249,8 @@ async function handleDoctor({ options }) {
|
|
|
246
249
|
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
247
250
|
}
|
|
248
251
|
if (capture) renderCaptureHealth(capture, out);
|
|
252
|
+
renderSkillsView(skills, out);
|
|
253
|
+
renderUpdateStatus(update, out);
|
|
249
254
|
if (adapter === "mobile") {
|
|
250
255
|
if (deviceView) renderMobileDeviceList(deviceView, out);
|
|
251
256
|
if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
|
|
@@ -374,6 +379,41 @@ function renderCaptureHealth(capture, out) {
|
|
|
374
379
|
console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
|
|
375
380
|
}
|
|
376
381
|
}
|
|
382
|
+
const SKILL_ROOTS = [path.join(".claude", "skills"), path.join(".agents", "skills")];
|
|
383
|
+
function skillsView(target) {
|
|
384
|
+
const names = /* @__PURE__ */ new Set();
|
|
385
|
+
let dir = null;
|
|
386
|
+
for (const root of SKILL_ROOTS) {
|
|
387
|
+
const absolute = path.join(target, root);
|
|
388
|
+
let entries;
|
|
389
|
+
try {
|
|
390
|
+
entries = fs.readdirSync(absolute, { withFileTypes: true });
|
|
391
|
+
} catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
dir ??= root;
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (entry.isDirectory() && entry.name.startsWith("mms-")) names.add(entry.name);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { dir, installed: [...names].sort() };
|
|
400
|
+
}
|
|
401
|
+
function renderSkillsView(skills, out) {
|
|
402
|
+
if (skills.installed.length === 0) {
|
|
403
|
+
console.log(`${out("label", "skills:")} ${out("warn", "none installed")} ${out("dim", "(yarn skills)")}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
console.log(`${out("label", "skills:")} ${skills.installed.length} installed ${out("dim", skills.dir ?? "")}`);
|
|
407
|
+
}
|
|
408
|
+
function renderUpdateStatus(status, out) {
|
|
409
|
+
if (!status.checked) {
|
|
410
|
+
console.log(`${out("label", "update:")} ${out("dim", "not checked")}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log(
|
|
414
|
+
status.updateAvailable ? `${out("label", "update:")} ${out("warn", `${status.current} \u2192 ${status.latest} available`)} ${out("dim", "(mm-harness update)")}` : status.latest && status.latest !== status.current ? `${out("label", "update:")} ${out("ok", status.current)} ${out("dim", `ahead of registry ${status.latest}`)}` : `${out("label", "update:")} ${out("ok", `${status.current} is latest`)}`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
377
417
|
function detectOrphanMetros(target, livePort) {
|
|
378
418
|
try {
|
|
379
419
|
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
@@ -493,5 +533,6 @@ function readTextSnapshot(file) {
|
|
|
493
533
|
export {
|
|
494
534
|
doctorFixNextActions,
|
|
495
535
|
ensureExtensionPerpsBuildFlag,
|
|
496
|
-
handleDoctor
|
|
536
|
+
handleDoctor,
|
|
537
|
+
skillsView
|
|
497
538
|
};
|
package/dist/commands/update.js
CHANGED
|
@@ -199,6 +199,23 @@ function nudgeDisabled() {
|
|
|
199
199
|
function nudgeLine(current, latest) {
|
|
200
200
|
return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
|
|
201
201
|
}
|
|
202
|
+
const DOCTOR_FETCH_TIMEOUT_MS = 3e3;
|
|
203
|
+
const DOCTOR_CACHE_FRESH_MS = 60 * 60 * 1e3;
|
|
204
|
+
function updateStatus(now = Date.now()) {
|
|
205
|
+
const current = currentVersion();
|
|
206
|
+
if (nudgeDisabled()) return { current, checked: false };
|
|
207
|
+
const file = cacheFile();
|
|
208
|
+
const cache = readCache(file);
|
|
209
|
+
const latest = cache?.latest && now - cache.lastCheck < DOCTOR_CACHE_FRESH_MS ? cache.latest : probeLatest(file, now);
|
|
210
|
+
if (!latest) return { current, checked: false };
|
|
211
|
+
return { current, checked: true, latest, updateAvailable: isNewer(latest, current) };
|
|
212
|
+
}
|
|
213
|
+
function probeLatest(file, now) {
|
|
214
|
+
const fetched = fetchLatest(DOCTOR_FETCH_TIMEOUT_MS);
|
|
215
|
+
if (!fetched.latest) return null;
|
|
216
|
+
writeCache(file, { lastCheck: now, latest: fetched.latest });
|
|
217
|
+
return fetched.latest;
|
|
218
|
+
}
|
|
202
219
|
function maybeNudge(now = Date.now()) {
|
|
203
220
|
if (nudgeDisabled()) return;
|
|
204
221
|
const file = cacheFile();
|
|
@@ -233,5 +250,6 @@ export {
|
|
|
233
250
|
maybeNudge,
|
|
234
251
|
nudgeDisabled,
|
|
235
252
|
nudgeLine,
|
|
236
|
-
registryCwd
|
|
253
|
+
registryCwd,
|
|
254
|
+
updateStatus
|
|
237
255
|
};
|
package/package.json
CHANGED
|
@@ -369,7 +369,7 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
369
369
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
370
370
|
return window.__siteCopied;
|
|
371
371
|
}
|
|
372
|
-
const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [] };
|
|
372
|
+
const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [], text: document.body.innerText };
|
|
373
373
|
for (const tab of document.querySelectorAll('[role="tab"]')) {
|
|
374
374
|
tab.click();
|
|
375
375
|
const panel = document.getElementById(tab.getAttribute('aria-controls'));
|
|
@@ -389,14 +389,15 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
389
389
|
// Setting up installs the harness and the skills; the work prompts invoke
|
|
390
390
|
// them by their exact installed names.
|
|
391
391
|
const expected = [
|
|
392
|
-
['
|
|
393
|
-
['
|
|
392
|
+
['/mms-recipe-setup', copied.setup],
|
|
393
|
+
['agentic/recipe-setup', copied.text],
|
|
394
394
|
['/mms-recipe-qa', copied.work.join('\n')],
|
|
395
|
-
['/mms-recipe-
|
|
395
|
+
['/mms-recipe-fix-bug', copied.work.join('\n')],
|
|
396
|
+
['/mms-recipe-dev', copied.work.join('\n')],
|
|
396
397
|
['/mms-recipe-performance', copied.work.join('\n')],
|
|
397
398
|
['/mms-recipe-pretty-pr', copied.work.join('\n')],
|
|
398
|
-
['perps/recipe-performance', copied.
|
|
399
|
-
['agentic/recipe-pretty-pr', copied.
|
|
399
|
+
['perps/recipe-performance', copied.text],
|
|
400
|
+
['agentic/recipe-pretty-pr', copied.text],
|
|
400
401
|
];
|
|
401
402
|
const missing = expected.filter(([line, text]) => !text?.includes(line)).map(([line]) => line);
|
|
402
403
|
if (missing.length) {
|
|
@@ -448,7 +449,7 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
448
449
|
return checked;
|
|
449
450
|
})()
|
|
450
451
|
`);
|
|
451
|
-
if (!previews.includes('review-pr/static-perps.mobile') || !previews.includes('qa/
|
|
452
|
+
if (!previews.includes('review-pr/static-perps.mobile') || !previews.includes('qa/default')) {
|
|
452
453
|
throw new Error('Static review and QA previews must both be available');
|
|
453
454
|
}
|
|
454
455
|
const progress = await page.evaluate(String.raw`
|