@deeeed/metamask-harness 0.92.22 → 0.92.23
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 +37 -0
- package/dist/adapters/core/surface.js +2 -3
- package/dist/adapters/extension/surface.js +1 -2
- package/dist/adapters/mobile/devices.js +0 -1
- package/dist/adapters/mobile/overlay.js +3 -2
- package/dist/adapters/mobile/prepare-target.js +13 -0
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/commands/doctor.js +3 -423
- package/dist/commands/review.js +5 -66
- package/dist/doctor.js +0 -11
- package/dist/mm-harness-cli.js +50 -20
- package/dist/paths.js +0 -2
- package/dist/provider-doctor.js +3 -1
- package/dist/provider-helpers.js +143 -1
- package/dist/provider-lifecycle.js +43 -1
- package/dist/review/knowledge.js +0 -119
- package/docs/CONTRIBUTING.md +3 -2
- package/package.json +3 -3
- package/dist/commands/checklist.js +0 -242
- package/dist/commands/domain.js +0 -57
- package/dist/commands/execution-template.js +0 -28
- package/dist/commands/logs.js +0 -99
- package/dist/commands/prepare.js +0 -433
- package/dist/commands/recipe-quality.js +0 -103
- package/dist/commands/status-watch.js +0 -317
- package/dist/commands/status.js +0 -146
- package/dist/commands/task-init.js +0 -49
- package/dist/task-view.js +0 -332
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.92.23 - 2026-10-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Pin `@farmslot/recipe-cli` 0.23.1 and `@farmslot/adapter-web` 0.10.15. A browser-extension launch no longer fails with `spawnSync ps ENOBUFS` on a host whose process list is over 1 MB.
|
|
10
|
+
- `task init` and `execution-template` now run through the shared Farmslot handlers. Their output is unchanged.
|
|
11
|
+
- `logs` now runs through the shared Farmslot handler:
|
|
12
|
+
- `--events` reaches the compact viewer as its argument; `logs` no longer sets `RECIPE_LOG_EVENTS` in its own environment.
|
|
13
|
+
- `--window` needs the dev-server log itself: when it is missing, `logs --window` exits 2 with `The primary log window is unavailable.` instead of opening a window on the missing file.
|
|
14
|
+
- `--full` runs `tail` as its own child, so stopping `mm-harness logs` also stops `tail`.
|
|
15
|
+
- A headless platform that declares logs is tailed; only one without logs is refused, naming that platform instead of always `core`.
|
|
16
|
+
- The "nothing running" error names the requested log only (on Extension it listed every log file).
|
|
17
|
+
- `--json` results include `target`.
|
|
18
|
+
- `recipe-quality` now runs through the shared Farmslot handler: `plan` and `advise` use the detected platform when `--adapter` is absent, instead of exiting 2.
|
|
19
|
+
- `checklist` now runs through the shared Farmslot handler:
|
|
20
|
+
- `mark` with `--checklist` or `--signal` checks the terminal signal and the check-diff gate against those files, not the defaults.
|
|
21
|
+
- `mark <task-dir> start` resumes a task whose signal is `blocked`.
|
|
22
|
+
- An explicit `--checklist` is used even when `checklist-target.json` is invalid.
|
|
23
|
+
- `status` now runs through the shared Farmslot handler. The dashboard is unchanged: it still saves an explicit `--device` choice and shows no command journal.
|
|
24
|
+
- `status --task` and `status --watch` JSON, and each line under `--watch --json`, end with a `context` field (target and slot).
|
|
25
|
+
- The task view no longer reports another checkout's runtime context (an inherited `RECIPE_RUNTIME_CONTEXT`, or one whose `repoRoot` is elsewhere) as this checkout's slot, device and ports.
|
|
26
|
+
- `doctor` now runs through the shared Farmslot handler. It still saves an explicit `--device` choice, and `--print-ready` still prints the Farmslot ready indicator.
|
|
27
|
+
- `--fix` prints one stderr progress line per repair stage, such as `[1/3] runtime-context: done, created, 0s` or `[3/3] host repair: failed, wallet-fixture, 0s`, with a heartbeat on a long stage. Stdout and the JSON are unchanged.
|
|
28
|
+
- A failing recorder without a next step no longer prints `Next: undefined`.
|
|
29
|
+
- Under `--expect-live --json`, a `RECORDER_UNAVAILABLE` error whose recorder gives no next step takes the runtime's next step as its `userAction`.
|
|
30
|
+
- `--fix` waits for a platform repair that returns a promise, and reports a rejected repair as failed instead of fixed.
|
|
31
|
+
- On a platform with both a recorder and live checks, the recorder check comes before the live checks in `checks` and `requiredChecks.failed`.
|
|
32
|
+
- `review` and `domain` now run through the shared Farmslot handler. They keep every library this machine resolves (`RECIPE_LIBRARY_PATH`, `mm-harness config`, sibling checkouts, the fetch cache), `MM_HARNESS_DOMAIN`, and the checklist header, steps and verdict. `help review` is unchanged.
|
|
33
|
+
- `domain` names where a declared domain came from: `domain: declared via --domain` or `domain: declared via MM_HARNESS_DOMAIN` (it printed `declared via --domain or MM_HARNESS_DOMAIN`).
|
|
34
|
+
- `domain --domain <name> --json` outside a review checkout reports `"adapter": "unscoped"`, as `review --json` does (it reported `null`).
|
|
35
|
+
- When `domain` classifies against an `owned-paths.json` with an empty `domain` or invalid globs for the reviewed platform, it exits 2 with `Invalid owned-paths.json: <file>` or `Invalid owned paths for <platform>: <file>` (it exited 1 with a different message). `review checklist --domain` no longer reads the library's `owned-paths.json`.
|
|
36
|
+
- Setup step 2 names `review/antipatterns.<platform>.md` when the library has only that file (it named `review/antipatterns.md`).
|
|
37
|
+
- `prepare` now runs through the shared Farmslot handler. Its steps, readiness record, refusals, Mobile device choice and exit codes are unchanged.
|
|
38
|
+
- Under `--json`, or when stdout or stderr is not a terminal, stderr gets a stage line per step (`[1/3] doctor --fix: started`, then `done` or `failed, <reason>`, with a heartbeat on a long step) and each step's own stage lines, prefixed with the step (`[1/3] doctor --fix › [1/3] runtime-context: done, created`). Stdout is unchanged.
|
|
39
|
+
- In a terminal, the running step's row shows its latest stage line instead of its hint.
|
|
40
|
+
- A platform plugin that declares `readiness.prepare` can be prepared.
|
|
41
|
+
|
|
5
42
|
## 0.92.22 - 2026-10-18
|
|
6
43
|
|
|
7
44
|
### Fixed
|
|
@@ -93,9 +93,8 @@ const coreSurface = defineAdapter({
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
-
readiness: coreReadiness,
|
|
97
|
-
fixtures: { operations: ["init", "sync", "set"] }
|
|
98
|
-
prepare: corePrepare
|
|
96
|
+
readiness: { ...coreReadiness, prepare: corePrepare },
|
|
97
|
+
fixtures: { operations: ["init", "sync", "set"] }
|
|
99
98
|
});
|
|
100
99
|
adapterRegistry.register(coreSurface);
|
|
101
100
|
export {
|
|
@@ -148,12 +148,11 @@ const extensionSurface = defineAdapter({
|
|
|
148
148
|
actions: extensionActions,
|
|
149
149
|
harness: extensionHarness,
|
|
150
150
|
runtimeContext: extensionRuntimeContext,
|
|
151
|
-
readiness: extensionReadiness,
|
|
151
|
+
readiness: { ...extensionReadiness, prepare: extensionPrepare },
|
|
152
152
|
fixtures: {
|
|
153
153
|
operations: ["init", "sync", "set", "reset", "generate", "finalize"],
|
|
154
154
|
apply: async (context) => (await import("./fixtures.js")).applyExtensionFixture(context)
|
|
155
155
|
},
|
|
156
|
-
prepare: extensionPrepare,
|
|
157
156
|
debug: {
|
|
158
157
|
refuse: ({ devMenu }) => devMenu ? { message: "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.", userAction: "mm-harness debug --worker" } : void 0,
|
|
159
158
|
args: ({ worker }) => [
|
|
@@ -17,7 +17,6 @@ const mobileDevices = {
|
|
|
17
17
|
renderList: renderMobileDeviceList,
|
|
18
18
|
renderAdditional: renderAdditionalReachableDevices,
|
|
19
19
|
renderLive: renderMobileLiveBlock,
|
|
20
|
-
renderPortHints: renderAndroidPortHints,
|
|
21
20
|
renderHints: (live, paint) => renderAndroidPortHints(live.androidPortHints, paint),
|
|
22
21
|
nextForLive
|
|
23
22
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, readRuntimeContextTarget } from "./prepare-target.js";
|
|
1
|
+
import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, mobileTargetAmbiguityError, readRuntimeContextTarget } from "./prepare-target.js";
|
|
2
2
|
const mobileHarness = {
|
|
3
3
|
install: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
|
|
4
4
|
cleanup: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh", fromInstalledRunner: true },
|
|
@@ -18,7 +18,8 @@ const mobileRuntimeContext = {
|
|
|
18
18
|
};
|
|
19
19
|
const mobilePrepare = {
|
|
20
20
|
clearMetro: true,
|
|
21
|
-
devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices)
|
|
21
|
+
devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices),
|
|
22
|
+
ambiguousTarget: mobileTargetAmbiguityError
|
|
22
23
|
};
|
|
23
24
|
export {
|
|
24
25
|
mobileHarness,
|
|
@@ -31,8 +31,21 @@ function mobilePlatformFromDevices(devices) {
|
|
|
31
31
|
];
|
|
32
32
|
return platforms.length === 1 ? platforms[0] : void 0;
|
|
33
33
|
}
|
|
34
|
+
function mobileTargetAmbiguityError() {
|
|
35
|
+
const choices = [
|
|
36
|
+
"1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
|
|
37
|
+
"2. Android mm-harness prepare --platform mobile --mobile-platform android"
|
|
38
|
+
];
|
|
39
|
+
return {
|
|
40
|
+
code: "PREPARE_MOBILE_TARGET_AMBIGUOUS",
|
|
41
|
+
message: "could not infer the mobile target from the connected devices",
|
|
42
|
+
userAction: `choose the mobile target, then rerun this command:
|
|
43
|
+
${choices.join("\n")}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
34
46
|
export {
|
|
35
47
|
mobilePlatformFromConfiguredTarget,
|
|
36
48
|
mobilePlatformFromDevices,
|
|
49
|
+
mobileTargetAmbiguityError,
|
|
37
50
|
readRuntimeContextTarget
|
|
38
51
|
};
|
|
@@ -107,12 +107,11 @@ const mobileSurface = defineAdapter({
|
|
|
107
107
|
actions: mobileActions,
|
|
108
108
|
harness: mobileHarness,
|
|
109
109
|
runtimeContext: mobileRuntimeContext,
|
|
110
|
-
readiness: mobileReadiness,
|
|
110
|
+
readiness: { ...mobileReadiness, prepare: mobilePrepare },
|
|
111
111
|
fixtures: {
|
|
112
112
|
operations: ["init", "sync", "set", "reset"],
|
|
113
113
|
apply: async (context) => (await import("./fixtures.js")).applyMobileFixture(context)
|
|
114
114
|
},
|
|
115
|
-
prepare: mobilePrepare,
|
|
116
115
|
debug: {
|
|
117
116
|
refuse: ({ worker }) => worker ? { message: "--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.", userAction: "mm-harness debug --dev-menu" } : void 0,
|
|
118
117
|
args: ({ devMenu }) => [
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,308 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
|
|
8
|
-
import { assertAdapter, runnerDir } from "../paths.js";
|
|
9
|
-
import { getAdapterSurface } from "../adapters/surface.js";
|
|
10
|
-
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
11
|
-
import { applyDeviceTargeting } from "./device-target.js";
|
|
12
|
-
import { farmslotReadyIndicator } from "./farmslot-ready.js";
|
|
13
|
-
import { updateStatus } from "./update.js";
|
|
14
|
-
import { ensureRuntimeContext } from "../runtime-context.js";
|
|
15
|
-
function applyDoctorRuntimePorts(options) {
|
|
16
|
-
const cdpPort = optionString(options, "cdpPort");
|
|
17
|
-
if (cdpPort) {
|
|
18
|
-
process.env.CDP_PORT = cdpPort;
|
|
19
|
-
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
20
|
-
}
|
|
21
|
-
const watcherPort = optionString(options, "watcherPort") ?? optionString(options, "port");
|
|
22
|
-
if (watcherPort) {
|
|
23
|
-
process.env.WATCHER_PORT = watcherPort;
|
|
24
|
-
process.env.METRO_PORT = watcherPort;
|
|
25
|
-
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async function handleDoctor({ options }) {
|
|
29
|
-
applyRuntimeDirOption(options);
|
|
30
|
-
const target = targetPath(options);
|
|
31
|
-
const json = optionFlag(options, "json");
|
|
32
|
-
const printReady = optionFlag(options, "printReady");
|
|
33
|
-
const expectLive = optionFlag(options, "expectLive") || printReady;
|
|
34
|
-
const allDevices = optionFlag(options, "allDevices");
|
|
35
|
-
const platformOption = optionString(options, "platform");
|
|
36
|
-
const adapter = contextAdapter(target);
|
|
37
|
-
if (!adapter) {
|
|
38
|
-
return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
|
|
39
|
-
}
|
|
40
|
-
assertAdapter(adapter);
|
|
41
|
-
const { readiness } = getAdapterSurface(adapter);
|
|
42
|
-
if (!fs.existsSync(target)) {
|
|
43
|
-
return usageOut(
|
|
44
|
-
json,
|
|
45
|
-
"doctor",
|
|
46
|
-
`target does not exist: ${target}`,
|
|
47
|
-
"pass --target <metamask-checkout> pointing to an existing checkout"
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (printReady && json) {
|
|
51
|
-
return usageOut(
|
|
52
|
-
json,
|
|
53
|
-
"doctor",
|
|
54
|
-
"--print-ready owns stdout for Farmslot health_check; drop --json (use --expect-live --json for the doctor envelope)",
|
|
55
|
-
"mm-harness doctor --print-ready --adapter <adapter> --target <path>"
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
59
|
-
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
60
|
-
const manifestValidation = await validateManifest(manifest);
|
|
61
|
-
if (optionFlag(options, "fix")) {
|
|
62
|
-
const surface = getAdapterSurface(adapter);
|
|
63
|
-
surface.resolveSlotPorts(target);
|
|
64
|
-
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
65
|
-
if ("code" in dtResult) {
|
|
66
|
-
return usageOut(json, "doctor", dtResult.message, dtResult.userAction);
|
|
67
|
-
}
|
|
68
|
-
applyDoctorRuntimePorts(options);
|
|
69
|
-
const lock = acquireCheckoutLock(target, "doctor-fix");
|
|
70
|
-
if ("message" in lock) {
|
|
71
|
-
return checkoutBusyOut(json, "doctor", lock.message, lock.path);
|
|
72
|
-
}
|
|
73
|
-
let fixed;
|
|
74
|
-
let failed;
|
|
75
|
-
try {
|
|
76
|
-
({ fixed, failed } = await runDoctorFix(adapter, target, manifestValidation, json));
|
|
77
|
-
} finally {
|
|
78
|
-
lock.release();
|
|
79
|
-
}
|
|
80
|
-
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
81
|
-
let runtime2;
|
|
82
|
-
let runtimeProbeError2;
|
|
83
|
-
try {
|
|
84
|
-
runtime2 = await surface.runtimeStatus(target);
|
|
85
|
-
} catch (error2) {
|
|
86
|
-
runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
|
|
87
|
-
}
|
|
88
|
-
const recorder2 = runtime2?.decision === "ready" ? await readiness.recorder?.() : void 0;
|
|
89
|
-
const ready2 = doctorReady(runtime2, recorder2);
|
|
90
|
-
const block = readiness.runtimeBlock?.(target, runtime2);
|
|
91
|
-
if (block) failed.push(block.id);
|
|
92
|
-
if (recorder2?.status === "fail") failed.push(recorder2.id);
|
|
93
|
-
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target, block);
|
|
94
|
-
if (recorder2?.status === "fail" && recorder2.userAction) nextActions.push(recorder2.userAction);
|
|
95
|
-
if (failed.length === 0 && runtime2?.decision !== "ready" && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
|
|
96
|
-
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
97
|
-
const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
98
|
-
const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
|
|
99
|
-
const error = status === "fail" ? {
|
|
100
|
-
code: "DOCTOR_FIX_INCOMPLETE",
|
|
101
|
-
message: "doctor --fix could not make every required check pass",
|
|
102
|
-
userAction: fixUserAction
|
|
103
|
-
} : void 0;
|
|
104
|
-
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready: ready2, runtime: runtime2, ...recorder2 ? { recorder: recorder2 } : {}, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
|
|
105
|
-
else {
|
|
106
|
-
console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready2} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
107
|
-
if (runtime2) {
|
|
108
|
-
console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
|
|
109
|
-
for (const reason of runtime2.reasons) console.log(` ${reason}`);
|
|
110
|
-
} else if (runtimeProbeError2) {
|
|
111
|
-
console.log(`runtime probe failed: ${runtimeProbeError2}`);
|
|
112
|
-
}
|
|
113
|
-
renderDoctorRecorder(recorder2);
|
|
114
|
-
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
115
|
-
}
|
|
116
|
-
return status === "pass" ? 0 : 1;
|
|
117
|
-
}
|
|
118
|
-
const result = createDoctorReport(
|
|
119
|
-
adapter,
|
|
120
|
-
target,
|
|
121
|
-
manifestValidation,
|
|
122
|
-
actionManifestPath,
|
|
123
|
-
platformOption
|
|
124
|
-
);
|
|
125
|
-
const view = collectCheckoutView(target, adapter);
|
|
126
|
-
const missingRequiredTool = result.checks.find(
|
|
127
|
-
(check) => check.required && check.status === "fail" && check.id.startsWith("device-tool-") && check.userAction
|
|
128
|
-
);
|
|
129
|
-
if (missingRequiredTool) {
|
|
130
|
-
const error = {
|
|
131
|
-
code: "DOCTOR_CHECKS_FAILED",
|
|
132
|
-
message: missingRequiredTool.message,
|
|
133
|
-
userAction: missingRequiredTool.userAction
|
|
134
|
-
};
|
|
135
|
-
if (json) {
|
|
136
|
-
console.log(JSON.stringify({ ...result, ready: false, error }, null, 2));
|
|
137
|
-
} else {
|
|
138
|
-
console.error(`\u2717 doctor: ${error.message}
|
|
139
|
-
Next: ${error.userAction}`);
|
|
140
|
-
}
|
|
141
|
-
return EXIT.runtime;
|
|
142
|
-
}
|
|
143
|
-
let runtime;
|
|
144
|
-
let runtimeProbeError;
|
|
145
|
-
try {
|
|
146
|
-
const surface = getAdapterSurface(adapter);
|
|
147
|
-
surface.resolveSlotPorts(target);
|
|
148
|
-
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
149
|
-
if ("code" in dtResult) {
|
|
150
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
|
|
151
|
-
else console.error(`\u2717 doctor: ${dtResult.message}
|
|
152
|
-
Next: ${dtResult.userAction}`);
|
|
153
|
-
return EXIT.usage;
|
|
154
|
-
}
|
|
155
|
-
applyDoctorRuntimePorts(options);
|
|
156
|
-
runtime = await surface.runtimeStatus(target);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
runtimeProbeError = error instanceof Error ? error.message : String(error);
|
|
159
|
-
}
|
|
160
|
-
const runtimeCheck = doctorRuntimeCheck(runtime, runtimeProbeError, expectLive);
|
|
161
|
-
const recorder = runtime?.decision === "ready" ? await readiness.recorder?.() : void 0;
|
|
162
|
-
const checks = [
|
|
163
|
-
...result.checks,
|
|
164
|
-
runtimeCheck,
|
|
165
|
-
...await readiness.liveChecks?.(target) ?? [],
|
|
166
|
-
...recorder ? [recorder] : [],
|
|
167
|
-
...await platformDoctorChecks(adapter, target),
|
|
168
|
-
// Which library plugin code this process loaded: library, module and digest.
|
|
169
|
-
...adapterPluginChecks()
|
|
170
|
-
];
|
|
171
|
-
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
172
|
-
const doctorResult = {
|
|
173
|
-
...result,
|
|
174
|
-
status: requiredChecks.status,
|
|
175
|
-
checks,
|
|
176
|
-
requiredChecks
|
|
177
|
-
};
|
|
178
|
-
const orphanMetros = readiness.orphanDevServers?.(target, process.env.WATCHER_PORT) ?? [];
|
|
179
|
-
const capture = captureHealth(readiness.captureProviders);
|
|
180
|
-
const deviceSource = await readiness.devices?.();
|
|
181
|
-
const deviceView = deviceSource ? deviceSource.view(allDevices) : null;
|
|
182
|
-
const liveView = deviceSource && deviceView ? await deviceSource.live(target, deviceView) : null;
|
|
183
|
-
const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
|
|
184
|
-
const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
|
|
185
|
-
const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
|
|
186
|
-
if (expectLive) {
|
|
187
|
-
return emitExpectLive(
|
|
188
|
-
adapter,
|
|
189
|
-
target,
|
|
190
|
-
runtime,
|
|
191
|
-
recorder,
|
|
192
|
-
doctorResult,
|
|
193
|
-
orphanMetros,
|
|
194
|
-
capture,
|
|
195
|
-
devices,
|
|
196
|
-
additionalReachableDevices,
|
|
197
|
-
json,
|
|
198
|
-
printReady,
|
|
199
|
-
discovery
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
const skills = skillsView(target);
|
|
203
|
-
const update = updateStatus();
|
|
204
|
-
const pinnedFlags = readiness.pinnedFlags?.(target) ?? null;
|
|
205
|
-
const featureFlags = pinnedFlags ? featureFlagReport(pinnedFlags.overrides, pinnedFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
|
|
206
|
-
const doctorUserAction = doctorResult.checks.find(
|
|
207
|
-
(check) => check.required && check.status === "fail" && check.userAction
|
|
208
|
-
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
209
|
-
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
210
|
-
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
211
|
-
const ready = doctorReady(runtime, recorder);
|
|
212
|
-
if (json) console.log(JSON.stringify({ ...doctorResult, ready, runtime, ...recorder ? { recorder } : {}, view, orphanMetros, capture, skills, update, featureFlags, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
213
|
-
else {
|
|
214
|
-
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
215
|
-
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
216
|
-
console.log(`${out(doctorResult.status === "pass" ? "ok" : "err", doctorResult.status)} ${out("bold", adapter)} ${doctorResult.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
|
|
217
|
-
console.log(
|
|
218
|
-
`${out("label", "harness:")} ${doctorResult.runner.packageName}@${doctorResult.runner.version} ${out("accent", doctorResult.runner.installKind)} ${out("dim", `executable=${doctorResult.runner.executablePath}`)}`
|
|
219
|
-
);
|
|
220
|
-
if (runtime) {
|
|
221
|
-
const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
|
|
222
|
-
const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
|
|
223
|
-
const depsStyle = provisionedDepsPending ? "info" : stateStyle(runtime.deps, "current");
|
|
224
|
-
const devServerStyle = provisionedDepsPending ? "info" : void 0;
|
|
225
|
-
const devServer = runtime.devServer ? ` ${runtime.devServer.label}=${out(devServerStyle ?? stateStyle(runtime.devServer.status, "up"), runtime.devServer.status)}` : "";
|
|
226
|
-
console.log(
|
|
227
|
-
`${out("label", "runtime:")} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out("dim", `(${runtime.reasonCode})`)}` : ""} deps=${out(depsStyle, runtime.deps ?? "unknown")}${devServer}`
|
|
228
|
-
);
|
|
229
|
-
for (const reason of runtime.reasons) console.log(` ${out("dim", reason)}`);
|
|
230
|
-
} else if (runtimeProbeError) {
|
|
231
|
-
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
232
|
-
}
|
|
233
|
-
renderDoctorRecorder(recorder, out);
|
|
234
|
-
console.log(renderCheckoutView(view));
|
|
235
|
-
console.log(`${out("label", "feature flags:")} ${out(
|
|
236
|
-
featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
|
|
237
|
-
featureFlags.summary.replace(/^feature flags: /u, "")
|
|
238
|
-
)}`);
|
|
239
|
-
for (const line of readiness.lines?.(target) ?? []) console.log(line);
|
|
240
|
-
if (orphanMetros.length > 0) {
|
|
241
|
-
console.log(
|
|
242
|
-
`${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
|
|
243
|
-
);
|
|
244
|
-
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
245
|
-
}
|
|
246
|
-
if (capture) renderCaptureHealth(capture, out);
|
|
247
|
-
renderSkillsView(skills, out);
|
|
248
|
-
renderUpdateStatus(update, out);
|
|
249
|
-
if (deviceSource) {
|
|
250
|
-
if (deviceView) deviceSource.renderList(deviceView, out);
|
|
251
|
-
if (additionalReachableDevices.length > 0) deviceSource.renderAdditional(additionalReachableDevices, out);
|
|
252
|
-
if (liveView) deviceSource.renderLive(deviceView?.devices ?? [], liveView.liveMap, out);
|
|
253
|
-
}
|
|
254
|
-
if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
|
|
255
|
-
}
|
|
256
|
-
return doctorResult.status === "pass" ? 0 : 1;
|
|
257
|
-
}
|
|
258
|
-
import { doctorReady as doctorReady2 } from "@farmslot/recipe-cli/harness";
|
|
259
|
-
async function platformDoctorChecks(adapter, target) {
|
|
260
|
-
try {
|
|
261
|
-
return await getAdapterSurface(adapter).doctor?.(target) ?? [];
|
|
262
|
-
} catch (error) {
|
|
263
|
-
return [{
|
|
264
|
-
id: `adapter-plugin:${adapter}:doctor`,
|
|
265
|
-
status: "fail",
|
|
266
|
-
required: true,
|
|
267
|
-
message: `${adapter}'s doctor checks failed to run.`,
|
|
268
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
269
|
-
userAction: `fix the doctor() of the ${adapter} adapter, then rerun mm-harness doctor --adapter ${adapter}`
|
|
270
|
-
}];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
function doctorRuntimeCheck(runtime, error, expectLive) {
|
|
274
|
-
if (error) {
|
|
275
|
-
return {
|
|
276
|
-
id: "runtime",
|
|
277
|
-
status: "fail",
|
|
278
|
-
required: true,
|
|
279
|
-
message: "Runtime readiness probe failed.",
|
|
280
|
-
detail: error
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
const ready = runtime?.decision === "ready";
|
|
284
|
-
return {
|
|
285
|
-
id: "runtime",
|
|
286
|
-
status: ready ? "pass" : "fail",
|
|
287
|
-
required: expectLive,
|
|
288
|
-
message: ready ? "Runtime is ready." : `Runtime is not ready${runtime?.reasonCode ? ` (${runtime.reasonCode})` : "."}`,
|
|
289
|
-
detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
function doctorFixNextActions(failed, fixture, adapter, target, block) {
|
|
293
|
-
const actions = [];
|
|
294
|
-
const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
|
|
295
|
-
const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
296
|
-
actions.push(...doctorFixtureNextActions(failed, fixture, scope, executable));
|
|
297
|
-
if (failed.includes("runtime-context")) {
|
|
298
|
-
actions.push(
|
|
299
|
-
`inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
|
|
303
|
-
if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
|
|
304
|
-
return actions;
|
|
305
|
-
}
|
|
3
|
+
import { shellQuote, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
4
|
+
import { fixtureSummary } from "../doctor.js";
|
|
5
|
+
import { runnerDir } from "../paths.js";
|
|
306
6
|
function doctorFixtureNextActions(failed, fixture, scope, executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness") {
|
|
307
7
|
const actions = [];
|
|
308
8
|
if (failed.includes("wallet-fixture")) {
|
|
@@ -312,91 +12,6 @@ function doctorFixtureNextActions(failed, fixture, scope, executable = process.e
|
|
|
312
12
|
}
|
|
313
13
|
return actions;
|
|
314
14
|
}
|
|
315
|
-
function emitExpectLiveVerdict(adapter, target, runtime, verdict, recorder) {
|
|
316
|
-
const out = (style, text) => color(style, text, { stream: process.stderr });
|
|
317
|
-
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
318
|
-
console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
319
|
-
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
320
|
-
if (recorder?.status === "fail") {
|
|
321
|
-
console.error(` ${out("err", "recorder:")} ${recorder.message}${recorder.detail ? ` ${out("dim", recorder.detail)}` : ""}`);
|
|
322
|
-
console.error(` Next: ${recorder.userAction}`);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
|
|
326
|
-
}
|
|
327
|
-
function emitExpectLive(adapter, target, runtime, recorder, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
|
|
328
|
-
const live = runtime?.decision === "ready";
|
|
329
|
-
const ready = doctorReady(runtime, recorder);
|
|
330
|
-
if (printReady) {
|
|
331
|
-
if (ready) {
|
|
332
|
-
const indicator = farmslotReadyIndicator(adapter, devices);
|
|
333
|
-
if (indicator) {
|
|
334
|
-
console.log(indicator);
|
|
335
|
-
return EXIT.ok;
|
|
336
|
-
}
|
|
337
|
-
emitExpectLiveVerdict(adapter, target, runtime, "not-ready");
|
|
338
|
-
return EXIT.runtime;
|
|
339
|
-
}
|
|
340
|
-
emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
|
|
341
|
-
return EXIT.runtime;
|
|
342
|
-
}
|
|
343
|
-
if (json) {
|
|
344
|
-
const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
|
|
345
|
-
const error = !live ? { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction } : !ready ? { code: "RECORDER_UNAVAILABLE", message: recorder.message, userAction: recorder.userAction } : void 0;
|
|
346
|
-
console.log(JSON.stringify({ ...result, ready, runtime, ...recorder ? { recorder } : {}, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
|
|
347
|
-
return ready ? EXIT.ok : EXIT.runtime;
|
|
348
|
-
}
|
|
349
|
-
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
350
|
-
if (ready) {
|
|
351
|
-
console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
|
|
352
|
-
return EXIT.ok;
|
|
353
|
-
}
|
|
354
|
-
emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
|
|
355
|
-
return EXIT.runtime;
|
|
356
|
-
}
|
|
357
|
-
function captureHealth(providers) {
|
|
358
|
-
if (!providers) return null;
|
|
359
|
-
const fallback = [...providers];
|
|
360
|
-
const helper = captureHelperHealth();
|
|
361
|
-
if (!helper) {
|
|
362
|
-
return {
|
|
363
|
-
status: "fallback",
|
|
364
|
-
failing: [],
|
|
365
|
-
screenshots: { available: true, providers: fallback },
|
|
366
|
-
recording: { available: false, provider: null }
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
return {
|
|
370
|
-
status: helper.status,
|
|
371
|
-
failing: helper.failing,
|
|
372
|
-
screenshots: { available: true, providers: ["capture-helper", ...fallback] },
|
|
373
|
-
recording: { available: helper.status === "pass", provider: helper.status === "pass" ? "capture-helper" : null }
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
function captureHelperHealth() {
|
|
377
|
-
if (process.platform !== "darwin") return null;
|
|
378
|
-
const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
379
|
-
try {
|
|
380
|
-
const out = execFileSync(bin, ["doctor", "--json"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
|
|
381
|
-
const parsed = JSON.parse(out);
|
|
382
|
-
const failing = (parsed.checks ?? []).filter((c) => c.required === true && c.ok === false).map((c) => c.name ?? c.id ?? "check");
|
|
383
|
-
return { status: parsed.ok === true && failing.length === 0 ? "pass" : "warn", failing };
|
|
384
|
-
} catch {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function renderCaptureHealth(capture, out) {
|
|
389
|
-
const video = capture.recording.available ? "capture-helper" : "unavailable";
|
|
390
|
-
console.log(
|
|
391
|
-
`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", `(screenshots: ${capture.screenshots.providers.join(" \u2192 ")}; video: ${video})`)}`
|
|
392
|
-
);
|
|
393
|
-
if (capture.status === "warn") {
|
|
394
|
-
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
395
|
-
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
396
|
-
} else if (capture.status === "fallback") {
|
|
397
|
-
console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
15
|
const SKILL_ROOTS = [path.join(".claude", "skills"), path.join(".agents", "skills")];
|
|
401
16
|
function skillsView(target) {
|
|
402
17
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -432,38 +47,6 @@ function renderUpdateStatus(status, out) {
|
|
|
432
47
|
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`)}`
|
|
433
48
|
);
|
|
434
49
|
}
|
|
435
|
-
async function runDoctorFix(adapter, target, manifestValidation, json) {
|
|
436
|
-
const fixed = [];
|
|
437
|
-
const failed = [];
|
|
438
|
-
if (recipeRunning(target)) {
|
|
439
|
-
failed.push("recipe-running");
|
|
440
|
-
return { fixed, failed };
|
|
441
|
-
}
|
|
442
|
-
if (Number(manifestValidation.summary?.errors ?? 0) > 0) failed.push("manifest");
|
|
443
|
-
try {
|
|
444
|
-
const surface = getAdapterSurface(adapter);
|
|
445
|
-
surface.resolveSlotPorts(target);
|
|
446
|
-
const runtimeContext = ensureRuntimeContext(target, adapter);
|
|
447
|
-
if (runtimeContext.created) fixed.push("runtime-context");
|
|
448
|
-
} catch {
|
|
449
|
-
failed.push("runtime-context");
|
|
450
|
-
}
|
|
451
|
-
for (const fix of getAdapterSurface(adapter).readiness.fixes ?? []) {
|
|
452
|
-
try {
|
|
453
|
-
if (fix.apply(target)) fixed.push(fix.id);
|
|
454
|
-
} catch {
|
|
455
|
-
failed.push(fix.id);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
const state = newHealState();
|
|
459
|
-
const ensured = await ensureOverlay(adapter, target, "infra-only", state, json);
|
|
460
|
-
if (!ensured.ok) failed.push("overlay");
|
|
461
|
-
else if (state.mutations.length > 0) fixed.push("overlay");
|
|
462
|
-
const fixture = await repairDoctorFixture(target, adapter, json);
|
|
463
|
-
fixed.push(...fixture.fixed);
|
|
464
|
-
failed.push(...fixture.failed);
|
|
465
|
-
return { fixed, failed };
|
|
466
|
-
}
|
|
467
50
|
async function repairDoctorFixture(target, adapter, json) {
|
|
468
51
|
if (fixtureSummary(target, adapter).status === "ready") return { fixed: [], failed: [] };
|
|
469
52
|
const syncFixture = path.join(runnerDir, "adapters/shared/sync-wallet-fixture.sh");
|
|
@@ -474,10 +57,7 @@ async function repairDoctorFixture(target, adapter, json) {
|
|
|
474
57
|
return fixtureSummary(target, adapter).status === "ready" ? { fixed: ["wallet-fixture"], failed: [] } : { fixed: [], failed: ["wallet-fixture"] };
|
|
475
58
|
}
|
|
476
59
|
export {
|
|
477
|
-
doctorFixNextActions,
|
|
478
60
|
doctorFixtureNextActions,
|
|
479
|
-
doctorReady2 as doctorReady,
|
|
480
|
-
handleDoctor,
|
|
481
61
|
renderSkillsView,
|
|
482
62
|
renderUpdateStatus,
|
|
483
63
|
repairDoctorFixture,
|