@deeeed/metamask-harness 0.73.0 → 0.75.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 +22 -0
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +8 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +17 -1
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +19 -1
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/launch/device.js +342 -0
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/pin.js +226 -0
- package/dist/commands/review.js +3 -1
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/commands/shared.js +2 -2
- package/dist/doctor.js +3 -1
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +27 -2
- package/dist/network-observation.js +10 -3
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/docs/CONTRIBUTING.md +20 -3
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
package/dist/cli-commands.js
CHANGED
|
@@ -19,6 +19,7 @@ const SPEC = {
|
|
|
19
19
|
{ name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
|
|
20
20
|
{ name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
|
|
21
21
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
22
|
+
{ name: "pin", desc: "Copy this install into an immutable store for a task to run", flags: ["--json"] },
|
|
22
23
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
23
24
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
24
25
|
{ name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
|
package/dist/command-contract.js
CHANGED
|
@@ -398,6 +398,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
398
398
|
update: {
|
|
399
399
|
options: options(HELP, JSON, { "--check": bool() })
|
|
400
400
|
},
|
|
401
|
+
pin: {
|
|
402
|
+
options: options(HELP, JSON)
|
|
403
|
+
},
|
|
401
404
|
fixtures: {
|
|
402
405
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
|
|
403
406
|
"--from": value(),
|
package/dist/commands/call.js
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
29
29
|
import { handleListExecutables } from "./list-executables.js";
|
|
30
30
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
31
|
-
import { coreDependencyBlock } from "./core-readiness.js";
|
|
32
31
|
import {
|
|
33
32
|
emitHealViolation,
|
|
34
33
|
executeWithHealBounds,
|
|
@@ -40,7 +39,6 @@ import {
|
|
|
40
39
|
synthesizeOneNodeRecipe,
|
|
41
40
|
validateRecipeAdapterAware
|
|
42
41
|
} from "./run-engine.js";
|
|
43
|
-
import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
44
42
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
45
43
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
46
44
|
import {
|
|
@@ -178,7 +176,7 @@ async function handleCall(argv) {
|
|
|
178
176
|
const resolvedAction = resolution.resolved;
|
|
179
177
|
const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
|
|
180
178
|
const defaultsUsed = actionDefaultsUsed(describedAction, args);
|
|
181
|
-
const depsBlock = adapter
|
|
179
|
+
const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, { action: resolvedAction }) ?? null;
|
|
182
180
|
if (depsBlock) {
|
|
183
181
|
if (json) {
|
|
184
182
|
console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, status: "fail", exitCode: EXIT.usage, error: depsBlock }, null, 2));
|
|
@@ -286,9 +284,8 @@ async function handleCall(argv) {
|
|
|
286
284
|
return checkoutBusyOut(json, "call", lock.message, lock.path);
|
|
287
285
|
}
|
|
288
286
|
try {
|
|
289
|
-
const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
|
|
290
287
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
291
|
-
|
|
288
|
+
appRestartAuthored: resolvedAction === "app.lifecycle" && args.command === "restart"
|
|
292
289
|
});
|
|
293
290
|
if (typeof prepared === "number") return prepared;
|
|
294
291
|
const { state } = prepared;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { color, colorHumanMessage } from "../../cli-color.js";
|
|
4
|
+
import { handleHarness } from "../../harness.js";
|
|
5
|
+
import { recipeRuntimePath, walletFixturePath } from "../../paths.js";
|
|
6
|
+
import { getAdapterSurface } from "../../adapters/surface.js";
|
|
7
|
+
import { EXIT, flag, str } from "../shared.js";
|
|
8
|
+
import {
|
|
9
|
+
checkHealBounds,
|
|
10
|
+
ensureOverlay,
|
|
11
|
+
newHealState
|
|
12
|
+
} from "../../heal-bounds.js";
|
|
13
|
+
import {
|
|
14
|
+
FeatureFlagSpecError,
|
|
15
|
+
fixtureFeatureFlags,
|
|
16
|
+
parseRemoteFlagSpec,
|
|
17
|
+
remoteFlagRequestFor
|
|
18
|
+
} from "../../feature-flags.js";
|
|
19
|
+
import { terminalOnlyLaunchFlags } from "../../adapters/terminal/launch.js";
|
|
20
|
+
import { shellQuote } from "./quote.js";
|
|
21
|
+
async function runDeviceLaunch(context, profile) {
|
|
22
|
+
const { adapter, options, target, heal, jsonOutput, machine, stream, usage } = context;
|
|
23
|
+
const terminalOnly = terminalOnlyLaunchFlags(options);
|
|
24
|
+
if (terminalOnly.length > 0) {
|
|
25
|
+
return usage(
|
|
26
|
+
`${terminalOnly.join(", ")} apply only to --adapter terminal; ${adapter} does not read them.`,
|
|
27
|
+
"drop the terminal flags, or pass --adapter terminal"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const wantBuild = flag(options, "build");
|
|
31
|
+
const wantVerify = flag(options, "verify");
|
|
32
|
+
const wantWatch = flag(options, "watch");
|
|
33
|
+
const wantRunway = flag(options, "runway");
|
|
34
|
+
const wantClearMetro = flag(options, "clearMetro");
|
|
35
|
+
const requestedSurface = str(options, "surface");
|
|
36
|
+
const legacySidepanel = flag(options, "sidepanel");
|
|
37
|
+
const legacyFullscreen = flag(options, "fullscreen");
|
|
38
|
+
const remoteFlagSpec = str(options, "remoteFlag");
|
|
39
|
+
const refused = profile.refuseFlags(context, { watch: wantWatch, runway: wantRunway, clearMetro: wantClearMetro, remoteFlagSpec });
|
|
40
|
+
if (refused) return usage(refused.message, refused.userAction);
|
|
41
|
+
if (requestedSurface !== void 0 && requestedSurface !== "fullscreen" && requestedSurface !== "sidepanel") {
|
|
42
|
+
return usage(
|
|
43
|
+
`--surface must be fullscreen or sidepanel (got: ${requestedSurface}).`,
|
|
44
|
+
"pass --surface fullscreen or --surface sidepanel"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (legacySidepanel && legacyFullscreen) {
|
|
48
|
+
return usage("--sidepanel and --fullscreen conflict.", "pass exactly one Extension surface");
|
|
49
|
+
}
|
|
50
|
+
const legacySurface = legacySidepanel ? "sidepanel" : legacyFullscreen ? "fullscreen" : void 0;
|
|
51
|
+
if (requestedSurface && legacySurface && requestedSurface !== legacySurface) {
|
|
52
|
+
return usage(
|
|
53
|
+
`--surface ${requestedSurface} conflicts with --${legacySurface}.`,
|
|
54
|
+
"pass one consistent Extension surface"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const displayMode = requestedSurface ?? legacySurface ?? "fullscreen";
|
|
58
|
+
let remoteFlagRequest;
|
|
59
|
+
try {
|
|
60
|
+
remoteFlagRequest = remoteFlagRequestFor({
|
|
61
|
+
...fixtureFeatureFlags(walletFixturePath(target)),
|
|
62
|
+
...remoteFlagSpec === void 0 ? {} : parseRemoteFlagSpec(remoteFlagSpec)
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!(error instanceof FeatureFlagSpecError)) throw error;
|
|
66
|
+
return usage(error.message, error.userAction);
|
|
67
|
+
}
|
|
68
|
+
for (const portFlag of ["cdpPort", "watcherPort"]) {
|
|
69
|
+
const value = str(options, portFlag);
|
|
70
|
+
if (value !== void 0 && !/^\d+$/u.test(value)) {
|
|
71
|
+
return usage(
|
|
72
|
+
`--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
|
|
73
|
+
`pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
78
|
+
const envError = profile.applyEnv?.(context);
|
|
79
|
+
if (envError) return usage(envError.message, envError.userAction);
|
|
80
|
+
const cdpPort = str(options, "cdpPort");
|
|
81
|
+
if (cdpPort) {
|
|
82
|
+
process.env.CDP_PORT = cdpPort;
|
|
83
|
+
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
84
|
+
}
|
|
85
|
+
const watcherPort = str(options, "watcherPort");
|
|
86
|
+
if (watcherPort) {
|
|
87
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
88
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
89
|
+
process.env.METRO_PORT = watcherPort;
|
|
90
|
+
}
|
|
91
|
+
const launch = {
|
|
92
|
+
...context,
|
|
93
|
+
tier: wantBuild ? "build" : "quick",
|
|
94
|
+
wantWatch,
|
|
95
|
+
wantVerify,
|
|
96
|
+
wantClearMetro,
|
|
97
|
+
displayMode,
|
|
98
|
+
remoteFlagRequest,
|
|
99
|
+
compose: (tier2, restartApp = false, clearMetro = false) => profile.compose(launch, tier2, restartApp, clearMetro),
|
|
100
|
+
settle: (state2, provenFlags, sidepanelRecoveryAllowed = true) => settleLaunch(launch, profile, state2, sidepanelRecoveryAllowed, provenFlags),
|
|
101
|
+
fail: (state2, failure) => launchFail(launch, profile, state2, failure)
|
|
102
|
+
};
|
|
103
|
+
const { tier } = launch;
|
|
104
|
+
profile.start(launch);
|
|
105
|
+
const state = newHealState();
|
|
106
|
+
if (tier === "quick" && nativeInputsChanged(target, profile.nativeInputs)) {
|
|
107
|
+
return usage("native build inputs changed since last build.", profile.rebuildCommand(launch));
|
|
108
|
+
}
|
|
109
|
+
stream.phase("install");
|
|
110
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, machine);
|
|
111
|
+
if (!ensured.ok) {
|
|
112
|
+
return launch.fail(state, {
|
|
113
|
+
code: "OVERLAY_INSTALL_FAILED",
|
|
114
|
+
message: ensured.error ?? "overlay install failed",
|
|
115
|
+
recoverable: false,
|
|
116
|
+
exitCode: EXIT.infra
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
stream.phase("healthcheck");
|
|
120
|
+
const blocked = await profile.healthcheck?.(launch, state);
|
|
121
|
+
if (blocked !== void 0) return blocked;
|
|
122
|
+
stream.phase("launch");
|
|
123
|
+
let attempt = await launch.compose(tier, false, wantClearMetro);
|
|
124
|
+
if (attempt.status === 0) {
|
|
125
|
+
return await launch.settle(state, attempt.remoteFeatureFlags);
|
|
126
|
+
}
|
|
127
|
+
const readinessFailure = readinessBlockFailure(attempt);
|
|
128
|
+
if (readinessFailure) return launch.fail(state, readinessFailure);
|
|
129
|
+
const classified = profile.classify(launch, attempt.output);
|
|
130
|
+
if (classified) return launch.fail(state, classified);
|
|
131
|
+
if (heal === "off") {
|
|
132
|
+
return launch.fail(state, {
|
|
133
|
+
code: "LAUNCH_FAILED",
|
|
134
|
+
message: `launch ${adapter}${launch.mobileTarget ? ` ${launch.mobileTarget}` : ""} failed (healing off)`,
|
|
135
|
+
recoverable: false,
|
|
136
|
+
exitCode: EXIT.infra
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const handled = await profile.beforeHeal?.(launch, attempt, state);
|
|
140
|
+
if (handled !== void 0) return handled;
|
|
141
|
+
const bound = checkHealBounds(target, attempt.output, state);
|
|
142
|
+
if (bound !== null) {
|
|
143
|
+
return launch.fail(state, {
|
|
144
|
+
code: bound.code,
|
|
145
|
+
message: bound.message,
|
|
146
|
+
recoverable: false,
|
|
147
|
+
userAction: bound.userAction,
|
|
148
|
+
exitCode: bound.exitCode,
|
|
149
|
+
originalError: bound.originalError
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const recovery = profile.recovery(launch, attempt.output);
|
|
153
|
+
state.attemptedRecoveries.push(recovery.code);
|
|
154
|
+
stream.phase("recover");
|
|
155
|
+
const recoveryTier = recovery.restartApp && tier === "build" ? "quick" : tier;
|
|
156
|
+
if (!jsonOutput) {
|
|
157
|
+
process.stderr.write(`launch: recovering (${recovery.code}) with the ${recoveryTier} tier
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
attempt = await launch.compose(recoveryTier, recovery.restartApp, false);
|
|
161
|
+
if (attempt.status === 0) {
|
|
162
|
+
state.recovered.push(recovery.code);
|
|
163
|
+
return await launch.settle(state, attempt.remoteFeatureFlags);
|
|
164
|
+
}
|
|
165
|
+
const retryReadinessFailure = readinessBlockFailure(attempt);
|
|
166
|
+
if (retryReadinessFailure) return launch.fail(state, retryReadinessFailure);
|
|
167
|
+
return launch.fail(state, {
|
|
168
|
+
code: "SAME_RECOVERY_TWICE",
|
|
169
|
+
message: `launch ${adapter}${launch.mobileTarget ? ` ${launch.mobileTarget}` : ""} failed again after ${recovery.code} recovery \u2014 refusing to loop.`,
|
|
170
|
+
recoverable: false,
|
|
171
|
+
exitCode: EXIT.bounded,
|
|
172
|
+
originalError: attempt.output.trim() || void 0
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function readinessBlockFailure(result) {
|
|
176
|
+
const block = result.readinessBlock;
|
|
177
|
+
if (!block) return void 0;
|
|
178
|
+
return {
|
|
179
|
+
code: block.reasonCode === "deps-not-ready" ? "MOBILE_DEPENDENCIES_NOT_READY" : "MOBILE_READINESS_BLOCKED",
|
|
180
|
+
message: block.message,
|
|
181
|
+
userAction: block.userAction,
|
|
182
|
+
recoverable: false,
|
|
183
|
+
exitCode: result.status
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function nativeInputsChanged(target, paths) {
|
|
187
|
+
const baselineFile = recipeRuntimePath(target, ".last-build-ref");
|
|
188
|
+
let baseline;
|
|
189
|
+
try {
|
|
190
|
+
baseline = fs.readFileSync(baselineFile, "utf8").trim();
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (!baseline) return false;
|
|
195
|
+
try {
|
|
196
|
+
const diff = execFileSync("git", ["-C", target, "diff", "--name-only", baseline, "--", ...paths], {
|
|
197
|
+
encoding: "utf8",
|
|
198
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
199
|
+
}).trim();
|
|
200
|
+
return diff.length > 0;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function settleLaunch(launch, profile, state, sidepanelRecoveryAllowed, provenFlags) {
|
|
206
|
+
const proven = profile.provenFlags(launch, launch.remoteFlagRequest?.flags ?? {}, provenFlags);
|
|
207
|
+
if (!("flags" in proven)) return launch.fail(state, proven);
|
|
208
|
+
const stopped = await profile.finish?.(launch, state, sidepanelRecoveryAllowed);
|
|
209
|
+
if (stopped !== void 0) return stopped;
|
|
210
|
+
const { adapter, target, stream, tier } = launch;
|
|
211
|
+
if (launch.wantVerify) {
|
|
212
|
+
stream.phase("verify");
|
|
213
|
+
const verifyArgs = ["verify", "--adapter", adapter, "--target", target, ...profile.verifyArgs(launch)];
|
|
214
|
+
if (launch.machine) verifyArgs.push("--json");
|
|
215
|
+
const exitCode = await handleHarness(verifyArgs);
|
|
216
|
+
if (stream.enabled) {
|
|
217
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
218
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
219
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
|
|
220
|
+
adapter,
|
|
221
|
+
platform: profile.platformLabel(launch),
|
|
222
|
+
tier,
|
|
223
|
+
recovered: state.recovered,
|
|
224
|
+
mutations: state.mutations,
|
|
225
|
+
metro: profile.evidence?.(target),
|
|
226
|
+
remoteFeatureFlags: proven.flags
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return exitCode;
|
|
230
|
+
}
|
|
231
|
+
return launchPass(launch, profile, state, proven.flags);
|
|
232
|
+
}
|
|
233
|
+
function launchPass(launch, profile, state, remoteFlags) {
|
|
234
|
+
const { adapter, mobileTarget, target, stream, tier } = launch;
|
|
235
|
+
if (stream.enabled) {
|
|
236
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
237
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
238
|
+
stream.complete("pass", EXIT.ok, {
|
|
239
|
+
adapter,
|
|
240
|
+
platform: profile.platformLabel(launch),
|
|
241
|
+
tier,
|
|
242
|
+
recovered: state.recovered,
|
|
243
|
+
mutations: state.mutations,
|
|
244
|
+
metro: profile.evidence?.(target),
|
|
245
|
+
remoteFeatureFlags: remoteFlags
|
|
246
|
+
});
|
|
247
|
+
} else if (launch.jsonOutput) {
|
|
248
|
+
console.log(
|
|
249
|
+
JSON.stringify(
|
|
250
|
+
{
|
|
251
|
+
schemaVersion: 1,
|
|
252
|
+
command: "launch",
|
|
253
|
+
adapter,
|
|
254
|
+
platform: profile.platformLabel(launch),
|
|
255
|
+
tier,
|
|
256
|
+
status: "pass",
|
|
257
|
+
phase: "launch",
|
|
258
|
+
recovered: state.recovered,
|
|
259
|
+
mutations: state.mutations,
|
|
260
|
+
metro: profile.evidence?.(target),
|
|
261
|
+
remoteFeatureFlags: remoteFlags,
|
|
262
|
+
exitCode: EXIT.ok
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
const tierNote = tier === "quick" ? "quick relaunch, no native build" : tier;
|
|
270
|
+
const devNote = process.env.MM_HARNESS_BIN ? ` ${color("dim", "[dev: MM_HARNESS_BIN]")}` : "";
|
|
271
|
+
console.error(
|
|
272
|
+
`${color("ok", "\u2713")} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} \u2014 ${color("ok", profile.deviceLabel(launch))} \xB7 ${tierNote} \xB7 app + bridge ready${devNote}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return EXIT.ok;
|
|
276
|
+
}
|
|
277
|
+
function launchFail(launch, profile, state, failure) {
|
|
278
|
+
const { adapter, mobileTarget, target, stream, tier } = launch;
|
|
279
|
+
const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
280
|
+
if (stream.enabled) {
|
|
281
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
282
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
283
|
+
stream.error({
|
|
284
|
+
code: failure.code,
|
|
285
|
+
message: failure.message,
|
|
286
|
+
retryable: failure.recoverable,
|
|
287
|
+
userAction,
|
|
288
|
+
originalError: failure.originalError ?? null
|
|
289
|
+
});
|
|
290
|
+
stream.complete("fail", failure.exitCode, {
|
|
291
|
+
adapter,
|
|
292
|
+
platform: mobileTarget ?? null,
|
|
293
|
+
tier,
|
|
294
|
+
recovered: state.recovered,
|
|
295
|
+
mutations: state.mutations,
|
|
296
|
+
recoverable: failure.recoverable,
|
|
297
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
298
|
+
metro: profile.evidence?.(target)
|
|
299
|
+
});
|
|
300
|
+
} else if (launch.jsonOutput) {
|
|
301
|
+
console.log(
|
|
302
|
+
JSON.stringify(
|
|
303
|
+
{
|
|
304
|
+
schemaVersion: 1,
|
|
305
|
+
command: "launch",
|
|
306
|
+
adapter,
|
|
307
|
+
platform: mobileTarget ?? null,
|
|
308
|
+
tier,
|
|
309
|
+
status: "fail",
|
|
310
|
+
phase: state.attemptedRecoveries.length ? "recover" : "launch",
|
|
311
|
+
recovered: state.recovered,
|
|
312
|
+
mutations: state.mutations,
|
|
313
|
+
recoverable: failure.recoverable,
|
|
314
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
315
|
+
metro: profile.evidence?.(target),
|
|
316
|
+
exitCode: failure.exitCode,
|
|
317
|
+
error: {
|
|
318
|
+
code: failure.code,
|
|
319
|
+
message: failure.message,
|
|
320
|
+
retryable: failure.recoverable,
|
|
321
|
+
userAction,
|
|
322
|
+
originalError: failure.originalError ?? null
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
null,
|
|
326
|
+
2
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
console.error(colorHumanMessage(
|
|
331
|
+
`\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
|
|
332
|
+
${failure.message}` + (failure.originalError ? `
|
|
333
|
+
--- original failure ---
|
|
334
|
+
${failure.originalError}` : "") + `
|
|
335
|
+
Next: ${userAction}`
|
|
336
|
+
));
|
|
337
|
+
}
|
|
338
|
+
return failure.exitCode;
|
|
339
|
+
}
|
|
340
|
+
export {
|
|
341
|
+
runDeviceLaunch
|
|
342
|
+
};
|