@deeeed/metamask-harness 0.72.0 → 0.74.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 +13 -0
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/surface.js +9 -0
- package/dist/adapters/mobile/surface.js +9 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/surface.js +4 -1
- package/dist/commands/launch/device.js +346 -0
- package/dist/commands/launch/extension.js +303 -3
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/mobile.js +190 -2
- package/dist/commands/review.js +3 -1
- package/dist/commands/run-engine.js +3 -66
- package/dist/commands/shared.js +2 -2
- package/dist/discovery.js +15 -0
- package/dist/recipe-assistance-context.js +2 -1
- package/docs/CONTRIBUTING.md +10 -3
- package/docs/cli-contract.md +3 -3
- package/package.json +6 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { prepareMobile } from "../../adapters/mobile/prepare.js";
|
|
2
3
|
import {
|
|
3
4
|
mobileSourceCheck,
|
|
@@ -7,7 +8,11 @@ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
|
7
8
|
import { pinMobileFeatureFlags } from "../../adapters/mobile/feature-flags.js";
|
|
8
9
|
import { toRemoteFlagSpec } from "../../feature-flags.js";
|
|
9
10
|
import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
|
|
10
|
-
import { EXIT } from "../shared.js";
|
|
11
|
+
import { EXIT, str, writeInteractiveProgress } from "../shared.js";
|
|
12
|
+
import { applyDeviceTargeting } from "../device-target.js";
|
|
13
|
+
import { RECOVERY_CODE } from "../../heal-bounds.js";
|
|
14
|
+
import { recipeRuntimePath } from "../../paths.js";
|
|
15
|
+
import { shellQuote } from "./device.js";
|
|
11
16
|
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false, remoteFlags) {
|
|
12
17
|
clearMobileReleaseArtifactState(target);
|
|
13
18
|
await ensureHarnessFresh(target, "mobile");
|
|
@@ -54,6 +59,189 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
|
|
|
54
59
|
}
|
|
55
60
|
return { ...result, remoteFeatureFlags: provenFlags };
|
|
56
61
|
}
|
|
62
|
+
function mobileProvisioningBlocked(output) {
|
|
63
|
+
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
64
|
+
}
|
|
65
|
+
function missingConfiguredIosSimulator(output) {
|
|
66
|
+
return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
|
|
67
|
+
}
|
|
68
|
+
function ambiguousConfiguredIosSimulator(output) {
|
|
69
|
+
return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
|
|
70
|
+
}
|
|
71
|
+
function mobileBridgeTargetMissing(output) {
|
|
72
|
+
return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
|
|
73
|
+
}
|
|
74
|
+
function mobileProvisionCommand(target, mobileTarget) {
|
|
75
|
+
const platform = mobileTarget === "android" ? "android" : "ios";
|
|
76
|
+
const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
|
|
77
|
+
const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
|
|
78
|
+
if (device) parts.push("--device", shellQuote(device));
|
|
79
|
+
return parts.join(" ");
|
|
80
|
+
}
|
|
81
|
+
function isIosSimulatorUdid(value) {
|
|
82
|
+
return /^[0-9A-Fa-f-]{36}$/u.test(value);
|
|
83
|
+
}
|
|
84
|
+
function launchDeviceLabel(mobileTarget) {
|
|
85
|
+
if (mobileTarget === "android") {
|
|
86
|
+
return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
|
|
87
|
+
}
|
|
88
|
+
if (mobileTarget === "ios") {
|
|
89
|
+
return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
|
|
90
|
+
}
|
|
91
|
+
return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
|
|
92
|
+
}
|
|
93
|
+
function metroLaunchEvidence(target) {
|
|
94
|
+
const evidencePath = recipeRuntimePath(target, "metro-generation.json");
|
|
95
|
+
let descriptor;
|
|
96
|
+
try {
|
|
97
|
+
descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
98
|
+
const stat = fs.fstatSync(descriptor);
|
|
99
|
+
if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
|
|
100
|
+
return JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
} finally {
|
|
104
|
+
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const mobileLaunchProfile = {
|
|
108
|
+
refuseFlags({ mobileTarget }) {
|
|
109
|
+
if (!mobileTarget) {
|
|
110
|
+
return { message: "target is required for mobile.", userAction: "mm-harness launch ios or mm-harness launch android" };
|
|
111
|
+
}
|
|
112
|
+
return void 0;
|
|
113
|
+
},
|
|
114
|
+
applyEnv({ adapter, options, mobileTarget, target }) {
|
|
115
|
+
const device = str(options, "device");
|
|
116
|
+
if (!device && mobileTarget !== "ios" && mobileTarget !== "android") return void 0;
|
|
117
|
+
const targetingOptions = {
|
|
118
|
+
...options,
|
|
119
|
+
...mobileTarget ? { platform: mobileTarget } : {}
|
|
120
|
+
};
|
|
121
|
+
const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
|
|
122
|
+
if ("code" in result) {
|
|
123
|
+
if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
|
|
124
|
+
process.env.IOS_SIMULATOR = device;
|
|
125
|
+
process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
|
|
126
|
+
process.env.ADB_SERIAL = "";
|
|
127
|
+
process.env.ANDROID_SERIAL = "";
|
|
128
|
+
process.env.ANDROID_DEVICE = "";
|
|
129
|
+
process.env.ANDROID_TARGET_DEVICE_NAME = "";
|
|
130
|
+
} else {
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
code: "DEVICE_WRONG_PLATFORM",
|
|
138
|
+
message: `--device ${device} did not resolve to an Android device for launch android.`,
|
|
139
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
code: "DEVICE_WRONG_PLATFORM",
|
|
146
|
+
message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
|
|
147
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return void 0;
|
|
151
|
+
},
|
|
152
|
+
start({ options, mobileTarget, machine, tier, wantClearMetro }) {
|
|
153
|
+
const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
|
|
154
|
+
const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
|
|
155
|
+
writeInteractiveProgress(
|
|
156
|
+
machine,
|
|
157
|
+
`\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
|
|
161
|
+
rebuildCommand: ({ mobileTarget }) => `mm-harness launch ${mobileTarget} --build`,
|
|
162
|
+
compose: ({ target, mobileTarget, machine, remoteFlagRequest }, tier, restartApp, clearMetro) => launchMobile(target, mobileTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags),
|
|
163
|
+
classify(_launch, output) {
|
|
164
|
+
if (output.includes("open-device: Android device is locked.")) {
|
|
165
|
+
return {
|
|
166
|
+
code: "MOBILE_DEVICE_LOCKED",
|
|
167
|
+
message: "the selected Android device is locked.",
|
|
168
|
+
recoverable: false,
|
|
169
|
+
userAction: "Unlock the selected Android device, then retry the launch.",
|
|
170
|
+
exitCode: EXIT.runtime,
|
|
171
|
+
originalError: output.trim()
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (/open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(output)) {
|
|
175
|
+
return {
|
|
176
|
+
code: "MOBILE_DEVICE_UI_UNAVAILABLE",
|
|
177
|
+
message: "the selected Android display could not be prepared.",
|
|
178
|
+
recoverable: false,
|
|
179
|
+
userAction: "Check adb access and the selected device screen, then retry the launch.",
|
|
180
|
+
exitCode: EXIT.runtime,
|
|
181
|
+
originalError: output.trim()
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return void 0;
|
|
185
|
+
},
|
|
186
|
+
async beforeHeal(launch, attempt, state) {
|
|
187
|
+
const { target, mobileTarget, tier } = launch;
|
|
188
|
+
const output = attempt.output;
|
|
189
|
+
const missingIosSimulator = missingConfiguredIosSimulator(output);
|
|
190
|
+
const ambiguousIosSimulator = ambiguousConfiguredIosSimulator(output);
|
|
191
|
+
if (ambiguousIosSimulator) {
|
|
192
|
+
return launch.fail(state, {
|
|
193
|
+
code: "MOBILE_DEVICE_AMBIGUOUS",
|
|
194
|
+
message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
|
|
195
|
+
recoverable: false,
|
|
196
|
+
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
197
|
+
exitCode: EXIT.runtime,
|
|
198
|
+
originalError: output.trim() || void 0
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
|
|
202
|
+
return launch.fail(state, {
|
|
203
|
+
code: "MOBILE_DEVICE_NOT_FOUND",
|
|
204
|
+
message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
|
|
205
|
+
recoverable: false,
|
|
206
|
+
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
207
|
+
exitCode: EXIT.runtime,
|
|
208
|
+
originalError: output.trim() || void 0
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (mobileProvisioningBlocked(output)) {
|
|
212
|
+
return launch.fail(state, {
|
|
213
|
+
code: "MOBILE_PROVISION_REQUIRED",
|
|
214
|
+
message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
|
|
215
|
+
recoverable: false,
|
|
216
|
+
userAction: mobileProvisionCommand(target, mobileTarget),
|
|
217
|
+
exitCode: EXIT.runtime,
|
|
218
|
+
originalError: output.trim() || void 0
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(output)) {
|
|
222
|
+
return launch.fail(state, {
|
|
223
|
+
code: "MOBILE_NATIVE_BUILD_FAILED",
|
|
224
|
+
message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
|
|
225
|
+
recoverable: false,
|
|
226
|
+
exitCode: EXIT.runtime,
|
|
227
|
+
originalError: output.trim() || void 0
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return void 0;
|
|
231
|
+
},
|
|
232
|
+
recovery(_launch, output) {
|
|
233
|
+
const restartApp = mobileBridgeTargetMissing(output);
|
|
234
|
+
return { code: restartApp ? "mobile.app-restarted" : RECOVERY_CODE.mobile, restartApp };
|
|
235
|
+
},
|
|
236
|
+
// Mobile proved its set inside launchMobile (null: nothing requested and no
|
|
237
|
+
// runtime answered).
|
|
238
|
+
provenFlags: (_launch, requested, proven) => ({ flags: proven === void 0 ? requested : proven }),
|
|
239
|
+
verifyArgs: ({ mobileTarget }) => mobileTarget ? ["--platform", mobileTarget] : [],
|
|
240
|
+
platformLabel: ({ mobileTarget }) => mobileTarget ?? null,
|
|
241
|
+
deviceLabel: ({ mobileTarget }) => launchDeviceLabel(mobileTarget),
|
|
242
|
+
evidence: metroLaunchEvidence
|
|
243
|
+
};
|
|
57
244
|
export {
|
|
58
|
-
launchMobile
|
|
245
|
+
launchMobile,
|
|
246
|
+
mobileLaunchProfile
|
|
59
247
|
};
|
package/dist/commands/review.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
parityAdapter,
|
|
9
9
|
readConfig,
|
|
10
10
|
referenceHint,
|
|
11
|
+
REVIEW_ADAPTERS,
|
|
11
12
|
renderReviewChecklist,
|
|
12
13
|
renderReviewHelp,
|
|
13
14
|
resolveLibrary,
|
|
@@ -30,7 +31,8 @@ function adapterFromOptions(options, target) {
|
|
|
30
31
|
if (raw !== "mobile" && raw !== "extension" && raw !== "core" && raw !== "terminal") throw usageError(`--adapter must be mobile, extension, core, or terminal (got ${raw}).`);
|
|
31
32
|
return raw;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
const detected = detectAdapter(target);
|
|
35
|
+
return REVIEW_ADAPTERS.find((adapter) => adapter === detected) ?? "unscoped";
|
|
34
36
|
}
|
|
35
37
|
function resolveReviewContext(options, { fetch = true } = {}) {
|
|
36
38
|
const target = targetPath(options);
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
validateMetaMaskActionInputs
|
|
31
31
|
} from "../metamask-action-validation.js";
|
|
32
32
|
import { gitLibraryProvenance } from "../library-provenance.js";
|
|
33
|
+
import { runnableLibraryRecipes } from "../discovery.js";
|
|
34
|
+
import { recipeComposition } from "@farmslot/recipe-cli";
|
|
33
35
|
import {
|
|
34
36
|
captureExecutionProvenance,
|
|
35
37
|
executionProvenanceDrift,
|
|
@@ -710,35 +712,7 @@ function effectiveLibrarySources(librarySources) {
|
|
|
710
712
|
return librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: path.resolve(runnerDir, "library") }];
|
|
711
713
|
}
|
|
712
714
|
async function listRunnableRecipes(adapter, librarySources) {
|
|
713
|
-
|
|
714
|
-
const sources = effectiveLibrarySources(librarySources);
|
|
715
|
-
const resolution = await harness.loadRecipeLibraries(
|
|
716
|
-
sources,
|
|
717
|
-
{
|
|
718
|
-
adapter
|
|
719
|
-
}
|
|
720
|
-
);
|
|
721
|
-
const protocol = await importRecipeProtocol();
|
|
722
|
-
const { manifest } = await resolveActionManifest(adapter, void 0, sources);
|
|
723
|
-
const externalRecipeIds = new Set(resolution.recipes.keys());
|
|
724
|
-
const validRefs = new Set([...resolution.recipes.values()].filter((recipe) => protocol.validateRecipeWithManifest(recipe.document, manifest, {
|
|
725
|
-
externalRecipeIds
|
|
726
|
-
}).status === "valid").map((recipe) => recipe.ref));
|
|
727
|
-
return [...resolution.recipes.values()].filter((recipe) => {
|
|
728
|
-
if (!validRefs.has(recipe.ref)) return false;
|
|
729
|
-
try {
|
|
730
|
-
const dependencies = harness.resolveRecipeDependencies({
|
|
731
|
-
rootRef: recipe.ref,
|
|
732
|
-
root: recipe.document,
|
|
733
|
-
rootSource: recipe.provenance,
|
|
734
|
-
recipes: resolution.recipes
|
|
735
|
-
});
|
|
736
|
-
return [...dependencies.recipes.keys()].every((ref) => validRefs.has(ref));
|
|
737
|
-
} catch (error) {
|
|
738
|
-
if (error instanceof harness.RecipeResolutionError) return false;
|
|
739
|
-
throw error;
|
|
740
|
-
}
|
|
741
|
-
}).map(
|
|
715
|
+
return (await runnableLibraryRecipes(adapter, effectiveLibrarySources(librarySources))).map(
|
|
742
716
|
(recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
|
|
743
717
|
).sort((left, right) => left.name.localeCompare(right.name));
|
|
744
718
|
}
|
|
@@ -822,42 +796,6 @@ function recipeParameters(document) {
|
|
|
822
796
|
};
|
|
823
797
|
});
|
|
824
798
|
}
|
|
825
|
-
function recipeComposition(recipe, recipes) {
|
|
826
|
-
const actions = /* @__PURE__ */ new Set();
|
|
827
|
-
const nestedRecipes = /* @__PURE__ */ new Set();
|
|
828
|
-
const unresolvedRecipes = /* @__PURE__ */ new Set();
|
|
829
|
-
const visitedRecipes = /* @__PURE__ */ new Set();
|
|
830
|
-
const visitNode = (value) => {
|
|
831
|
-
if (!isRecord(value) || typeof value.action !== "string") return;
|
|
832
|
-
if (value.action !== "call") {
|
|
833
|
-
actions.add(value.action);
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
if (typeof value.ref !== "string") return;
|
|
837
|
-
nestedRecipes.add(value.ref);
|
|
838
|
-
const dependency = recipes.get(value.ref);
|
|
839
|
-
if (!dependency) {
|
|
840
|
-
unresolvedRecipes.add(value.ref);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (visitedRecipes.has(value.ref)) return;
|
|
844
|
-
visitedRecipes.add(value.ref);
|
|
845
|
-
visitRecipe(dependency.document);
|
|
846
|
-
};
|
|
847
|
-
const visitWorkflow = (value) => {
|
|
848
|
-
if (!isRecord(value)) return;
|
|
849
|
-
if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
|
|
850
|
-
};
|
|
851
|
-
const visitRecipe = (document) => {
|
|
852
|
-
visitWorkflow(document.workflow);
|
|
853
|
-
};
|
|
854
|
-
visitRecipe(recipe);
|
|
855
|
-
return {
|
|
856
|
-
actions: [...actions].sort(),
|
|
857
|
-
nestedRecipes: [...nestedRecipes].sort(),
|
|
858
|
-
unresolvedRecipes: [...unresolvedRecipes].sort()
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
799
|
async function validateRunRecipeStatic(recipeArg, adapter, options, params = {}) {
|
|
862
800
|
let librarySources = await resolveMetaMaskLibrarySources(
|
|
863
801
|
optionStrings(options, "library")
|
|
@@ -1492,7 +1430,6 @@ export {
|
|
|
1492
1430
|
preflightRecipe,
|
|
1493
1431
|
prepareHeal,
|
|
1494
1432
|
prepareRuntimeIfNeeded,
|
|
1495
|
-
recipeComposition,
|
|
1496
1433
|
resolveMetaMaskLibrarySources,
|
|
1497
1434
|
resolveRunRecipeArg,
|
|
1498
1435
|
runOneNode,
|
package/dist/commands/shared.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { detectAdapter } from "../harness.js";
|
|
8
8
|
import { colorHumanMessage } from "../cli-color.js";
|
|
9
9
|
import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
|
|
10
|
+
import { adapterRegistry } from "../adapters/registry.js";
|
|
10
11
|
const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
|
|
11
12
|
function writeInteractiveProgress(json, message, {
|
|
12
13
|
stdoutIsTTY = Boolean(process.stdout.isTTY),
|
|
@@ -59,11 +60,10 @@ function flag(options, key) {
|
|
|
59
60
|
function targetOf(options) {
|
|
60
61
|
return path.resolve(str(options, "target") ?? process.cwd());
|
|
61
62
|
}
|
|
62
|
-
const ADAPTER_TOKENS = ["mobile", "extension", "core", "terminal"];
|
|
63
63
|
function resolveAdapter(options, target, hint) {
|
|
64
64
|
const platform = str(options, "platform");
|
|
65
65
|
const explicit = str(options, "adapter") ?? (platform === "ios" || platform === "android" ? "mobile" : platform);
|
|
66
|
-
if (explicit &&
|
|
66
|
+
if (explicit && adapterRegistry.has(explicit)) return explicit;
|
|
67
67
|
if (hint) return hint;
|
|
68
68
|
return detectAdapter(target);
|
|
69
69
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { assessRecipe } from "@farmslot/recipe-cli";
|
|
2
|
+
import {
|
|
3
|
+
loadRecipeLibraries
|
|
4
|
+
} from "@farmslot/recipe-harness";
|
|
5
|
+
import { resolveActionManifest } from "./manifest.js";
|
|
6
|
+
async function runnableLibraryRecipes(adapter, sources) {
|
|
7
|
+
const resolution = await loadRecipeLibraries(sources, { adapter });
|
|
8
|
+
const { manifest } = await resolveActionManifest(adapter, void 0, sources);
|
|
9
|
+
return [...resolution.recipes.values()].filter(
|
|
10
|
+
(recipe) => assessRecipe({ resolution, manifest }, recipe).length === 0
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
runnableLibraryRecipes
|
|
15
|
+
};
|
|
@@ -4,7 +4,8 @@ import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } fro
|
|
|
4
4
|
import { isSensitiveKey } from "./command-journal.js";
|
|
5
5
|
import { resolveActionManifest } from "./manifest.js";
|
|
6
6
|
import { describeManifestActions } from "./commands/manifest.js";
|
|
7
|
-
import { recipeComposition
|
|
7
|
+
import { recipeComposition } from "@farmslot/recipe-cli";
|
|
8
|
+
import { resolveMetaMaskLibrarySources, validateRunRecipeStatic } from "./commands/run-engine.js";
|
|
8
9
|
import { CliError, isRecord, optionStrings } from "./commands/parse-args.js";
|
|
9
10
|
import { EXIT } from "./commands/shared.js";
|
|
10
11
|
import { MAX_ADVICE_STATE_BYTES } from "./recipe-assistance-rubric.js";
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -12,6 +12,7 @@ bin/mm-harness
|
|
|
12
12
|
-> src/ typed CLI and product decisions
|
|
13
13
|
-> @farmslot/agent-runtime checklist state
|
|
14
14
|
-> @farmslot/handoff scrubbed learning capture/share
|
|
15
|
+
-> @farmslot/recipe-cli recipe discovery: libraries, runnable checks, composition
|
|
15
16
|
-> @farmslot/recipe-harness generic execution, UI transports, evidence
|
|
16
17
|
-> @farmslot/protocol schemas
|
|
17
18
|
-> adapters/ focused host/browser/device leaves
|
|
@@ -24,6 +25,7 @@ bin/mm-harness
|
|
|
24
25
|
| `@farmslot/protocol` | recipe and evidence schemas |
|
|
25
26
|
| `@farmslot/agent-runtime` | task-local checklist state and terminal contracts |
|
|
26
27
|
| `@farmslot/handoff` | learning assembly, scrubbing, validation, approval, and publication |
|
|
28
|
+
| `@farmslot/recipe-cli` | recipe discovery: library resolution, runnable checks, composition |
|
|
27
29
|
| `@farmslot/recipe-harness` | generic execution, recovery, traces, artifacts, `ui.*` |
|
|
28
30
|
| `mm-harness` | MetaMask runtime control, diagnostics, durable domain actions |
|
|
29
31
|
| skills/checklists | task workflow and proof expectations |
|
|
@@ -59,9 +61,14 @@ exits.
|
|
|
59
61
|
## Adapter surface
|
|
60
62
|
|
|
61
63
|
Commands resolve shared platform behavior through
|
|
62
|
-
`src/adapters/surface.ts`.
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
`src/adapters/surface.ts`. Each platform's surface registers itself in
|
|
65
|
+
`src/adapters/registry.ts` when its module loads; a new platform registers the
|
|
66
|
+
same way and needs no edit to shared code. Extend `AdapterSurface` only when all
|
|
67
|
+
platforms need the concept, implement it in every adapter, and let Core report
|
|
68
|
+
headless/N/A explicitly. Do not add command-local platform ladders for
|
|
69
|
+
surface-owned behavior: `launch` already dispatches through
|
|
70
|
+
`AdapterSurface.launch`, and the device platforms supply a `DeviceLaunchProfile`
|
|
71
|
+
(`src/commands/launch/device.ts`).
|
|
65
72
|
|
|
66
73
|
Each checkout owns:
|
|
67
74
|
|
package/docs/cli-contract.md
CHANGED
|
@@ -122,10 +122,10 @@ Each case runs under a safety cap, `GOLDENS_CASE_TIMEOUT` (default 600 s). A cas
|
|
|
122
122
|
Each family is a normal contract test (`tests/contract/goldens-<family>.test.sh`). `gd_init` (`tests/contract/goldens/lib.sh`) builds a sandbox on `ct_init`, and every case runs under `env -i`:
|
|
123
123
|
- `gd_init` first unsets every inherited `MM_HARNESS_*`, `RECIPE_*`, `METAMASK_*` and `FARMSLOT_*` variable, plus device and port pins (`IOS_SIMULATOR`, `ADB_SERIAL`, `WATCHER_PORT`, `METRO_PORT`, `CDP_PORT`, `IDB_PATH`, `ANDROID_HOME`, …). A developer's `MM_HARNESS_BIN` or `MM_HARNESS_IDB_PATH` export therefore cannot redirect a case;
|
|
124
124
|
- the environment is an allowlist: sandbox `HOME`, caches, config, `FARMSLOT_HOME`, `TZ=UTC`, no update probe, and only the variables the family sets itself;
|
|
125
|
-
- every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `
|
|
125
|
+
- every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `git` (`git --exec-path`), `bash` (`$BASH`) and `timeout`. The goldens need no Python. A tool that resolves to a script, the shape of an asdf/mise/volta/pyenv shim, fails the family up front with a message naming it. Each is checked to run with a sandboxed `HOME`;
|
|
126
126
|
- `PATH` is pinned to the case stubs, the sandbox stubs, those real binaries, and `/usr/bin:/bin:/usr/sbin:/sbin`. Host tools (`idb`, `capture-helper`, a developer's Chrome) cannot change a golden;
|
|
127
127
|
- device-tool discovery is pinned: `MM_HARNESS_IDB_PATH` and `MM_HARNESS_ADB_PATH` point at sandbox stubs, which discovery checks before any absolute Homebrew or pipx path;
|
|
128
|
-
- localhost probes cannot reach a real server. `gd_reserve_ports`
|
|
128
|
+
- localhost probes cannot reach a real server. `gd_reserve_ports` runs a Node port holder (`tests/contract/goldens/port-holder.mjs`) that holds ephemeral ports on `127.0.0.1` and `::1` and resets every connection it accepts, so Metro `/status` and CDP probes never get a response and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, `:<port>`, `port <n>`/`PORT=<n>`, or a string that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
|
|
129
129
|
- `tmux`, `xcrun` and `adb` are stubbed (`ct_init`, plus one booted simulator and one Android device for Mobile);
|
|
130
130
|
- forbidden stubs record any call to `open`, `osascript`, `lsappinfo`, `screencapture`, `yarn`, `npx`, `npm`, `corepack`, `gh` and Chrome/Chromium, and the family fails if one is called. A browser stub answers only `--version`, and `npm` only `root -g` (install-source detection), with a sandbox path;
|
|
131
131
|
- inert stubs on the case PATH only: `lsof`, `pgrep` and `pkill` find nothing, `ps` passes per-pid queries (`-p`, the CLI identifying its own processes) and lists an empty host for table listings, and `watchman` and `caffeinate` are no-ops. `stop` cannot see or signal a real process or touch a host daemon;
|
|
@@ -149,7 +149,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
|
|
|
149
149
|
|
|
150
150
|
`tests/contract/goldens/normalize.mjs` is the only normaliser. It rewrites only volatile values:
|
|
151
151
|
|
|
152
|
-
1. Absolute paths: the sandbox becomes `<SANDBOX>`, the repo `<REPO>`, the real `$HOME` `<HOME>`, the node binary and its prefix `<NODE>`/`<NODE_PREFIX>`, and `os.tmpdir()` `<TMP>`. Both `/var` and `/private/var` spellings are covered. Ports from `gd_reserve_ports` become `<PORT:NAME>` in port positions only: numbers under `*port` keys, and in text after `:`, `port `, `port=` or `PORT=`, or a
|
|
152
|
+
1. Absolute paths: the sandbox becomes `<SANDBOX>`, the repo `<REPO>`, the real `$HOME` `<HOME>`, the node binary and its prefix `<NODE>`/`<NODE_PREFIX>`, and `os.tmpdir()` `<TMP>`. Both `/var` and `/private/var` spellings are covered. Ports from `gd_reserve_ports` become `<PORT:NAME>` in port positions only: numbers under `*port` keys, and in text after `:`, `port `, `port=` or `PORT=`, or a string that is exactly the port (`^<port>$`; `60000 ms` is left alone).
|
|
153
153
|
2. UUIDs become `<UUID>`. Git object ids (40 hex, or a short hex value under a `*head`/`*gitRef`/`*commit` key) become `<GIT_REF>`. ISO-8601 timestamps become `<TIMESTAMP>`, and compact stamps in names `<STAMP>`. Path-derived slot ids `local-<adapter>-<8 hex>` become `local-<adapter>-<HASH>`.
|
|
154
154
|
3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
|
|
155
155
|
4. Numbers under volatile keys: `*pid` becomes `<PID>`, `*port` `<PORT>`, `*At`/`*Time`/`*timestamp`/`mtime*` `<TIMESTAMP>`, and `*Ms`/`duration*`/`elapsed*` `<DURATION>`. Values inside `schema` and `examples` subtrees (static manifest content) are never rewritten.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/agent-runtime": "0.
|
|
25
|
-
"@farmslot/expo-recipe": "0.
|
|
24
|
+
"@farmslot/agent-runtime": "0.16.0",
|
|
25
|
+
"@farmslot/expo-recipe": "0.13.1",
|
|
26
26
|
"@farmslot/handoff": "^0.3.1",
|
|
27
|
-
"@farmslot/protocol": "0.
|
|
28
|
-
"@farmslot/recipe-
|
|
27
|
+
"@farmslot/protocol": "0.34.0",
|
|
28
|
+
"@farmslot/recipe-cli": "0.1.1",
|
|
29
|
+
"@farmslot/recipe-harness": "0.22.1",
|
|
29
30
|
"agent-device": "0.19.3",
|
|
30
31
|
"commander": "^12.0.0",
|
|
31
32
|
"es-module-lexer": "2.3.1",
|