@deeeed/metamask-harness 0.88.0 → 0.90.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 +25 -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/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 +10 -3
- package/dist/runner.js +1 -0
- package/library/actions/extension/platform/cdp.mjs +3 -19
- package/library/actions/mobile/platform/observe-ui.mjs +45 -15
- 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,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.90.0 - 2026-10-12
|
|
6
|
+
|
|
7
|
+
- Use typed iOS simulator recording targets, retain original Android recording segments, and use the shared action-context snapshot API for Extension evidence.
|
|
8
|
+
|
|
9
|
+
- Share compact log viewing and owned-window recovery with the project recipe CLI, preserving event counts and cancellation.
|
|
10
|
+
|
|
11
|
+
- Preserve project doctor reports, setup advisories, explicit fixture repairs and wallet-aware health indicators through the shared provider host.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- Collect opt-in rendered HUD observations around screenshot capture for live conformance.
|
|
18
|
+
- Bind navigation-memory suite claims to explicit assertions on the measured bounds.
|
|
19
|
+
|
|
20
|
+
## 0.89.0 - 2026-10-11
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Export the canonical command grammar with the provider so shared hosts validate domain options before constructing an executing provider.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Pin recipe-runner 0.29.0, recipe-cli 0.13.0, adapter-sdk 0.8.6, adapter-node 0.1.12, adapter-rn 0.19.10 and adapter-web 0.6.10 so the provider shares the generic host's signal-aware execution and finalization runtime. Protocol and agent-runtime stay at their current published versions.
|
|
29
|
+
|
|
5
30
|
## 0.88.0 - 2026-10-11
|
|
6
31
|
|
|
7
32
|
### 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:
|
|
@@ -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
|
package/dist/doctor.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { color, harnessContext, harnessContextField, harnessExecutable, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { color, createDoctorReport as sharedDoctorReport, harnessContext, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath, runnerInstallKind as sharedInstallKind, runnerProvenance as sharedProvenance } from "@farmslot/recipe-cli/harness";
|
|
5
4
|
import { normalizeFeatureFlagRecord } from "./feature-flags.js";
|
|
6
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
6
|
import { isPin } from "./harness-pin.js";
|
|
8
|
-
import { readJson
|
|
7
|
+
import { readJson } from "./paths.js";
|
|
9
8
|
function requiredDoctorCheckSummary(checks) {
|
|
10
9
|
const required = checks.filter((check) => check.required);
|
|
11
10
|
const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
|
|
@@ -139,99 +138,40 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
139
138
|
}
|
|
140
139
|
return lines.join("\n");
|
|
141
140
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
{
|
|
148
|
-
id: "manifest",
|
|
149
|
-
status: manifestErrors === 0 ? "pass" : "fail",
|
|
150
|
-
required: true,
|
|
151
|
-
message: manifestErrors === 0 ? "Action manifest is valid Recipe v1." : `Action manifest has ${manifestErrors} validation error(s).`
|
|
152
|
-
},
|
|
153
|
-
{
|
|
141
|
+
const doctorReportOptions = {
|
|
142
|
+
provenance: { name: "@metamask/recipe-runner", pinned: isPin },
|
|
143
|
+
checks(target, adapter) {
|
|
144
|
+
const mode = compatibilityMode(adapter, target);
|
|
145
|
+
return [{
|
|
154
146
|
id: "bridge",
|
|
155
147
|
status: mode === "unsupported/no bridge" ? "fail" : "pass",
|
|
156
148
|
required: false,
|
|
157
149
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
schemaVersion: 1,
|
|
164
|
-
protocolVersion: "v1",
|
|
165
|
-
runner_protocol_version: 1,
|
|
166
|
-
status: requiredChecks.status,
|
|
167
|
-
checks,
|
|
168
|
-
requiredChecks,
|
|
169
|
-
adapter,
|
|
170
|
-
target,
|
|
171
|
-
...harnessContextField(),
|
|
172
|
-
runner: runnerProvenance(actionManifestPath),
|
|
173
|
-
compatibilityMode: mode,
|
|
150
|
+
}];
|
|
151
|
+
},
|
|
152
|
+
fields: (target, adapter, environment) => ({
|
|
153
|
+
compatibilityMode: compatibilityMode(adapter, target),
|
|
174
154
|
shape: repoShape(target),
|
|
175
155
|
fixture: fixtureSummary(target, adapter),
|
|
176
|
-
environment
|
|
177
|
-
runtimeContext: runtimeContextSummary(target, adapter)
|
|
178
|
-
|
|
179
|
-
|
|
156
|
+
environment,
|
|
157
|
+
runtimeContext: runtimeContextSummary(target, adapter)
|
|
158
|
+
})
|
|
159
|
+
};
|
|
160
|
+
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = getAdapterSurface(adapter).actions.manifestPath(), platform) {
|
|
161
|
+
const report = sharedDoctorReport(adapter, target, manifestValidation, actionManifestPath, platform, doctorReportOptions);
|
|
162
|
+
return { ...report, runner: metaMaskProvenance(report.runner), manifestValidation: manifestValidation.summary };
|
|
180
163
|
}
|
|
181
164
|
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
182
|
-
|
|
183
|
-
const normalizedRoot = path.normalize(runnerRoot);
|
|
184
|
-
const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
|
|
185
|
-
const globalNodeModulesSegment = `${path.sep}lib${path.sep}node_modules${path.sep}`;
|
|
186
|
-
if (normalizedRoot.includes(globalNodeModulesSegment)) return "global-install";
|
|
187
|
-
const nodeModulesIndex = normalizedRoot.lastIndexOf(nodeModulesSegment);
|
|
188
|
-
if (nodeModulesIndex >= 0) {
|
|
189
|
-
const nodeModulesRoot = normalizedRoot.slice(
|
|
190
|
-
0,
|
|
191
|
-
nodeModulesIndex + nodeModulesSegment.length - 1
|
|
192
|
-
);
|
|
193
|
-
const invoked = path.resolve(invokedPath);
|
|
194
|
-
const projectBin = `${path.join(nodeModulesRoot, ".bin")}${path.sep}`;
|
|
195
|
-
if (invoked === path.resolve(executablePath) || invoked.startsWith(projectBin)) {
|
|
196
|
-
return "project-install";
|
|
197
|
-
}
|
|
198
|
-
return "global-install";
|
|
199
|
-
}
|
|
200
|
-
if (path.resolve(invokedPath) !== path.resolve(executablePath)) return "local-link";
|
|
201
|
-
return "source-checkout";
|
|
165
|
+
return sharedInstallKind(runnerRoot, invokedPath, executablePath, isPin);
|
|
202
166
|
}
|
|
203
167
|
function runnerProvenance(actionManifestPath) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
throw new Error(`Expected @deeeed/metamask-harness package name in ${packagePath}`);
|
|
210
|
-
}
|
|
211
|
-
if (typeof version !== "string" || !version) {
|
|
212
|
-
throw new Error(`Expected package version in ${packagePath}`);
|
|
168
|
+
return metaMaskProvenance(sharedProvenance(actionManifestPath, doctorReportOptions.provenance));
|
|
169
|
+
}
|
|
170
|
+
function metaMaskProvenance(provenance) {
|
|
171
|
+
if (provenance.name !== "@metamask/recipe-runner" || provenance.packageName !== "@deeeed/metamask-harness") {
|
|
172
|
+
throw new Error("Doctor provenance does not identify the MetaMask provider.");
|
|
213
173
|
}
|
|
214
|
-
|
|
215
|
-
const invokedPath = path.resolve(process.env.MM_HARNESS_INVOKED_AS ?? executablePath);
|
|
216
|
-
const installKind = runnerInstallKind(runnerDir, invokedPath, executablePath);
|
|
217
|
-
const packageSource = isPin(runnerDir) ? "pin" : runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
|
|
218
|
-
return {
|
|
219
|
-
name: "@metamask/recipe-runner",
|
|
220
|
-
packageName,
|
|
221
|
-
version,
|
|
222
|
-
runnerDir,
|
|
223
|
-
packagePath,
|
|
224
|
-
packageSource,
|
|
225
|
-
installKind,
|
|
226
|
-
linked: installKind === "local-link",
|
|
227
|
-
global: installKind === "global-install",
|
|
228
|
-
invokedPath,
|
|
229
|
-
executablePath,
|
|
230
|
-
actionManifestPath,
|
|
231
|
-
harnessPackage: "@farmslot/recipe-runner",
|
|
232
|
-
// The @farmslot/recipe-cli this harness runs on (`--version --verbose` prints it too).
|
|
233
|
-
recipeCliVersion: RECIPE_CLI_VERSION
|
|
234
|
-
};
|
|
174
|
+
return { ...provenance, name: provenance.name, packageName: provenance.packageName };
|
|
235
175
|
}
|
|
236
176
|
function readJsonObject(file) {
|
|
237
177
|
const value = readJson(file);
|
|
@@ -243,6 +183,7 @@ function readJsonObject(file) {
|
|
|
243
183
|
export {
|
|
244
184
|
compatibilityMode,
|
|
245
185
|
createDoctorReport,
|
|
186
|
+
doctorReportOptions,
|
|
246
187
|
fixtureFileSummary,
|
|
247
188
|
fixtureSummary,
|
|
248
189
|
renderRuntimeContext,
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
parseArgs,
|
|
21
21
|
targetPath
|
|
22
22
|
} from "@farmslot/recipe-cli/harness";
|
|
23
|
-
import {
|
|
23
|
+
import { stopOptions } from "./provider-lifecycle.js";
|
|
24
24
|
import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources } from "./adapters/plugin-surface.js";
|
|
25
25
|
import { withMainnetOrderAbort } from "./adapters/perps-mainnet-cleanup.js";
|
|
26
26
|
import { adapterRegistry } from "./adapters/registry.js";
|
|
@@ -70,12 +70,6 @@ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "
|
|
|
70
70
|
function hostVersion() {
|
|
71
71
|
return JSON.parse(fs.readFileSync(path.join(harnessHost().packageRoot, "package.json"), "utf8")).version;
|
|
72
72
|
}
|
|
73
|
-
const stopOptions = {
|
|
74
|
-
companions: async (target) => {
|
|
75
|
-
const collector = await stopProjectCollector(target);
|
|
76
|
-
return collector.stopped && collector.pid !== null ? [{ label: "analytics collector", pid: collector.pid }] : [];
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
73
|
const targetDevice = (command, adapter, options) => applyDeviceTargeting(command, adapter, options, { gate: true, rerun: "" });
|
|
80
74
|
const runOptions = {
|
|
81
75
|
engine: metaMaskEngine,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { doctorFixtureNextActions, repairDoctorFixture, renderSkillsView, renderUpdateStatus, skillsView } from "./commands/doctor.js";
|
|
2
|
+
import { farmslotReadyIndicator } from "./commands/farmslot-ready.js";
|
|
3
|
+
import { updateStatus } from "./commands/update.js";
|
|
4
|
+
import { doctorReportOptions, fixtureSummary } from "./doctor.js";
|
|
5
|
+
import { loadActionManifest, validateManifest } from "./manifest.js";
|
|
6
|
+
import { statusOptions } from "./provider-lifecycle.js";
|
|
7
|
+
import { ensureRuntimeContext } from "./runtime-context.js";
|
|
8
|
+
const doctorOptions = {
|
|
9
|
+
...statusOptions,
|
|
10
|
+
manifest: async (adapter, override) => validateManifest(loadActionManifest(adapter, override)),
|
|
11
|
+
report: doctorReportOptions,
|
|
12
|
+
orphanDevServers: { field: "orphanMetros", label: "orphan Metro" },
|
|
13
|
+
advisory(target) {
|
|
14
|
+
const skills = skillsView(target);
|
|
15
|
+
const update = updateStatus();
|
|
16
|
+
return [
|
|
17
|
+
{ key: "skills", value: skills, render: (out) => renderSkillsView(skills, out) },
|
|
18
|
+
{ key: "update", value: update, render: (out) => renderUpdateStatus(update, out) }
|
|
19
|
+
];
|
|
20
|
+
},
|
|
21
|
+
fix: {
|
|
22
|
+
ensureRuntimeContext,
|
|
23
|
+
repair: repairDoctorFixture,
|
|
24
|
+
nextActions: (failed, { adapter, target, scope }) => doctorFixtureNextActions(failed, fixtureSummary(target, adapter), scope),
|
|
25
|
+
userAction: (failed, retry) => failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retry}` : void 0
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function configureProviderReadiness(runtime) {
|
|
29
|
+
Object.assign(runtime.readiness, {
|
|
30
|
+
readyIndicator: (devices) => farmslotReadyIndicator(runtime.id, [...devices])
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
configureProviderReadiness,
|
|
35
|
+
doctorOptions
|
|
36
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
|
|
2
|
+
import { collectCheckoutView, renderCheckoutView } from "./checkout-view.js";
|
|
3
|
+
import { applyDeviceTargeting } from "./commands/device-target.js";
|
|
4
|
+
import { featureFlagReport } from "./feature-flags.js";
|
|
5
|
+
const statusOptions = {
|
|
6
|
+
checkoutView(target, adapter) {
|
|
7
|
+
const view = collectCheckoutView(target, adapter);
|
|
8
|
+
return { json: view, text: renderCheckoutView(view) };
|
|
9
|
+
},
|
|
10
|
+
featureFlags: { report: (overrides, detail) => featureFlagReport(overrides, detail) },
|
|
11
|
+
previewDevice: (command, adapter, options) => applyDeviceTargeting(command, adapter, options, {
|
|
12
|
+
gate: false,
|
|
13
|
+
rerun: "",
|
|
14
|
+
persistPin: false
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
const stopOptions = {
|
|
18
|
+
companions: async (target) => {
|
|
19
|
+
const collector = await stopProjectCollector(target);
|
|
20
|
+
return collector.stopped && collector.pid !== null ? [{ label: "analytics collector", pid: collector.pid }] : [];
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export {
|
|
24
|
+
statusOptions,
|
|
25
|
+
stopOptions
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { EXIT, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { runnerDir } from "./paths.js";
|
|
4
|
+
function createLogsOptions(surface) {
|
|
5
|
+
return {
|
|
6
|
+
async compact({ target, logFile, events, mode, signal }) {
|
|
7
|
+
const result = await spawnScriptStreaming(
|
|
8
|
+
process.execPath,
|
|
9
|
+
[path.join(runnerDir, "adapters/shared/log-tui.mjs"), "tail", "--log", logFile, "--events", events, "--follow", "--mode", mode],
|
|
10
|
+
target,
|
|
11
|
+
{ signal, forward: "original", capture: false }
|
|
12
|
+
);
|
|
13
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
14
|
+
},
|
|
15
|
+
async window({ target, logFile, signal }) {
|
|
16
|
+
surface.resolveSlotPorts(target);
|
|
17
|
+
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
|
|
18
|
+
const window = `${surface.devServer.label}-${port}`;
|
|
19
|
+
const result = await spawnScriptStreaming(
|
|
20
|
+
path.join(runnerDir, "adapters/shared/open-log-window.sh"),
|
|
21
|
+
["--window", window, "--log", logFile, "--runtime-dir", path.dirname(logFile)],
|
|
22
|
+
target,
|
|
23
|
+
{ signal, forward: "stderr", capture: false }
|
|
24
|
+
);
|
|
25
|
+
return { window, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
createLogsOptions
|
|
31
|
+
};
|
package/dist/provider.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
applyRuntimeDirOption,
|
|
6
6
|
configureHarnessHost,
|
|
7
7
|
ensureAdapterLoaded,
|
|
8
|
-
|
|
8
|
+
isRecipeExecution,
|
|
9
9
|
optionStrings,
|
|
10
10
|
usageError
|
|
11
11
|
} from "@farmslot/recipe-cli/harness";
|
|
@@ -17,7 +17,11 @@ import { getAdapterSurface } from "./adapters/surface.js";
|
|
|
17
17
|
import { applyDeviceTargeting } from "./commands/device-target.js";
|
|
18
18
|
import { metaMaskEngine } from "./engine.js";
|
|
19
19
|
import { providerHost } from "./host.js";
|
|
20
|
+
import { configureProviderReadiness, doctorOptions } from "./provider-doctor.js";
|
|
21
|
+
import { statusOptions, stopOptions } from "./provider-lifecycle.js";
|
|
22
|
+
import { createLogsOptions } from "./provider-logs.js";
|
|
20
23
|
import { providerHost as providerHost2 } from "./host.js";
|
|
24
|
+
import { PUBLIC_COMMANDS } from "./command-contract.js";
|
|
21
25
|
async function createProvider(context) {
|
|
22
26
|
configureHarnessHost(context.host ?? providerHost);
|
|
23
27
|
configureHarnessAdapters(adapterRegistry);
|
|
@@ -36,8 +40,9 @@ async function createProvider(context) {
|
|
|
36
40
|
});
|
|
37
41
|
await afterAdapterLoad();
|
|
38
42
|
const runtime = getAdapterSurface(adapter);
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
configureProviderReadiness(runtime);
|
|
44
|
+
const executes = isRecipeExecution(context.command, options);
|
|
45
|
+
if (executes || context.command === "reload") {
|
|
41
46
|
applyRuntimeDirOption(options);
|
|
42
47
|
runtime.resolveSlotPorts(context.target.value);
|
|
43
48
|
const selected = applyDeviceTargeting(context.command, adapter, {
|
|
@@ -51,6 +56,7 @@ async function createProvider(context) {
|
|
|
51
56
|
return {
|
|
52
57
|
runtime,
|
|
53
58
|
engine: metaMaskEngine,
|
|
59
|
+
commandOptions: { status: statusOptions, stop: stopOptions, doctor: doctorOptions, logs: createLogsOptions(runtime) },
|
|
54
60
|
...executes ? {
|
|
55
61
|
async cancel() {
|
|
56
62
|
await abortRunMainnetOrders(context.target.value);
|
|
@@ -68,5 +74,6 @@ async function createProvider(context) {
|
|
|
68
74
|
}
|
|
69
75
|
export {
|
|
70
76
|
createProvider,
|
|
77
|
+
PUBLIC_COMMANDS as providerCommands,
|
|
71
78
|
providerHost2 as providerHost
|
|
72
79
|
};
|
package/dist/runner.js
CHANGED
|
@@ -70,6 +70,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
70
70
|
const core = createStandardCoreAdapters({ actions });
|
|
71
71
|
const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
|
|
72
72
|
const ui = createStandardUiAdapters({
|
|
73
|
+
captureHudEvidence: options.captureHudEvidence,
|
|
73
74
|
actions: actions.filter((action) => !projectOwnedOfficialActions.has(action)),
|
|
74
75
|
transport: createMetaMaskUiTransport(adapter, {
|
|
75
76
|
createCdpWebUiTransport,
|
|
@@ -20,7 +20,7 @@ import { spawn } from 'node:child_process';
|
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
|
|
24
24
|
import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, extensionIdPath, walletFixturePath } from '../../harness-exports.mjs';
|
|
25
25
|
import { liveAdapterDeadline } from '../../shared/app/live-adapter-deadline.mjs';
|
|
26
26
|
import { withRateLimitRetry } from '../../shared/perps/rate-limit.mjs';
|
|
@@ -375,25 +375,9 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
|
|
|
375
375
|
try {
|
|
376
376
|
pid = await captureHelperBrowserPid(context, page.port);
|
|
377
377
|
const timeoutMs = Number(metadata?.timeoutMs ?? 30000);
|
|
378
|
-
|
|
379
|
-
pid,
|
|
380
|
-
destination.stagedPath,
|
|
381
|
-
timeoutMs,
|
|
382
|
-
);
|
|
383
|
-
if (sessionSnapshot) {
|
|
378
|
+
if (context.captureRecordingSnapshot) {
|
|
384
379
|
mode = 'record_session_snapshot';
|
|
385
|
-
details =
|
|
386
|
-
} else if (
|
|
387
|
-
parsePositivePid(process.env.METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID) === pid
|
|
388
|
-
) {
|
|
389
|
-
await cleanupEvidenceStaging(destination.stagingDir);
|
|
390
|
-
return captureCdpSnapshot(
|
|
391
|
-
page,
|
|
392
|
-
context,
|
|
393
|
-
relPath,
|
|
394
|
-
metadata,
|
|
395
|
-
'native snapshot skipped while full-run recording owns the browser window',
|
|
396
|
-
);
|
|
380
|
+
details = await context.captureRecordingSnapshot(path.relative(artifactsRoot, destination.stagedPath));
|
|
397
381
|
} else {
|
|
398
382
|
await reapOrphanedCaptureHelpers(context);
|
|
399
383
|
const result = await runProcess(
|
|
@@ -53,10 +53,29 @@ const IOS_TEXT_TEST_IDS = `(() => {
|
|
|
53
53
|
})()`;
|
|
54
54
|
|
|
55
55
|
export async function observeNativeUi(payload, context) {
|
|
56
|
-
await hideAccessibilityMaskingHud(payload, context);
|
|
57
56
|
const refs = Array.isArray(payload?.refs)
|
|
58
57
|
? payload.refs.filter((ref) => typeof ref === 'string')
|
|
59
58
|
: [];
|
|
59
|
+
if (refs.length === 1 && refs[0] === 'ui.hud') {
|
|
60
|
+
// The HUD must stay visible for this capture-bound observation.
|
|
61
|
+
try {
|
|
62
|
+
const hierarchy = await readNativeHierarchy(payload, context, []);
|
|
63
|
+
const expected = String(payload?.node?.intent ?? '').replace(/\s+/gu, ' ').trim();
|
|
64
|
+
const intents = hierarchy.visible.items.flatMap((item) => {
|
|
65
|
+
const label = String(item.label ?? '').replace(/\s+/gu, ' ').trim();
|
|
66
|
+
const intent = hudLabelIntent(label);
|
|
67
|
+
return intent && item.visible !== false && item.bounds?.width > 0 && item.bounds?.height > 0 ? [intent] : [];
|
|
68
|
+
});
|
|
69
|
+
const intent = intents.find((value) => value === expected);
|
|
70
|
+
return { observations: { 'ui.hud': {
|
|
71
|
+
visible: Boolean(expected && intent && hierarchy.visible.truncated !== true),
|
|
72
|
+
intent: intent ?? '', provider: hierarchy.screen.provider,
|
|
73
|
+
} } };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { warnings: [{ ref: 'ui.hud', message: error instanceof Error ? error.message : String(error) }] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await hideAccessibilityMaskingHud(payload, context);
|
|
60
79
|
const supported = refs.filter((ref) => ref === 'ui.screen' || ref === 'ui.visible');
|
|
61
80
|
const warnings = refs
|
|
62
81
|
.filter((ref) => !supported.includes(ref))
|
|
@@ -139,6 +158,10 @@ export async function hideAccessibilityMaskingHud(payload, context) {
|
|
|
139
158
|
}
|
|
140
159
|
}
|
|
141
160
|
|
|
161
|
+
function hudLabelIntent(label) {
|
|
162
|
+
return /^(?:RUN|PASS|FAIL|OK)(?: \d+\/\d+)?\s+(.+)$/u.exec(text(label) ?? '')?.[1];
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
async function readNativeHierarchy(payload, context, textTestIds) {
|
|
143
166
|
const node = record(payload?.node);
|
|
144
167
|
const env = {
|
|
@@ -149,23 +172,24 @@ async function readNativeHierarchy(payload, context, textTestIds) {
|
|
|
149
172
|
env.METAMASK_RECIPE_PROJECT_ROOT = String(context.projectRoot);
|
|
150
173
|
}
|
|
151
174
|
const platform = resolvePlatform(node, env);
|
|
175
|
+
const hudIntent = payload?.refs?.length === 1 && payload.refs[0] === 'ui.hud' ? text(node.intent) : undefined;
|
|
152
176
|
const appLabel = nativeAppLabel(env);
|
|
153
177
|
const hierarchy = platform === 'android'
|
|
154
|
-
? await readAndroidHierarchy(node, env)
|
|
155
|
-
: await readIosHierarchy(node, env, textTestIds);
|
|
178
|
+
? await readAndroidHierarchy(node, env, hudIntent, context?.signal)
|
|
179
|
+
: await readIosHierarchy(node, env, textTestIds, hudIntent, context?.signal);
|
|
156
180
|
// No screen name and no labelled item: name the screen after the app.
|
|
157
181
|
hierarchy.screen.name ??= appLabel;
|
|
158
182
|
return hierarchy;
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
async function readIosHierarchy(node, env, textTestIds) {
|
|
185
|
+
async function readIosHierarchy(node, env, textTestIds, hudIntent, signal) {
|
|
162
186
|
const idbPath = resolveMobileToolPath('idb');
|
|
163
187
|
if (!idbPath) {
|
|
164
188
|
throw new Error(
|
|
165
189
|
`iOS UI observation requires the idb client.\n Next: ${mobileToolRecovery('idb')}`,
|
|
166
190
|
);
|
|
167
191
|
}
|
|
168
|
-
const udid = await resolveIosUdid(node, env);
|
|
192
|
+
const udid = await resolveIosUdid(node, env, signal);
|
|
169
193
|
const { stdout } = await execFileAsync(
|
|
170
194
|
idbPath,
|
|
171
195
|
['ui', 'describe-all', '--udid', udid],
|
|
@@ -174,10 +198,11 @@ async function readIosHierarchy(node, env, textTestIds) {
|
|
|
174
198
|
timeout: 15_000,
|
|
175
199
|
maxBuffer: 16 * 1024 * 1024,
|
|
176
200
|
env,
|
|
201
|
+
signal,
|
|
177
202
|
},
|
|
178
203
|
);
|
|
179
204
|
const parsed = parseJsonOutput(stdout);
|
|
180
|
-
const hierarchy = flattenIosHierarchy(parsed, textTestIds);
|
|
205
|
+
const hierarchy = flattenIosHierarchy(parsed, textTestIds, hudIntent);
|
|
181
206
|
if (hierarchy.nodeCount === 0) {
|
|
182
207
|
throw new Error('idb returned an empty accessibility hierarchy.');
|
|
183
208
|
}
|
|
@@ -191,15 +216,16 @@ async function readIosHierarchy(node, env, textTestIds) {
|
|
|
191
216
|
);
|
|
192
217
|
}
|
|
193
218
|
|
|
194
|
-
async function readAndroidHierarchy(node, env) {
|
|
219
|
+
async function readAndroidHierarchy(node, env, hudIntent, signal) {
|
|
195
220
|
const adbPath = resolveMobileToolPath('adb');
|
|
196
221
|
if (!adbPath) {
|
|
197
222
|
throw new Error(
|
|
198
223
|
`Android UI observation requires Android SDK Platform-Tools.\n Next: ${mobileToolRecovery('adb')}`,
|
|
199
224
|
);
|
|
200
225
|
}
|
|
201
|
-
const serial = await resolveAndroidSerial(adbPath, node, env);
|
|
226
|
+
const serial = await resolveAndroidSerial(adbPath, node, env, signal);
|
|
202
227
|
if (isOpaqueRuntime(env)) {
|
|
228
|
+
if (hudIntent) throw new Error('An opaque runtime has no observable HUD.');
|
|
203
229
|
return readAndroidAgentDeviceHierarchy(serial, env);
|
|
204
230
|
}
|
|
205
231
|
const { stdout } = await execFileAsync(
|
|
@@ -210,10 +236,12 @@ async function readAndroidHierarchy(node, env) {
|
|
|
210
236
|
timeout: 15_000,
|
|
211
237
|
maxBuffer: 16 * 1024 * 1024,
|
|
212
238
|
env,
|
|
239
|
+
signal,
|
|
213
240
|
},
|
|
214
241
|
);
|
|
215
|
-
const hierarchy = parseAndroidHierarchy(stdout);
|
|
242
|
+
const hierarchy = parseAndroidHierarchy(stdout, hudIntent);
|
|
216
243
|
if (hierarchy.nodeCount === 0) {
|
|
244
|
+
if (hudIntent) throw new Error('The native HUD hierarchy is empty.');
|
|
217
245
|
return readAndroidAgentDeviceHierarchy(serial, env);
|
|
218
246
|
}
|
|
219
247
|
return normalizedHierarchy(
|
|
@@ -338,7 +366,7 @@ function normalizedHierarchy(
|
|
|
338
366
|
};
|
|
339
367
|
}
|
|
340
368
|
|
|
341
|
-
async function resolveIosUdid(node, env) {
|
|
369
|
+
async function resolveIosUdid(node, env, signal) {
|
|
342
370
|
const requested = text(node.simulator) ?? text(node.ios_simulator) ??
|
|
343
371
|
text(env.SIM_UDID) ?? text(env.IOS_SIMULATOR) ?? 'booted';
|
|
344
372
|
if (/^[0-9a-f-]{36}$/iu.test(requested)) return requested;
|
|
@@ -350,6 +378,7 @@ async function resolveIosUdid(node, env) {
|
|
|
350
378
|
timeout: 5_000,
|
|
351
379
|
maxBuffer: 4 * 1024 * 1024,
|
|
352
380
|
env,
|
|
381
|
+
signal,
|
|
353
382
|
},
|
|
354
383
|
);
|
|
355
384
|
const parsed = JSON.parse(stdout);
|
|
@@ -368,7 +397,7 @@ async function resolveIosUdid(node, env) {
|
|
|
368
397
|
return udid;
|
|
369
398
|
}
|
|
370
399
|
|
|
371
|
-
async function resolveAndroidSerial(adbPath, node, env) {
|
|
400
|
+
async function resolveAndroidSerial(adbPath, node, env, signal) {
|
|
372
401
|
const explicit = text(node.adb_serial) ?? text(node.android_serial) ??
|
|
373
402
|
text(env.ADB_SERIAL) ?? text(env.ANDROID_SERIAL);
|
|
374
403
|
if (explicit) return explicit;
|
|
@@ -377,6 +406,7 @@ async function resolveAndroidSerial(adbPath, node, env) {
|
|
|
377
406
|
timeout: 5_000,
|
|
378
407
|
maxBuffer: 4 * 1024 * 1024,
|
|
379
408
|
env,
|
|
409
|
+
signal,
|
|
380
410
|
});
|
|
381
411
|
const serials = stdout
|
|
382
412
|
.split(/\r?\n/u)
|
|
@@ -418,7 +448,7 @@ function parseJsonOutput(output) {
|
|
|
418
448
|
}
|
|
419
449
|
}
|
|
420
450
|
|
|
421
|
-
function flattenIosHierarchy(value, textTestIds = []) {
|
|
451
|
+
function flattenIosHierarchy(value, textTestIds = [], hudIntent) {
|
|
422
452
|
const texts = (Array.isArray(textTestIds) ? textTestIds : []).map((entry) => ({
|
|
423
453
|
testId: text(entry?.testId),
|
|
424
454
|
label: text(entry?.label),
|
|
@@ -449,7 +479,7 @@ function flattenIosHierarchy(value, textTestIds = []) {
|
|
|
449
479
|
screenName ??= label;
|
|
450
480
|
screenBounds ??= bounds;
|
|
451
481
|
}
|
|
452
|
-
if (isActionableRole(role) || testId) {
|
|
482
|
+
if (isActionableRole(role) || testId || (hudIntent && hudLabelIntent(label) === hudIntent)) {
|
|
453
483
|
const normalized = compactItem({
|
|
454
484
|
role,
|
|
455
485
|
label,
|
|
@@ -473,7 +503,7 @@ function flattenIosHierarchy(value, textTestIds = []) {
|
|
|
473
503
|
return { visible, hidden, nodeCount, screenName };
|
|
474
504
|
}
|
|
475
505
|
|
|
476
|
-
function parseAndroidHierarchy(output) {
|
|
506
|
+
function parseAndroidHierarchy(output, hudIntent) {
|
|
477
507
|
const xmlStart = Math.max(output.indexOf('<?xml'), output.indexOf('<hierarchy'));
|
|
478
508
|
const xml = xmlStart >= 0 ? output.slice(xmlStart) : output;
|
|
479
509
|
const visible = [];
|
|
@@ -514,7 +544,7 @@ function parseAndroidHierarchy(output) {
|
|
|
514
544
|
isActionableRole(role) ||
|
|
515
545
|
attributes.clickable === 'true' ||
|
|
516
546
|
attributes['long-clickable'] === 'true';
|
|
517
|
-
if (isActionable || item.test_id) {
|
|
547
|
+
if (isActionable || item.test_id || (hudIntent && hudLabelIntent(label) === hudIntent)) {
|
|
518
548
|
if (isVisible) {
|
|
519
549
|
visible.push(item);
|
|
520
550
|
} else {
|
|
@@ -184,10 +184,7 @@
|
|
|
184
184
|
"timeout_ms": 400000,
|
|
185
185
|
"max_detached_growth": 50,
|
|
186
186
|
"intent": "Compare retained detached DOM before and after market-detail navigation churn.",
|
|
187
|
-
"
|
|
188
|
-
"detached-dom"
|
|
189
|
-
],
|
|
190
|
-
"next": "idle-control-market-detail"
|
|
187
|
+
"next": "assert-detached"
|
|
191
188
|
},
|
|
192
189
|
"idle-control-market-detail": {
|
|
193
190
|
"action": "metamask.performance.compare_idle_navigation_memory",
|
|
@@ -199,10 +196,7 @@
|
|
|
199
196
|
"timeout_ms": 400000,
|
|
200
197
|
"max_attributable_listeners_per_cycle": 2,
|
|
201
198
|
"intent": "Subtract equal-duration idle growth from the market-detail navigation slope.",
|
|
202
|
-
"
|
|
203
|
-
"idle-control"
|
|
204
|
-
],
|
|
205
|
-
"next": "return-home"
|
|
199
|
+
"next": "assert-idle-control"
|
|
206
200
|
},
|
|
207
201
|
"return-home": {
|
|
208
202
|
"action": "ui.navigate",
|
|
@@ -213,6 +207,34 @@
|
|
|
213
207
|
"done": {
|
|
214
208
|
"action": "end",
|
|
215
209
|
"status": "pass"
|
|
210
|
+
},
|
|
211
|
+
"assert-detached": {
|
|
212
|
+
"action": "assert_output",
|
|
213
|
+
"source": "detached-market-detail",
|
|
214
|
+
"assert": {
|
|
215
|
+
"path": "$.passed",
|
|
216
|
+
"operator": "eq",
|
|
217
|
+
"value": true
|
|
218
|
+
},
|
|
219
|
+
"proves": [
|
|
220
|
+
"detached-dom"
|
|
221
|
+
],
|
|
222
|
+
"intent": "Verify the observed detached-DOM growth stays within its bound",
|
|
223
|
+
"next": "idle-control-market-detail"
|
|
224
|
+
},
|
|
225
|
+
"assert-idle-control": {
|
|
226
|
+
"action": "assert_output",
|
|
227
|
+
"source": "idle-control-market-detail",
|
|
228
|
+
"assert": {
|
|
229
|
+
"path": "$.passed",
|
|
230
|
+
"operator": "eq",
|
|
231
|
+
"value": true
|
|
232
|
+
},
|
|
233
|
+
"proves": [
|
|
234
|
+
"idle-control"
|
|
235
|
+
],
|
|
236
|
+
"intent": "Verify the observed navigation-attributable growth stays within its bound",
|
|
237
|
+
"next": "return-home"
|
|
216
238
|
}
|
|
217
239
|
}
|
|
218
240
|
}
|
|
@@ -69,11 +69,33 @@
|
|
|
69
69
|
"screen_timeout_ms": "{{params.screen_timeout_ms}}",
|
|
70
70
|
"timeout_ms": "{{params.timeout_ms}}",
|
|
71
71
|
"intent": "Measure post-GC retained-node and listener slopes for the requested screen",
|
|
72
|
-
"next": "
|
|
72
|
+
"next": "assert-node-bound"
|
|
73
73
|
},
|
|
74
74
|
"done": {
|
|
75
75
|
"action": "end",
|
|
76
76
|
"status": "pass"
|
|
77
|
+
},
|
|
78
|
+
"assert-node-bound": {
|
|
79
|
+
"action": "assert_output",
|
|
80
|
+
"source": "measure",
|
|
81
|
+
"assert": {
|
|
82
|
+
"path": "$.passed.nodes",
|
|
83
|
+
"operator": "eq",
|
|
84
|
+
"value": true
|
|
85
|
+
},
|
|
86
|
+
"intent": "Verify the observed retained-node slope satisfies its declared bound",
|
|
87
|
+
"next": "assert-listener-bound"
|
|
88
|
+
},
|
|
89
|
+
"assert-listener-bound": {
|
|
90
|
+
"action": "assert_output",
|
|
91
|
+
"source": "measure",
|
|
92
|
+
"assert": {
|
|
93
|
+
"path": "$.passed.listeners",
|
|
94
|
+
"operator": "eq",
|
|
95
|
+
"value": true
|
|
96
|
+
},
|
|
97
|
+
"intent": "Verify the observed listener slope satisfies its declared bound",
|
|
98
|
+
"next": "done"
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-node": "0.1.
|
|
25
|
-
"@farmslot/adapter-rn": "0.
|
|
26
|
-
"@farmslot/adapter-sdk": "0.
|
|
27
|
-
"@farmslot/adapter-web": "0.
|
|
28
|
-
"@farmslot/agent-runtime": "0.
|
|
24
|
+
"@farmslot/adapter-node": "0.1.13",
|
|
25
|
+
"@farmslot/adapter-rn": "0.20.0",
|
|
26
|
+
"@farmslot/adapter-sdk": "0.9.0",
|
|
27
|
+
"@farmslot/adapter-web": "0.7.0",
|
|
28
|
+
"@farmslot/agent-runtime": "0.18.0",
|
|
29
29
|
"@farmslot/handoff": "^0.3.1",
|
|
30
|
-
"@farmslot/protocol": "0.
|
|
31
|
-
"@farmslot/recipe-cli": "0.
|
|
32
|
-
"@farmslot/recipe-runner": "0.
|
|
30
|
+
"@farmslot/protocol": "0.39.0",
|
|
31
|
+
"@farmslot/recipe-cli": "0.14.1",
|
|
32
|
+
"@farmslot/recipe-runner": "0.30.0",
|
|
33
33
|
"agent-device": "0.19.3",
|
|
34
34
|
"commander": "^12.0.0",
|
|
35
35
|
"esbuild": "0.28.1",
|