@deeeed/metamask-harness 0.89.0 → 0.91.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 +31 -0
- package/adapters/mobile/reload-app.mjs +10 -0
- package/dist/adapters/extension/reload.js +3 -3
- package/dist/adapters/extension/run.js +1 -6
- package/dist/adapters/mobile/android-screenrecord.js +28 -58
- package/dist/adapters/mobile/devices.js +2 -1
- package/dist/adapters/mobile/reload.js +3 -3
- package/dist/adapters/mobile/run.js +2 -3
- package/dist/checkout-view.js +10 -3
- package/dist/command-contract.js +2 -0
- package/dist/commands/check.js +18 -20
- package/dist/commands/device-target.js +1 -1
- package/dist/commands/doctor.js +31 -27
- package/dist/commands/logs.js +13 -30
- package/dist/doctor.js +26 -85
- package/dist/mm-harness-cli.js +1 -7
- package/dist/provider-doctor.js +36 -0
- package/dist/provider-lifecycle.js +26 -0
- package/dist/provider-logs.js +31 -0
- package/dist/provider.js +6 -1
- package/dist/runner.js +1 -0
- package/library/actions/core/perps/_controller.mjs +1 -19
- package/library/actions/extension/perps/perps.mjs +1 -11
- package/library/actions/extension/platform/cdp.mjs +3 -19
- package/library/actions/extension/ui/navigate.mjs +27 -2
- package/library/actions/mobile/perps/perps.mjs +1 -7
- package/library/actions/mobile/platform/observe-ui.mjs +45 -15
- package/library/actions/shared/perps/item-side.mjs +18 -0
- package/library/manifests/core.action-manifest.json +6 -1
- package/library/manifests/extension.action-manifest.json +8 -2
- package/library/manifests/mobile.action-manifest.json +13 -3
- package/library/manifests/web-dapp.action-manifest.json +38 -0
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +30 -8
- package/library/recipes/extension/performance/navigation-memory.recipe.json +23 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.91.0 - 2026-10-12
|
|
6
|
+
|
|
7
|
+
- Support Extension `ui.navigate page=settings` through the visible account menu and verify the Settings page before returning.
|
|
8
|
+
|
|
9
|
+
- Declare bounded `watch_logs.timeout_ms` for Core, Mobile, Extension and web-dapp, including Terminal inheritance. Use the supporting protocol 0.40.0 and runner 0.31.0 through coordinated runtime pins.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Breaking correction inherited from runner 0.31.0: run-scoped log observations require paths resolved before producers execute; output-derived paths must use `scope=file`. Nested output assertions and static JSON paths now receive complete preflight validation.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Run the declared Oxlint script for changed-source checks and explicit fixes instead of skipping lint when ESLint is absent.
|
|
18
|
+
|
|
19
|
+
- Mobile Perps position selectors now enforce long/short selection when the controller returns signed size without a side label. Core and Extension reuse the same side normalization while preserving their existing selection rules.
|
|
20
|
+
|
|
21
|
+
## 0.90.0 - 2026-10-12
|
|
22
|
+
|
|
23
|
+
- Use typed iOS simulator recording targets, retain original Android recording segments, and use the shared action-context snapshot API for Extension evidence.
|
|
24
|
+
|
|
25
|
+
- Share compact log viewing and owned-window recovery with the project recipe CLI, preserving event counts and cancellation.
|
|
26
|
+
|
|
27
|
+
- Preserve project doctor reports, setup advisories, explicit fixture repairs and wallet-aware health indicators through the shared provider host.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Pin the coordinated shared runtime releases: protocol 0.39.0, recipe-runner 0.30.0, adapter-sdk 0.9.0, agent-runtime 0.18.0, recipe-cli 0.14.1, adapter-node 0.1.13, adapter-rn 0.20.0 and adapter-web 0.7.0.
|
|
32
|
+
- Preserve checkout status details, resolved project library revisions and owned collector cleanup for shared project lifecycle commands; keep readiness device selection read-only and refuse Metro-wide reload when multiple devices are connected and forward interruption to owned reload processes.
|
|
33
|
+
- Collect opt-in rendered HUD observations around screenshot capture for live conformance.
|
|
34
|
+
- Bind navigation-memory suite claims to explicit assertions on the measured bounds.
|
|
35
|
+
|
|
5
36
|
## 0.89.0 - 2026-10-11
|
|
6
37
|
|
|
7
38
|
### Added
|
|
@@ -79,6 +79,16 @@ async function reload(port) {
|
|
|
79
79
|
`No React Native debug target with a stable ID is connected to Metro on port ${numericPort}`,
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
|
+
const response = await fetch(`http://127.0.0.1:${numericPort}/json/list`, {
|
|
83
|
+
signal: AbortSignal.timeout(5000),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) throw new Error(`Cannot inspect Metro clients on port ${numericPort}`);
|
|
86
|
+
const clients = rankRuntimeCandidates(await response.json());
|
|
87
|
+
const deviceIds = new Set(clients.map((client) => deviceIdFromUrl(client.webSocketDebuggerUrl)));
|
|
88
|
+
// Metro's message channel reloads every connected client, even when CDP is pinned.
|
|
89
|
+
if (deviceIds.size !== 1 || !deviceIds.has(deviceIdFromUrl(targetBefore.wsUrl))) {
|
|
90
|
+
throw new Error(`Metro reload needs one connected device on port ${numericPort}. Next: use a dedicated slot Metro or an explicit targeted app.lifecycle restart.`);
|
|
91
|
+
}
|
|
82
92
|
const processBefore = readAndroidPid();
|
|
83
93
|
if (androidSerial() && !processBefore) {
|
|
84
94
|
throw new Error(`The Android app is not running on ${androidSerial()}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { EXIT,
|
|
1
|
+
import { EXIT, spawnScriptStreaming, usageOut } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir } from "../../paths.js";
|
|
4
|
-
function reloadExtension(target, json) {
|
|
4
|
+
async function reloadExtension(target, json) {
|
|
5
5
|
const adapter = "extension";
|
|
6
6
|
if (!process.env.CDP_PORT) {
|
|
7
7
|
return usageOut(
|
|
@@ -16,7 +16,7 @@ function reloadExtension(target, json) {
|
|
|
16
16
|
if (process.env.WATCHER_PORT) {
|
|
17
17
|
args.push("--watcher-port", process.env.WATCHER_PORT);
|
|
18
18
|
}
|
|
19
|
-
const result =
|
|
19
|
+
const result = await spawnScriptStreaming(script, args, target, { forward: json ? "none" : "stderr" });
|
|
20
20
|
if (json) {
|
|
21
21
|
console.log(
|
|
22
22
|
JSON.stringify(
|
|
@@ -159,12 +159,7 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
159
159
|
return null;
|
|
160
160
|
}
|
|
161
161
|
const extensionRecording = {
|
|
162
|
-
target: (context) => resolveExtensionRecordingTarget(context)
|
|
163
|
-
framed: {
|
|
164
|
-
browserPid: (projectRoot, artifactsDir, cdpPort) => resolveExtensionBrowserPid(projectRoot, artifactsDir, cdpPort),
|
|
165
|
-
// cdp.mjs snapshots the framed recording of this pid.
|
|
166
|
-
activePidEnv: "METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID"
|
|
167
|
-
}
|
|
162
|
+
target: (context) => resolveExtensionRecordingTarget(context)
|
|
168
163
|
};
|
|
169
164
|
async function resolveExtensionRecordingTarget(context) {
|
|
170
165
|
const pid = resolveExtensionBrowserPid(
|
|
@@ -173,38 +173,38 @@ async function startRecording(serial, request, adb, options) {
|
|
|
173
173
|
}
|
|
174
174
|
if (recorded.length === 0) throw recordingFailure("RECORDING_FAILED", `Android recording output is empty: ${request.outputPath}`);
|
|
175
175
|
const startedAtUnixMs = recorded[0].startedAtUnixMs;
|
|
176
|
-
const stoppedAtUnixMs = recorded.at(-1).stoppedAtUnixMs;
|
|
177
176
|
const placed = recorded.map((segment) => ({ ...segment, offsetMs: segment.startedAtUnixMs - startedAtUnixMs }));
|
|
178
|
-
joinSegments(placed, request.outputPath);
|
|
179
177
|
const timingEvidencePath = `${path.basename(request.outputPath)}.segments.json`;
|
|
180
|
-
fs.writeFileSync(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})}
|
|
192
|
-
`
|
|
193
|
-
);
|
|
194
|
-
const timing = await optionalVideoTiming(request.outputPath, { startedAtUnixMs, stoppedAtUnixMs });
|
|
178
|
+
fs.writeFileSync(path.join(stagingDir, timingEvidencePath), `${JSON.stringify({
|
|
179
|
+
recorder: "adb-screenrecord",
|
|
180
|
+
serial,
|
|
181
|
+
segments: placed.map(({ offsetMs, startedAtUnixMs: startedAtUnixMs2, stoppedAtUnixMs, bytes }) => ({
|
|
182
|
+
offsetMs,
|
|
183
|
+
startedAtUnixMs: startedAtUnixMs2,
|
|
184
|
+
stoppedAtUnixMs,
|
|
185
|
+
bytes
|
|
186
|
+
}))
|
|
187
|
+
})}
|
|
188
|
+
`);
|
|
195
189
|
const recorder = { name: "adb-screenrecord", platform: "android", target: { selector: "adb-serial", value: serial } };
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
const originals = await Promise.all(recorded.map(async (segment, index) => ({
|
|
191
|
+
path: path.basename(segment.localPath),
|
|
192
|
+
...index > 0 ? { gapBefore: true } : {},
|
|
193
|
+
...await optionalVideoTiming(segment.localPath, {
|
|
194
|
+
startedAtUnixMs: segment.startedAtUnixMs,
|
|
195
|
+
stoppedAtUnixMs: segment.stoppedAtUnixMs
|
|
196
|
+
}),
|
|
198
197
|
timingEvidencePath,
|
|
199
|
-
recorder
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
recorder
|
|
199
|
+
})));
|
|
200
|
+
for (const segment of recorded) localFiles.delete(segment.localPath);
|
|
201
|
+
return {
|
|
202
|
+
segments: originals,
|
|
203
|
+
...interruption ? { interruption: {
|
|
204
|
+
frames: originals.reduce((sum, segment) => sum + (segment.timing?.framesMs.length ?? 0), 0),
|
|
205
|
+
mediaTimeMs: placed.at(-1).offsetMs + (originals.at(-1).timing?.framesMs.at(-1) ?? 0),
|
|
206
|
+
cause: interruption
|
|
207
|
+
} } : {}
|
|
208
208
|
};
|
|
209
209
|
} finally {
|
|
210
210
|
abandon();
|
|
@@ -213,36 +213,6 @@ async function startRecording(serial, request, adb, options) {
|
|
|
213
213
|
}
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
|
-
function latestVideoZero(timing, segments) {
|
|
217
|
-
return segments.reduce((latest, segment, index) => {
|
|
218
|
-
const to = segments[index + 1]?.offsetMs ?? Infinity;
|
|
219
|
-
const last = timing.framesMs.filter((time) => time >= segment.offsetMs && time < to).at(-1);
|
|
220
|
-
return last === void 0 ? latest : Math.max(latest, segment.stoppedAtUnixMs - last);
|
|
221
|
-
}, timing.clock.latestZeroUnixMs);
|
|
222
|
-
}
|
|
223
|
-
function joinSegments(segments, outputPath) {
|
|
224
|
-
if (segments.length === 1) {
|
|
225
|
-
fs.renameSync(segments[0].localPath, outputPath);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const listPath = `${outputPath}.segments.txt`;
|
|
229
|
-
const list = segments.map((segment, index) => {
|
|
230
|
-
const entry = `file '${segment.localPath.replaceAll("'", "'\\''")}'`;
|
|
231
|
-
const next = segments[index + 1];
|
|
232
|
-
return next ? `${entry}
|
|
233
|
-
duration ${((next.offsetMs - segment.offsetMs) / 1e3).toFixed(3)}` : entry;
|
|
234
|
-
});
|
|
235
|
-
fs.writeFileSync(listPath, `${list.join("\n")}
|
|
236
|
-
`);
|
|
237
|
-
try {
|
|
238
|
-
const join = spawnSync(ffmpegPath(), ["-v", "error", "-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", outputPath], {
|
|
239
|
-
encoding: "utf8"
|
|
240
|
-
});
|
|
241
|
-
if (join.status !== 0) throw new Error(`Could not join Android recording segments: ${failure(join)}`);
|
|
242
|
-
} finally {
|
|
243
|
-
fs.rmSync(listPath, { force: true });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
216
|
function ffmpegPath() {
|
|
247
217
|
return process.env.FFMPEG_PATH || "ffmpeg";
|
|
248
218
|
}
|
|
@@ -12,12 +12,13 @@ const mobileDevices = {
|
|
|
12
12
|
// adapter-sdk's live view names the app's feature-flag overrides `featureFlags`.
|
|
13
13
|
async live(target, view) {
|
|
14
14
|
const live = await mobileDeviceLiveView(target, view);
|
|
15
|
-
return { ...live, featureFlags: live.featureFlagOverrides };
|
|
15
|
+
return { ...live, featureFlags: live.featureFlagOverrides, statusFields: live.androidPortHints.length ? { androidPortHints: live.androidPortHints } : {} };
|
|
16
16
|
},
|
|
17
17
|
renderList: renderMobileDeviceList,
|
|
18
18
|
renderAdditional: renderAdditionalReachableDevices,
|
|
19
19
|
renderLive: renderMobileLiveBlock,
|
|
20
20
|
renderPortHints: renderAndroidPortHints,
|
|
21
|
+
renderHints: (live, paint) => renderAndroidPortHints(live.androidPortHints, paint),
|
|
21
22
|
nextForLive
|
|
22
23
|
};
|
|
23
24
|
export {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { EXIT,
|
|
1
|
+
import { EXIT, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir } from "../../paths.js";
|
|
4
|
-
function reloadMobile(target, json) {
|
|
4
|
+
async function reloadMobile(target, json) {
|
|
5
5
|
const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
|
|
6
6
|
const args = [];
|
|
7
7
|
if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
|
|
8
8
|
if (json) args.push("--json");
|
|
9
|
-
const result =
|
|
9
|
+
const result = await spawnScriptStreaming(process.execPath, [script, ...args], target, { forward: json ? "none" : "stderr" });
|
|
10
10
|
if (json) process.stdout.write(result.output);
|
|
11
11
|
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
12
12
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EXIT, isRecord } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { IOS_SIMULATOR_RECORDING_TARGET_PREFIX } from "@farmslot/adapter-rn";
|
|
5
4
|
import { devicePlatformFromEnv } from "../performance/device-platform.js";
|
|
6
5
|
import {
|
|
7
6
|
resolveAndroidDeviceSerial,
|
|
@@ -171,8 +170,8 @@ async function resolveMobileRecordingTarget(context) {
|
|
|
171
170
|
);
|
|
172
171
|
}
|
|
173
172
|
return {
|
|
174
|
-
kind: "
|
|
175
|
-
|
|
173
|
+
kind: "simulator",
|
|
174
|
+
device: simulator
|
|
176
175
|
};
|
|
177
176
|
}
|
|
178
177
|
const mobileDiagnostics = {
|
package/dist/checkout-view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { color, recipeRuntimeDir } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { color, harnessContext, recipeRuntimeDir } from "@farmslot/recipe-cli/harness";
|
|
4
4
|
import { renderRuntimeContext, runtimeContextSummary } from "./doctor.js";
|
|
5
5
|
import {
|
|
6
6
|
configPath,
|
|
@@ -12,11 +12,18 @@ const NO_LIBRARIES = "none registered. Learn how: docs/RECIPES.md, or run: mm-ha
|
|
|
12
12
|
const FIXTURE_REPORT = path.join("prepare", "fixtures.json");
|
|
13
13
|
function collectCheckoutView(target, adapter) {
|
|
14
14
|
const declared = declaredLibraryPathEnv();
|
|
15
|
-
const
|
|
15
|
+
const bound = harnessContext()?.project?.libraries;
|
|
16
|
+
const sources = bound !== void 0 ? bound.map((library) => ({
|
|
17
|
+
name: library.name,
|
|
18
|
+
root: library.root,
|
|
19
|
+
source: "binding",
|
|
20
|
+
revision: library.identity.head ?? library.revision
|
|
21
|
+
})) : discoverLibraries({
|
|
16
22
|
target,
|
|
17
23
|
config: readConfig(configPath()),
|
|
18
24
|
...declared ? { env: { ...process.env, RECIPE_LIBRARY_PATH: declared.value } } : {}
|
|
19
|
-
})
|
|
25
|
+
});
|
|
26
|
+
const libraries = sources.map((library) => ({
|
|
20
27
|
name: library.name,
|
|
21
28
|
root: library.root,
|
|
22
29
|
source: library.source,
|
package/dist/command-contract.js
CHANGED
|
@@ -42,6 +42,7 @@ const RECIPE_RUNTIME = {
|
|
|
42
42
|
"--validation-runtime-dir": value(),
|
|
43
43
|
"--launch-existing-dist": bool(),
|
|
44
44
|
"--record-video": optionalValue(["full-run", "off"]),
|
|
45
|
+
"--recording-reason": value(),
|
|
45
46
|
"--source-trust": value(["trusted", "untrusted", "unknown"]),
|
|
46
47
|
"--source-kind": value(["bundled", "operator", "task", "recipe-file", "library", "custom-adapter"]),
|
|
47
48
|
"--source-name": value(),
|
|
@@ -788,6 +789,7 @@ Common overrides:
|
|
|
788
789
|
--json Machine-readable output
|
|
789
790
|
--json-stream Line-flushed JSONL progress + terminal event
|
|
790
791
|
--record-video=full-run Record a video of the run
|
|
792
|
+
--recording-reason <text> Explain an explicit recording opt-out
|
|
791
793
|
--hud <show|hide> Recipe HUD: each node's intent drawn over the app on every adapter, headless included (default: show)
|
|
792
794
|
|
|
793
795
|
Advanced integrations:
|
package/dist/commands/check.js
CHANGED
|
@@ -62,34 +62,32 @@ async function handleCheck(argv) {
|
|
|
62
62
|
});
|
|
63
63
|
const policyFailed = suppressionCheck.status === "fail";
|
|
64
64
|
const fixes = [];
|
|
65
|
-
const
|
|
65
|
+
const usesOxlint = typeof packageJson?.scripts?.["lint:oxlint"] === "string";
|
|
66
|
+
const lintCommandArgs = usesOxlint ? ["lint:oxlint"] : resolveEslintConfigArgs(packageJson);
|
|
67
|
+
const lintCacheArgs = usesOxlint ? [] : ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache")];
|
|
68
|
+
const lintCheck = {
|
|
69
|
+
id: usesOxlint ? "oxlint" : "eslint",
|
|
70
|
+
label: usesOxlint ? "Changed-file Oxlint" : "Changed-file ESLint",
|
|
71
|
+
target,
|
|
72
|
+
artifactsDir,
|
|
73
|
+
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
74
|
+
command: usesOxlint ? "yarn" : executable(target, "eslint"),
|
|
75
|
+
args: [...lintCommandArgs, ...lintCacheArgs]
|
|
76
|
+
};
|
|
66
77
|
if (fix && !policyFailed) {
|
|
67
78
|
fixes.push(
|
|
68
79
|
runToolCheck({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
74
|
-
command: executable(target, "eslint"),
|
|
75
|
-
args: [...eslintConfigArgs, "--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
80
|
+
...lintCheck,
|
|
81
|
+
id: `${lintCheck.id}-fix`,
|
|
82
|
+
label: `${lintCheck.label} fix`,
|
|
83
|
+
args: [...lintCommandArgs, "--fix", ...lintCacheArgs]
|
|
76
84
|
})
|
|
77
85
|
);
|
|
78
86
|
fixes.push(runFormatterFix({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
79
87
|
}
|
|
80
88
|
const checks = [suppressionCheck];
|
|
81
89
|
if (!policyFailed) {
|
|
82
|
-
checks.push(
|
|
83
|
-
runToolCheck({
|
|
84
|
-
id: "eslint",
|
|
85
|
-
label: "Changed-file ESLint",
|
|
86
|
-
target,
|
|
87
|
-
artifactsDir,
|
|
88
|
-
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
89
|
-
command: executable(target, "eslint"),
|
|
90
|
-
args: [...eslintConfigArgs, "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
91
|
-
})
|
|
92
|
-
);
|
|
90
|
+
checks.push(runToolCheck(lintCheck));
|
|
93
91
|
checks.push(runFormatterCheck({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
94
92
|
checks.push(
|
|
95
93
|
...runJestChecks({
|
|
@@ -115,7 +113,7 @@ async function handleCheck(argv) {
|
|
|
115
113
|
}
|
|
116
114
|
const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
|
|
117
115
|
const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
|
|
118
|
-
const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id ===
|
|
116
|
+
const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === lintCheck.id && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
|
|
119
117
|
const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
|
|
120
118
|
const summary = {
|
|
121
119
|
schemaVersion: 1,
|
|
@@ -129,7 +129,7 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
129
129
|
}
|
|
130
130
|
if (matched.platform === "android") setAndroidDeviceEnv(matched.id, matched.name);
|
|
131
131
|
else setIosDeviceEnv(matched.id, matched.name);
|
|
132
|
-
persistExplicitDevicePin(targetPath(options), matched);
|
|
132
|
+
if (opts.persistPin !== false) persistExplicitDevicePin(targetPath(options), matched);
|
|
133
133
|
return { ok: true };
|
|
134
134
|
};
|
|
135
135
|
const androidById = connected2.find((d) => d.platform === "android" && d.id === device);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { featureFlagReport } from "../feature-flags.js";
|
|
5
|
-
import { adapterPluginChecks, color, acquireCheckoutLock, contextAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, targetPath } from "@farmslot/recipe-cli/harness";
|
|
5
|
+
import { adapterPluginChecks, color, acquireCheckoutLock, contextAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, spawnScriptStreaming, doctorReady, renderDoctorRecorder, targetPath } from "@farmslot/recipe-cli/harness";
|
|
6
6
|
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
7
7
|
import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
|
|
8
8
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
@@ -110,7 +110,7 @@ async function handleDoctor({ options }) {
|
|
|
110
110
|
} else if (runtimeProbeError2) {
|
|
111
111
|
console.log(`runtime probe failed: ${runtimeProbeError2}`);
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
renderDoctorRecorder(recorder2);
|
|
114
114
|
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
115
115
|
}
|
|
116
116
|
return status === "pass" ? 0 : 1;
|
|
@@ -230,7 +230,7 @@ async function handleDoctor({ options }) {
|
|
|
230
230
|
} else if (runtimeProbeError) {
|
|
231
231
|
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
232
232
|
}
|
|
233
|
-
|
|
233
|
+
renderDoctorRecorder(recorder, out);
|
|
234
234
|
console.log(renderCheckoutView(view));
|
|
235
235
|
console.log(`${out("label", "feature flags:")} ${out(
|
|
236
236
|
featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
|
|
@@ -255,9 +255,7 @@ async function handleDoctor({ options }) {
|
|
|
255
255
|
}
|
|
256
256
|
return doctorResult.status === "pass" ? 0 : 1;
|
|
257
257
|
}
|
|
258
|
-
|
|
259
|
-
return runtime?.decision === "ready" && recorder?.status !== "fail";
|
|
260
|
-
}
|
|
258
|
+
import { doctorReady as doctorReady2 } from "@farmslot/recipe-cli/harness";
|
|
261
259
|
async function platformDoctorChecks(adapter, target) {
|
|
262
260
|
try {
|
|
263
261
|
return await getAdapterSurface(adapter).doctor?.(target) ?? [];
|
|
@@ -295,11 +293,7 @@ function doctorFixNextActions(failed, fixture, adapter, target, block) {
|
|
|
295
293
|
const actions = [];
|
|
296
294
|
const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
|
|
297
295
|
const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
298
|
-
|
|
299
|
-
const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
|
|
300
|
-
actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
|
|
301
|
-
actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
|
|
302
|
-
}
|
|
296
|
+
actions.push(...doctorFixtureNextActions(failed, fixture, scope, executable));
|
|
303
297
|
if (failed.includes("runtime-context")) {
|
|
304
298
|
actions.push(
|
|
305
299
|
`inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
|
|
@@ -309,6 +303,15 @@ function doctorFixNextActions(failed, fixture, adapter, target, block) {
|
|
|
309
303
|
if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
|
|
310
304
|
return actions;
|
|
311
305
|
}
|
|
306
|
+
function doctorFixtureNextActions(failed, fixture, scope, executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness") {
|
|
307
|
+
const actions = [];
|
|
308
|
+
if (failed.includes("wallet-fixture")) {
|
|
309
|
+
const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
|
|
310
|
+
actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
|
|
311
|
+
actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
|
|
312
|
+
}
|
|
313
|
+
return actions;
|
|
314
|
+
}
|
|
312
315
|
function emitExpectLiveVerdict(adapter, target, runtime, verdict, recorder) {
|
|
313
316
|
const out = (style, text) => color(style, text, { stream: process.stderr });
|
|
314
317
|
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
@@ -456,26 +459,27 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
|
|
|
456
459
|
const ensured = await ensureOverlay(adapter, target, "infra-only", state, json);
|
|
457
460
|
if (!ensured.ok) failed.push("overlay");
|
|
458
461
|
else if (state.mutations.length > 0) fixed.push("overlay");
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
execFileSync("bash", [syncFixture, "--target", target], {
|
|
464
|
-
encoding: "utf8",
|
|
465
|
-
stdio: json ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
466
|
-
timeout: 3e4
|
|
467
|
-
});
|
|
468
|
-
} catch {
|
|
469
|
-
}
|
|
470
|
-
fixture = fixtureSummary(target, adapter);
|
|
471
|
-
if (fixture.status === "ready") fixed.push("wallet-fixture");
|
|
472
|
-
else failed.push("wallet-fixture");
|
|
473
|
-
}
|
|
462
|
+
const fixture = await repairDoctorFixture(target, adapter, json);
|
|
463
|
+
fixed.push(...fixture.fixed);
|
|
464
|
+
failed.push(...fixture.failed);
|
|
474
465
|
return { fixed, failed };
|
|
475
466
|
}
|
|
467
|
+
async function repairDoctorFixture(target, adapter, json) {
|
|
468
|
+
if (fixtureSummary(target, adapter).status === "ready") return { fixed: [], failed: [] };
|
|
469
|
+
const syncFixture = path.join(runnerDir, "adapters/shared/sync-wallet-fixture.sh");
|
|
470
|
+
await spawnScriptStreaming("bash", [syncFixture, "--target", target], target, {
|
|
471
|
+
forward: json ? "none" : "stderr",
|
|
472
|
+
timeoutMs: 3e4
|
|
473
|
+
});
|
|
474
|
+
return fixtureSummary(target, adapter).status === "ready" ? { fixed: ["wallet-fixture"], failed: [] } : { fixed: [], failed: ["wallet-fixture"] };
|
|
475
|
+
}
|
|
476
476
|
export {
|
|
477
477
|
doctorFixNextActions,
|
|
478
|
-
|
|
478
|
+
doctorFixtureNextActions,
|
|
479
|
+
doctorReady2 as doctorReady,
|
|
479
480
|
handleDoctor,
|
|
481
|
+
renderSkillsView,
|
|
482
|
+
renderUpdateStatus,
|
|
483
|
+
repairDoctorFixture,
|
|
480
484
|
skillsView
|
|
481
485
|
};
|
package/dist/commands/logs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnInherit,
|
|
1
|
+
import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnInherit, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { createLogsOptions } from "../provider-logs.js";
|
|
5
5
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
6
6
|
const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json", "window"]);
|
|
7
7
|
async function handleLogs(argv) {
|
|
@@ -66,27 +66,12 @@ async function handleLogs(argv) {
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
if (flag(options, "window")) {
|
|
69
|
-
surface.
|
|
70
|
-
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
|
|
71
|
-
const windowName = `${surface.devServer.label}-${port}`;
|
|
72
|
-
const devLog = logSources[0].path;
|
|
73
|
-
const leaf = path.join(runnerDir, "adapters/shared/open-log-window.sh");
|
|
74
|
-
const result = spawnScript(
|
|
75
|
-
leaf,
|
|
76
|
-
["--window", windowName, "--log", devLog, "--runtime-dir", path.dirname(devLog)],
|
|
69
|
+
const result = await createLogsOptions(surface).window({
|
|
77
70
|
target,
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
if (json) {
|
|
81
|
-
|
|
82
|
-
JSON.stringify(
|
|
83
|
-
{ schemaVersion: 1, command: "logs", action: "window", adapter, window: windowName, logFile: devLog, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
84
|
-
null,
|
|
85
|
-
2
|
|
86
|
-
)
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
71
|
+
logFile: logSources[0].path
|
|
72
|
+
});
|
|
73
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "logs", action: "window", adapter, ...result }, null, 2));
|
|
74
|
+
return result.exitCode;
|
|
90
75
|
}
|
|
91
76
|
if (json) {
|
|
92
77
|
console.log(JSON.stringify({ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: EXIT.ok }, null, 2));
|
|
@@ -102,14 +87,12 @@ async function handleLogs(argv) {
|
|
|
102
87
|
if (full) {
|
|
103
88
|
return spawnInherit("tail", ["-n", "+1", "-F", logFile], target) === 0 ? EXIT.ok : EXIT.runtime;
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
process.
|
|
110
|
-
|
|
111
|
-
target
|
|
112
|
-
) === 0 ? EXIT.ok : EXIT.runtime;
|
|
90
|
+
return createLogsOptions(surface).compact({
|
|
91
|
+
target,
|
|
92
|
+
logFile,
|
|
93
|
+
events: process.env.RECIPE_LOG_EVENTS ?? "20",
|
|
94
|
+
mode: process.env.RECIPE_LOG_UI_MODE ?? "compact"
|
|
95
|
+
});
|
|
113
96
|
}
|
|
114
97
|
export {
|
|
115
98
|
handleLogs
|