@deeeed/metamask-harness 0.5.0 → 0.6.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 +51 -20
- package/adapters/core/inject.sh +1 -0
- package/adapters/extension/cleanup.mjs +1 -1
- package/adapters/extension/inject.mjs +4 -1
- package/adapters/extension/launch-browser.cjs +18 -4
- package/adapters/extension/live.sh +14 -2
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/sidepanel-toggle.sh +33 -12
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/wallet-fixture-state.cjs +38 -24
- package/adapters/manifest.json +32 -3
- package/adapters/mobile/inject.sh +1 -0
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/stop-metro.sh +20 -0
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +44 -25
- package/adapters/shared/ensure-runner-deps.sh +30 -0
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/reap-checkout-metros.sh +53 -0
- package/adapters/shared/recipe-harness-root.mjs +23 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- package/bin/mm-harness +45 -4
- package/dist/adapters/extension/runtime.js +3 -1
- package/dist/adapters/mobile/provision.js +34 -1
- package/dist/adapters/mobile/runtime-decision.js +3 -1
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +18 -29
- package/dist/cli.js +32 -1463
- package/dist/commands/call.js +183 -0
- package/dist/commands/completion-candidates.js +58 -0
- package/dist/commands/doctor.js +101 -0
- package/dist/commands/ensure-ready.js +24 -0
- package/dist/commands/flows.js +62 -0
- package/dist/commands/launch/extension.js +40 -0
- package/dist/commands/{launch.js → launch/index.js} +15 -47
- package/dist/commands/launch/mobile.js +10 -0
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/parse-args.js +189 -0
- package/dist/commands/provision.js +136 -0
- package/dist/commands/resolve-extension.js +23 -0
- package/dist/commands/run-engine.js +341 -0
- package/dist/commands/run.js +217 -0
- package/dist/commands/runtime-decision.js +58 -0
- package/dist/commands/runtime-health.js +25 -0
- package/dist/commands/runtime-launch.js +139 -0
- package/dist/commands/self-test.js +52 -0
- package/dist/commands/stop.js +52 -0
- package/dist/harness.js +8 -48
- package/dist/mm-harness-cli.js +13 -8
- package/docs/CLI-SPEC.md +1 -1
- package/docs/CODE-MAP.md +62 -0
- package/library/README.md +14 -0
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -2
- package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +6 -7
- package/src/adapters/core/surface.ts +0 -71
- package/src/adapters/extension/ensure-ready.ts +0 -185
- package/src/adapters/extension/extension-id.ts +0 -107
- package/src/adapters/extension/runtime-decision.ts +0 -445
- package/src/adapters/extension/runtime.ts +0 -407
- package/src/adapters/extension/surface.ts +0 -88
- package/src/adapters/mobile/deps-markers.ts +0 -21
- package/src/adapters/mobile/prepare.ts +0 -246
- package/src/adapters/mobile/provision.ts +0 -594
- package/src/adapters/mobile/runtime-decision.ts +0 -459
- package/src/adapters/mobile/surface.ts +0 -71
- package/src/adapters/slot-ports.ts +0 -165
- package/src/adapters/surface.ts +0 -117
- package/src/adapters.ts +0 -601
- package/src/cli-color.ts +0 -92
- package/src/cli-commands.ts +0 -250
- package/src/cli-version.ts +0 -141
- package/src/cli.ts +0 -2091
- package/src/commands/debug.ts +0 -65
- package/src/commands/fixtures.ts +0 -198
- package/src/commands/launch.ts +0 -470
- package/src/commands/logs.ts +0 -99
- package/src/commands/shared.ts +0 -235
- package/src/commands/update.ts +0 -316
- package/src/completions-cache.ts +0 -86
- package/src/doctor.ts +0 -215
- package/src/harness.ts +0 -797
- package/src/heal-bounds.ts +0 -198
- package/src/index.ts +0 -15
- package/src/leaf-invoke.ts +0 -28
- package/src/live-adapter-contract.ts +0 -274
- package/src/manifest.ts +0 -47
- package/src/mm-harness-cli.ts +0 -655
- package/src/paths.ts +0 -198
- package/src/progress.ts +0 -117
- package/src/recording-target.ts +0 -147
- package/src/run-recording.ts +0 -329
- package/src/runner.ts +0 -108
- package/src/types.ts +0 -57
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { walletFixturePath } from "../paths.js";
|
|
4
|
+
import { EXIT } from "./shared.js";
|
|
5
|
+
import {
|
|
6
|
+
adapterOption,
|
|
7
|
+
optionFlag,
|
|
8
|
+
optionString,
|
|
9
|
+
requiredOption,
|
|
10
|
+
resolveAdapter,
|
|
11
|
+
runtimeOptionsFromCli,
|
|
12
|
+
targetPath,
|
|
13
|
+
usageError
|
|
14
|
+
} from "./parse-args.js";
|
|
15
|
+
import {
|
|
16
|
+
countRecipeNodes,
|
|
17
|
+
emitHealViolation,
|
|
18
|
+
executeWithHealBounds,
|
|
19
|
+
prepareHeal,
|
|
20
|
+
resolveMetaMaskLibrarySources,
|
|
21
|
+
runRecipe,
|
|
22
|
+
validateRunRecipeStatic
|
|
23
|
+
} from "./run-engine.js";
|
|
24
|
+
async function handleRun({ positional, options }) {
|
|
25
|
+
const targetRecipe = positional[0];
|
|
26
|
+
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
27
|
+
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
28
|
+
const adapter = adapterOption(options);
|
|
29
|
+
const json = optionFlag(options, "json");
|
|
30
|
+
const target = targetPath(options);
|
|
31
|
+
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
32
|
+
const prepared = await prepareHeal(adapter, target, options, json);
|
|
33
|
+
if (typeof prepared === "number") return prepared;
|
|
34
|
+
const { state, heal } = prepared;
|
|
35
|
+
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
36
|
+
if (validated.usageError) {
|
|
37
|
+
return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
38
|
+
}
|
|
39
|
+
if (validated.errorCount > 0) {
|
|
40
|
+
return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
41
|
+
}
|
|
42
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
43
|
+
const runtimeOptions = {
|
|
44
|
+
...runtimeOptionsFromCli(options),
|
|
45
|
+
...librarySources ? { librarySources } : {},
|
|
46
|
+
stdoutIsMachineContract: json
|
|
47
|
+
};
|
|
48
|
+
const { result, violation } = await executeWithHealBounds(
|
|
49
|
+
() => runRecipe(adapter, targetRecipe, artifactsDir, target, optionString(options, "actionManifest"), runtimeOptions),
|
|
50
|
+
adapter,
|
|
51
|
+
target,
|
|
52
|
+
heal,
|
|
53
|
+
state
|
|
54
|
+
);
|
|
55
|
+
if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
|
|
56
|
+
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(
|
|
59
|
+
JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
command: "run",
|
|
63
|
+
adapter,
|
|
64
|
+
status: result.status,
|
|
65
|
+
exitCode,
|
|
66
|
+
recovered: state.recovered,
|
|
67
|
+
mutations: state.mutations,
|
|
68
|
+
result
|
|
69
|
+
},
|
|
70
|
+
null,
|
|
71
|
+
2
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
console.log(`MetaMask recipe run: ${result.status}
|
|
76
|
+
Artifacts: ${result.artifactManifestPath}`);
|
|
77
|
+
}
|
|
78
|
+
return exitCode;
|
|
79
|
+
}
|
|
80
|
+
async function handleRunPlan(recipeArg, options) {
|
|
81
|
+
const json = optionFlag(options, "json");
|
|
82
|
+
const { adapter, target } = resolveAdapter(options);
|
|
83
|
+
const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
|
|
84
|
+
if (validated.usageError) {
|
|
85
|
+
return emitPlanUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
86
|
+
}
|
|
87
|
+
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
|
|
88
|
+
const status = errorCount === 0 ? "pass" : "fail";
|
|
89
|
+
const nodeCount = countRecipeNodes(recipe);
|
|
90
|
+
const artifactsDir = optionString(options, "artifactsDir");
|
|
91
|
+
const plan = [
|
|
92
|
+
{ step: "resolve.recipe", confidence: "static", status: "ok", detail: recipeFile },
|
|
93
|
+
{ step: "resolve.adapter", confidence: "static", status: "ok", detail: adapter },
|
|
94
|
+
{
|
|
95
|
+
step: "resolve.artifactsDir",
|
|
96
|
+
confidence: "static",
|
|
97
|
+
status: "ok",
|
|
98
|
+
detail: artifactsDir ? path.resolve(artifactsDir) : "(resolved to the slot artifacts dir at run time)"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
step: "validate.manifest",
|
|
102
|
+
confidence: "static",
|
|
103
|
+
status: manifestOk ? "ok" : "error",
|
|
104
|
+
detail: manifestOk ? "action manifest is well-formed" : "action manifest failed validation"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
step: "validate.schema",
|
|
108
|
+
confidence: "static",
|
|
109
|
+
status: manifestOk && schemaValid ? "ok" : "error",
|
|
110
|
+
detail: "recipe document schema + action existence/platform vs the adapter manifest"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
step: "fixture.file",
|
|
114
|
+
confidence: "static",
|
|
115
|
+
status: "ok",
|
|
116
|
+
detail: fs.existsSync(walletFixturePath(target)) ? `present: ${walletFixturePath(target)}` : `absent: ${walletFixturePath(target)} (fixtures set to seed)`
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
step: "overlay.ensure",
|
|
120
|
+
confidence: "conditional",
|
|
121
|
+
status: "planned",
|
|
122
|
+
detail: "would auto-ensure the runtime overlay if missing (install phase)"
|
|
123
|
+
},
|
|
124
|
+
...adapter === "core" ? [] : [
|
|
125
|
+
{
|
|
126
|
+
step: "launch.app",
|
|
127
|
+
confidence: "conditional",
|
|
128
|
+
status: "planned",
|
|
129
|
+
detail: "would launch/attach the app + heal transport (Metro/Chrome/CDP) before executing"
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
{
|
|
133
|
+
step: "execute.nodes",
|
|
134
|
+
confidence: "conditional",
|
|
135
|
+
status: "planned",
|
|
136
|
+
detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
if (json) {
|
|
140
|
+
const payload = { schemaVersion: 1, command: "run", mode: "plan", status, adapter, recipe: recipeFile, findings, plan };
|
|
141
|
+
if (status === "fail") payload.error = { code: "RECIPE_VALIDATION_FAILED", message: `recipe validation found ${errorCount} error(s)` };
|
|
142
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
|
|
145
|
+
for (const item of plan) {
|
|
146
|
+
const mark = item.status === "error" ? "\u2717" : item.status === "ok" ? "\u2713" : "\xB7";
|
|
147
|
+
console.log(` ${mark} [${item.confidence}] ${item.step}: ${item.detail}`);
|
|
148
|
+
}
|
|
149
|
+
if (findings.length) {
|
|
150
|
+
console.log("findings:");
|
|
151
|
+
for (const finding of findings) {
|
|
152
|
+
console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return status === "pass" ? EXIT.ok : EXIT.validation;
|
|
157
|
+
}
|
|
158
|
+
function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
159
|
+
if (json) {
|
|
160
|
+
console.log(
|
|
161
|
+
JSON.stringify(
|
|
162
|
+
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
|
|
163
|
+
null,
|
|
164
|
+
2
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
console.error(`\u2717 run --plan: ${message}`);
|
|
169
|
+
}
|
|
170
|
+
return EXIT.usage;
|
|
171
|
+
}
|
|
172
|
+
function emitRunUsageError(json, adapter, recipeFile, code, message) {
|
|
173
|
+
if (json) {
|
|
174
|
+
console.log(
|
|
175
|
+
JSON.stringify(
|
|
176
|
+
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message } },
|
|
177
|
+
null,
|
|
178
|
+
2
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
console.error(`\u2717 run: ${message}`);
|
|
183
|
+
}
|
|
184
|
+
return EXIT.usage;
|
|
185
|
+
}
|
|
186
|
+
function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
|
|
187
|
+
const message = `recipe validation found ${errorCount} error(s)`;
|
|
188
|
+
if (json) {
|
|
189
|
+
console.log(
|
|
190
|
+
JSON.stringify(
|
|
191
|
+
{
|
|
192
|
+
schemaVersion: 1,
|
|
193
|
+
command: "run",
|
|
194
|
+
adapter,
|
|
195
|
+
status: "fail",
|
|
196
|
+
exitCode: EXIT.validation,
|
|
197
|
+
recovered: [],
|
|
198
|
+
mutations: [],
|
|
199
|
+
recipe: recipeFile,
|
|
200
|
+
findings,
|
|
201
|
+
error: { code: "RECIPE_VALIDATION_FAILED", message }
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`\u2717 run: ${message}`);
|
|
209
|
+
for (const finding of findings) {
|
|
210
|
+
if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return EXIT.validation;
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
handleRun
|
|
217
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { decideExtensionReadiness } from "../adapters/extension/runtime-decision.js";
|
|
2
|
+
import { decideMobileReadiness } from "../adapters/mobile/runtime-decision.js";
|
|
3
|
+
import {
|
|
4
|
+
adapterOption,
|
|
5
|
+
applyRuntimeDirOption,
|
|
6
|
+
optionFlag,
|
|
7
|
+
optionString,
|
|
8
|
+
parsePort,
|
|
9
|
+
targetPath
|
|
10
|
+
} from "./parse-args.js";
|
|
11
|
+
async function handleRuntimeDecision({ options }) {
|
|
12
|
+
applyRuntimeDirOption(options);
|
|
13
|
+
const adapter = adapterOption(options);
|
|
14
|
+
const target = targetPath(options);
|
|
15
|
+
if (adapter === "mobile") {
|
|
16
|
+
const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
|
|
17
|
+
const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
|
|
18
|
+
const report2 = await decideMobileReadiness(target, {
|
|
19
|
+
watcherPort,
|
|
20
|
+
metroLog: optionString(options, "metroLog"),
|
|
21
|
+
platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
|
|
22
|
+
record: optionFlag(options, "record"),
|
|
23
|
+
preflightMode: optionString(options, "preflightMode")
|
|
24
|
+
});
|
|
25
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
|
|
26
|
+
else console.log(`${report2.decision} ${report2.reasonCode} \u2014 ${report2.reasons[0] ?? ""}`);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (adapter !== "extension") {
|
|
30
|
+
const report2 = {
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
adapter,
|
|
33
|
+
target,
|
|
34
|
+
decision: "unknown",
|
|
35
|
+
clean: false,
|
|
36
|
+
reasonCode: "adapter-unsupported",
|
|
37
|
+
reasons: [`runtime-decision applies to extension and mobile adapters, not ${adapter}.`],
|
|
38
|
+
checks: {},
|
|
39
|
+
actions: []
|
|
40
|
+
};
|
|
41
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
|
|
42
|
+
else console.log(`unknown adapter-unsupported \u2014 ${report2.reasons[0]}`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
46
|
+
const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
|
|
47
|
+
const report = await decideExtensionReadiness(target, {
|
|
48
|
+
cdpPort,
|
|
49
|
+
watchLog: optionString(options, "watchLog"),
|
|
50
|
+
record: optionFlag(options, "record")
|
|
51
|
+
});
|
|
52
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
|
|
53
|
+
else console.log(`${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
handleRuntimeDecision
|
|
58
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adapterOption,
|
|
3
|
+
optionFlag,
|
|
4
|
+
optionString,
|
|
5
|
+
parsePort,
|
|
6
|
+
targetPath
|
|
7
|
+
} from "./parse-args.js";
|
|
8
|
+
async function handleRuntimeHealth({ options }) {
|
|
9
|
+
const adapter = adapterOption(options);
|
|
10
|
+
if (adapter !== "extension") throw new Error("runtime-health currently applies to the extension adapter.");
|
|
11
|
+
const target = targetPath(options);
|
|
12
|
+
const cdpPort = parsePort(
|
|
13
|
+
optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
|
|
14
|
+
"runtime-health requires --cdp-port <port>."
|
|
15
|
+
);
|
|
16
|
+
const { checkExtensionRuntimeHealth, formatHealthFailure } = await import("../adapters/extension/runtime.js");
|
|
17
|
+
const report = await checkExtensionRuntimeHealth(target, cdpPort);
|
|
18
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
|
|
19
|
+
else if (report.status === "PASS") console.log(`PASS extension runtime cdp=${cdpPort} target=${report.targetUrl}`);
|
|
20
|
+
else console.error(formatHealthFailure(report, target));
|
|
21
|
+
return report.status === "PASS" ? 0 : 1;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
handleRuntimeHealth
|
|
25
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { recipeHarnessPath } from "../paths.js";
|
|
5
|
+
import {
|
|
6
|
+
adapterOption,
|
|
7
|
+
isRecord,
|
|
8
|
+
optionFlag,
|
|
9
|
+
optionString,
|
|
10
|
+
parsePort,
|
|
11
|
+
shellQuote,
|
|
12
|
+
targetPath
|
|
13
|
+
} from "./parse-args.js";
|
|
14
|
+
async function handleRuntimeLaunch({ options }) {
|
|
15
|
+
const adapter = adapterOption(options);
|
|
16
|
+
const target = targetPath(options);
|
|
17
|
+
if (adapter !== "extension") throw new Error("runtime-launch currently applies to the extension adapter.");
|
|
18
|
+
const cdpPort = parsePort(
|
|
19
|
+
optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
|
|
20
|
+
"runtime-launch requires --cdp-port <port>."
|
|
21
|
+
);
|
|
22
|
+
const chromeUserDataDir = optionString(options, "chromeUserDataDir");
|
|
23
|
+
const artifactsDir = path.resolve(
|
|
24
|
+
optionString(options, "artifactsDir") ?? recipeHarnessPath(target, "extension", "runtime-launch", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, ""))
|
|
25
|
+
);
|
|
26
|
+
const startWatch = optionFlag(options, "startWatch");
|
|
27
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
28
|
+
const command = [
|
|
29
|
+
"bash",
|
|
30
|
+
liveScript,
|
|
31
|
+
"--target",
|
|
32
|
+
target,
|
|
33
|
+
"--cdp-port",
|
|
34
|
+
String(cdpPort),
|
|
35
|
+
startWatch ? "--start-watch" : "--launch-existing-dist",
|
|
36
|
+
"--artifacts-dir",
|
|
37
|
+
artifactsDir
|
|
38
|
+
];
|
|
39
|
+
if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
|
|
40
|
+
const remoteFlag = optionString(options, "remoteFlag");
|
|
41
|
+
if (remoteFlag) command.push("--remote-flag", remoteFlag);
|
|
42
|
+
if (!fs.existsSync(liveScript)) {
|
|
43
|
+
const report2 = runtimeLaunchReport("fail", {
|
|
44
|
+
adapter,
|
|
45
|
+
target,
|
|
46
|
+
cdpPort,
|
|
47
|
+
artifactsDir,
|
|
48
|
+
reason: "harness_live_script_missing",
|
|
49
|
+
fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
|
|
50
|
+
command
|
|
51
|
+
});
|
|
52
|
+
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
56
|
+
const jsonMode = optionFlag(options, "json");
|
|
57
|
+
if (!jsonMode) {
|
|
58
|
+
console.error(`recipe: runtime-launch starting (cdp=${cdpPort}${startWatch ? ", clean webpack build" : ", existing dist"})`);
|
|
59
|
+
} else {
|
|
60
|
+
console.error(JSON.stringify({
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
type: "progress",
|
|
63
|
+
command: "rebuild",
|
|
64
|
+
phase: "runtime-launch",
|
|
65
|
+
message: startWatch ? "clean webpack build starting" : "launching existing dist",
|
|
66
|
+
cdpPort
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
70
|
+
cwd: target,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
73
|
+
stdio: jsonMode ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
74
|
+
});
|
|
75
|
+
const summaryPath = path.join(artifactsDir, "summary.json");
|
|
76
|
+
const summary = readJsonIfExists(summaryPath);
|
|
77
|
+
if (result.status === 0 && isRecord(summary) && summary.status === "pass") {
|
|
78
|
+
const report2 = runtimeLaunchReport("pass", {
|
|
79
|
+
adapter,
|
|
80
|
+
target,
|
|
81
|
+
cdpPort,
|
|
82
|
+
artifactsDir,
|
|
83
|
+
summaryPath,
|
|
84
|
+
reason: "runtime_ready",
|
|
85
|
+
fix: "",
|
|
86
|
+
command
|
|
87
|
+
});
|
|
88
|
+
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const launchLogPath = path.join(artifactsDir, "launch", "logs", "launch.log");
|
|
92
|
+
const report = runtimeLaunchReport("fail", {
|
|
93
|
+
adapter,
|
|
94
|
+
target,
|
|
95
|
+
cdpPort,
|
|
96
|
+
artifactsDir,
|
|
97
|
+
summaryPath: fs.existsSync(summaryPath) ? summaryPath : void 0,
|
|
98
|
+
launchLogPath: fs.existsSync(launchLogPath) ? launchLogPath : void 0,
|
|
99
|
+
reason: "runtime_launch_failed",
|
|
100
|
+
fix: `Read ${fs.existsSync(launchLogPath) ? launchLogPath : summaryPath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, startWatch)}`,
|
|
101
|
+
command,
|
|
102
|
+
exitCode: result.status ?? 1
|
|
103
|
+
});
|
|
104
|
+
printRuntimeLaunchReport(report, optionFlag(options, "json"));
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, startWatch = false) {
|
|
108
|
+
const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
|
|
109
|
+
const withMode = startWatch ? `${base} --start-watch` : base;
|
|
110
|
+
return chromeUserDataDir ? `${withMode} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : withMode;
|
|
111
|
+
}
|
|
112
|
+
function runtimeLaunchReport(status, fields) {
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
status,
|
|
116
|
+
...fields
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function printRuntimeLaunchReport(report, json) {
|
|
120
|
+
if (json) {
|
|
121
|
+
console.log(JSON.stringify(report, null, 2));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const status = String(report.status).toUpperCase();
|
|
125
|
+
console.log(`${status} runtime-launch ${report.reason}`);
|
|
126
|
+
if (report.status === "fail") console.log(`Fix: ${report.fix}`);
|
|
127
|
+
console.log(`Artifacts: ${report.artifactsDir}`);
|
|
128
|
+
}
|
|
129
|
+
function readJsonIfExists(file) {
|
|
130
|
+
if (!fs.existsSync(file)) return null;
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
handleRuntimeLaunch
|
|
139
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdtemp } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
5
|
+
import { recipePath, runnerDir } from "../paths.js";
|
|
6
|
+
import {
|
|
7
|
+
optionFlag,
|
|
8
|
+
optionString,
|
|
9
|
+
requiredOption
|
|
10
|
+
} from "./parse-args.js";
|
|
11
|
+
import { runRecipe } from "./run-engine.js";
|
|
12
|
+
async function handleSelfTest({ options }) {
|
|
13
|
+
const result = await runSelfTest(options);
|
|
14
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
|
|
15
|
+
else console.log(`MetaMask runner self-test: ${result.status}
|
|
16
|
+
Artifacts: ${result.artifactsDir}`);
|
|
17
|
+
return result.status === "pass" ? 0 : 1;
|
|
18
|
+
}
|
|
19
|
+
async function runSelfTest(options) {
|
|
20
|
+
const root = optionString(options, "artifactsDir") ? path.resolve(requiredOption(options, "artifactsDir", "self-test artifacts dir missing.")) : await mkdtemp(path.join(os.tmpdir(), "mm-harness-runner-"));
|
|
21
|
+
const previousAutoHud = process.env.METAMASK_RECIPE_AUTO_HUD;
|
|
22
|
+
const runs = [];
|
|
23
|
+
try {
|
|
24
|
+
process.env.METAMASK_RECIPE_AUTO_HUD = "0";
|
|
25
|
+
for (const adapter of ["mobile", "extension"]) {
|
|
26
|
+
const manifest = loadActionManifest(adapter);
|
|
27
|
+
const manifestValidation = await validateManifest(manifest);
|
|
28
|
+
const smokeRecipe = recipePath(
|
|
29
|
+
adapter === "mobile" ? "smoke.mobile.recipe.json" : "smoke.extension.recipe.json"
|
|
30
|
+
);
|
|
31
|
+
const artifactsDir = path.join(root, adapter);
|
|
32
|
+
const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
|
|
33
|
+
skipExtensionRuntimePrepare: true
|
|
34
|
+
});
|
|
35
|
+
runs.push({ adapter, manifestValidation: manifestValidation.summary, artifactsDir, result });
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
restoreEnv("METAMASK_RECIPE_AUTO_HUD", previousAutoHud);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
status: runs.every((run) => run.result.status === "pass") ? "pass" : "fail",
|
|
42
|
+
artifactsDir: root,
|
|
43
|
+
runs
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function restoreEnv(key, value) {
|
|
47
|
+
if (value === void 0) delete process.env[key];
|
|
48
|
+
else process.env[key] = value;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
handleSelfTest
|
|
52
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { color } from "../cli-color.js";
|
|
2
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
3
|
+
import { usageOut } from "./shared.js";
|
|
4
|
+
import {
|
|
5
|
+
parseArgs,
|
|
6
|
+
optionFlag,
|
|
7
|
+
optionString,
|
|
8
|
+
resolveAdapter
|
|
9
|
+
} from "./parse-args.js";
|
|
10
|
+
async function handleStop(argv) {
|
|
11
|
+
const { options } = parseArgs(argv, "stop");
|
|
12
|
+
const json = optionFlag(options, "json");
|
|
13
|
+
const { adapter, target } = resolveAdapter(options);
|
|
14
|
+
const surface = getAdapterSurface(adapter);
|
|
15
|
+
const explicitPort = optionString(options, "port") ?? optionString(options, "watcherPort");
|
|
16
|
+
if (explicitPort) {
|
|
17
|
+
process.env.WATCHER_PORT = explicitPort;
|
|
18
|
+
process.env.METRO_PORT = explicitPort;
|
|
19
|
+
} else {
|
|
20
|
+
surface.resolveSlotPorts(target);
|
|
21
|
+
}
|
|
22
|
+
const stop = surface.devServer.stop(target);
|
|
23
|
+
if (stop.kind === "headless") {
|
|
24
|
+
return usageOut(json, "stop", stop.message, stop.userAction);
|
|
25
|
+
}
|
|
26
|
+
if (json) {
|
|
27
|
+
console.log(
|
|
28
|
+
JSON.stringify(
|
|
29
|
+
{
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
command: "stop",
|
|
32
|
+
adapter,
|
|
33
|
+
target,
|
|
34
|
+
status: stop.status === 0 ? "pass" : "fail",
|
|
35
|
+
...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
|
|
36
|
+
exitCode: stop.status,
|
|
37
|
+
...stop.output ? { output: stop.output } : {}
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
if (stop.output) process.stderr.write(`${stop.output}
|
|
45
|
+
`);
|
|
46
|
+
console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
|
|
47
|
+
}
|
|
48
|
+
return stop.status;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
handleStop
|
|
52
|
+
};
|
package/dist/harness.js
CHANGED
|
@@ -4,10 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
5
5
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
6
6
|
import { prepareMobile } from "./adapters/mobile/prepare.js";
|
|
7
|
-
const HARNESS_ACTIONS = ["install", "
|
|
7
|
+
const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
|
|
8
8
|
const ADAPTERS = ["mobile", "extension", "core"];
|
|
9
9
|
function isValidAdapterAction(adapter, action) {
|
|
10
|
-
if (adapter === "core") return action === "install" || action === "
|
|
10
|
+
if (adapter === "core") return action === "install" || action === "verify" || action === "cleanup";
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
13
|
function harnessUsage() {
|
|
@@ -19,8 +19,6 @@ is auto-detected from the repo. Pass --platform only to override.
|
|
|
19
19
|
Commands (one copy-pasteable example each):
|
|
20
20
|
install Install the recipe harness runtime overlay into the checkout.
|
|
21
21
|
mm-harness install
|
|
22
|
-
provision Install the cached Runway mobile dev client (same path as install --runway).
|
|
23
|
-
mm-harness provision runway ios --adapter mobile
|
|
24
22
|
verify Check the harness/runtime is present and healthy (no app launch).
|
|
25
23
|
mm-harness verify
|
|
26
24
|
cleanup Remove the installed harness overlay and restore the checkout.
|
|
@@ -404,11 +402,8 @@ async function handleHarness(argv) {
|
|
|
404
402
|
}
|
|
405
403
|
const autoDetected = parsedAdapter === void 0;
|
|
406
404
|
let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
|
|
407
|
-
if (harnessAction === "provision") {
|
|
408
|
-
return handleRunwayInstall(adapter, target, provisionRunwayForward(forward), json, "provision");
|
|
409
|
-
}
|
|
410
405
|
if (harnessAction === "install" && hasArg(forward, "--runway")) {
|
|
411
|
-
return handleRunwayInstall(adapter, target, forward, json
|
|
406
|
+
return handleRunwayInstall(adapter, target, forward, json);
|
|
412
407
|
}
|
|
413
408
|
if (adapter === "extension" && (harnessAction === "live" || harnessAction === "verify")) {
|
|
414
409
|
forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
|
|
@@ -501,9 +496,9 @@ async function handleHarness(argv) {
|
|
|
501
496
|
}
|
|
502
497
|
return exitCode;
|
|
503
498
|
}
|
|
504
|
-
async function handleRunwayInstall(adapter, target, forward, json
|
|
499
|
+
async function handleRunwayInstall(adapter, target, forward, json) {
|
|
505
500
|
const { getAdapterSurface } = await import("./adapters/surface.js");
|
|
506
|
-
const rerunCommand =
|
|
501
|
+
const rerunCommand = runwayInstallRerunCommand(adapter, target, forward, json);
|
|
507
502
|
const surface = getAdapterSurface(adapter);
|
|
508
503
|
const runtimeDir = argValue(forward, "--runtime-dir");
|
|
509
504
|
const watcherPort = argValue(forward, "--watcher-port");
|
|
@@ -533,26 +528,15 @@ async function handleRunwayInstall(adapter, target, forward, json, action) {
|
|
|
533
528
|
if (result.resolveOnly) {
|
|
534
529
|
console.error(`\u2713 resolved Runway app for ${adapter} ${result.platform ?? ""} run=${artifact?.runId ?? "unknown"} revision=${artifact?.revision ?? "unknown"} artifact=${artifact?.artifactName ?? "unknown"}`);
|
|
535
530
|
} else {
|
|
536
|
-
const
|
|
537
|
-
console.error(`\u2713 ${
|
|
531
|
+
const action = result.skipped ? "already provisioned" : "installed Runway app";
|
|
532
|
+
console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
|
|
538
533
|
}
|
|
539
534
|
} else {
|
|
540
|
-
|
|
541
|
-
console.error(`\u2717 mm-harness ${label}: ${result.error?.message ?? "runway install failed"}
|
|
535
|
+
console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
|
|
542
536
|
Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
543
537
|
}
|
|
544
538
|
return result.exitCode;
|
|
545
539
|
}
|
|
546
|
-
function provisionRunwayForward(forward) {
|
|
547
|
-
const normalized = [];
|
|
548
|
-
let index = 0;
|
|
549
|
-
if (forward[index] === "runway") index += 1;
|
|
550
|
-
if (forward[index] && !forward[index].startsWith("-")) {
|
|
551
|
-
if (!hasArg(forward, "--platform")) normalized.push("--platform", forward[index]);
|
|
552
|
-
index += 1;
|
|
553
|
-
}
|
|
554
|
-
return [...normalized, ...forward.slice(index)];
|
|
555
|
-
}
|
|
556
540
|
function runwayInstallRerunCommand(adapter, target, forward, json) {
|
|
557
541
|
const parts = ["mm-harness", "install", "--runway", "--adapter", adapter, "--target", shellQuote(target)];
|
|
558
542
|
const valueFlags = [
|
|
@@ -578,30 +562,6 @@ function runwayInstallRerunCommand(adapter, target, forward, json) {
|
|
|
578
562
|
if (json) parts.push("--json");
|
|
579
563
|
return parts.join(" ");
|
|
580
564
|
}
|
|
581
|
-
function runwayProvisionRerunCommand(adapter, target, forward, json) {
|
|
582
|
-
const parts = ["mm-harness", "provision", "runway", shellQuote(argValue(forward, "--platform") ?? "ios"), "--adapter", adapter, "--target", shellQuote(target)];
|
|
583
|
-
const valueFlags = [
|
|
584
|
-
"--branch",
|
|
585
|
-
"--default-branch",
|
|
586
|
-
"--run",
|
|
587
|
-
"--cache-root",
|
|
588
|
-
"--simulator",
|
|
589
|
-
"--device",
|
|
590
|
-
"--slot",
|
|
591
|
-
"--watcher-port",
|
|
592
|
-
"--runtime-dir",
|
|
593
|
-
"--runtime",
|
|
594
|
-
"--device-type"
|
|
595
|
-
];
|
|
596
|
-
for (const flag of valueFlags) {
|
|
597
|
-
const value = argValue(forward, flag);
|
|
598
|
-
if (value) parts.push(flag, shellQuote(value));
|
|
599
|
-
}
|
|
600
|
-
if (hasArg(forward, "--force")) parts.push("--force");
|
|
601
|
-
if (hasArg(forward, "--resolve-only")) parts.push("--resolve-only");
|
|
602
|
-
if (json) parts.push("--json");
|
|
603
|
-
return parts.join(" ");
|
|
604
|
-
}
|
|
605
565
|
function harnessSummary(action, adapter, target, status, exitCode, autoDetected, error) {
|
|
606
566
|
return JSON.stringify({
|
|
607
567
|
schemaVersion: 1,
|