@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 hasNodeModules = fs.existsSync(path.join(resolved, "node_modules"));
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
- process.stderr.write(
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);
@@ -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)")}`);
@@ -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
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.54.1",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -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
- ['npm i -g @deeeed/metamask-harness@latest', copied.setup],
393
- ['yarn skills', copied.setup],
392
+ ['/mms-recipe-setup', copied.setup],
393
+ ['agentic/recipe-setup', copied.text],
394
394
  ['/mms-recipe-qa', copied.work.join('\n')],
395
- ['/mms-recipe-cook', copied.work.join('\n')],
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.setup],
399
- ['agentic/recipe-pretty-pr', copied.setup],
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/pr.extension')) {
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`