@deeeed/metamask-harness 0.75.0 → 0.76.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/adapters/shared/reap-checkout-metros.sh +9 -0
- package/dist/adapters/core/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/surface.js +7 -0
- package/dist/adapters/extension/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -0
- package/dist/adapters/extension/overlay.js +55 -0
- package/dist/adapters/extension/readiness.js +32 -0
- package/dist/adapters/extension/reload.js +40 -0
- package/dist/adapters/extension/surface.js +22 -1
- package/dist/adapters/harness-freshness.js +4 -2
- package/dist/adapters/mobile/devices.js +21 -0
- package/dist/adapters/mobile/doctor-checks.js +40 -0
- package/dist/adapters/mobile/fixtures.js +141 -0
- package/dist/adapters/mobile/overlay.js +27 -0
- package/dist/adapters/mobile/prepare-target.js +38 -0
- package/dist/adapters/mobile/readiness.js +39 -0
- package/dist/adapters/mobile/reload.js +15 -0
- package/dist/adapters/mobile/surface.js +28 -1
- package/dist/adapters/terminal/doctor-checks.js +22 -0
- package/dist/adapters/terminal/overlay.js +18 -0
- package/dist/adapters/terminal/readiness.js +11 -0
- package/dist/adapters/terminal/surface.js +15 -2
- package/dist/cli.js +3 -1
- package/dist/commands/debug.js +6 -25
- package/dist/commands/device-target.js +2 -1
- package/dist/commands/doctor.js +26 -137
- package/dist/commands/fixtures.js +13 -217
- package/dist/commands/logs.js +1 -1
- package/dist/commands/parse-args.js +9 -4
- package/dist/commands/prepare.js +23 -53
- package/dist/commands/provision.js +3 -7
- package/dist/commands/reload.js +8 -47
- package/dist/commands/runtime-decision.js +5 -27
- package/dist/commands/self-test.js +1 -3
- package/dist/commands/status.js +20 -28
- package/dist/commands/stop.js +1 -1
- package/dist/doctor.js +9 -119
- package/dist/harness.js +31 -100
- package/dist/mm-harness-cli.js +3 -2
- package/dist/paths.js +8 -2
- package/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +27 -17
- package/package.json +1 -1
package/dist/commands/prepare.js
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
import { color, stripAnsi } from "../cli-color.js";
|
|
11
11
|
import { runnerProvenance } from "../doctor.js";
|
|
12
12
|
import { detectAdapter } from "../harness.js";
|
|
13
|
-
import {
|
|
13
|
+
import { adapterRegistry } from "../adapters/registry.js";
|
|
14
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
15
|
+
import { harnessExecutable, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
|
|
14
16
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
15
17
|
import { EXIT } from "./shared.js";
|
|
16
18
|
import { formatDuration } from "./status-watch.js";
|
|
@@ -52,8 +54,8 @@ const STEP_HINT = {
|
|
|
52
54
|
launch: "can take minutes on Mobile: Metro bundle and app start",
|
|
53
55
|
fixtures: "under a minute"
|
|
54
56
|
};
|
|
55
|
-
function stepChain(
|
|
56
|
-
return
|
|
57
|
+
function stepChain(surface) {
|
|
58
|
+
return surface.headless ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
|
|
57
59
|
}
|
|
58
60
|
function lastRunDurations(recordPath) {
|
|
59
61
|
let parsed;
|
|
@@ -225,36 +227,6 @@ function readDevices(artifactsDir) {
|
|
|
225
227
|
const devices = parsed?.devices;
|
|
226
228
|
return Array.isArray(devices) ? devices : [];
|
|
227
229
|
}
|
|
228
|
-
function readRuntimeContextTarget(target) {
|
|
229
|
-
const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
|
|
230
|
-
let parsed;
|
|
231
|
-
try {
|
|
232
|
-
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
233
|
-
} catch {
|
|
234
|
-
return {};
|
|
235
|
-
}
|
|
236
|
-
const record = parsed ?? {};
|
|
237
|
-
const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
|
|
238
|
-
return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
|
|
239
|
-
}
|
|
240
|
-
function mobilePlatformFromConfiguredTarget(env, context) {
|
|
241
|
-
const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
|
|
242
|
-
const android = Boolean(
|
|
243
|
-
env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
|
|
244
|
-
);
|
|
245
|
-
if (ios === android) return void 0;
|
|
246
|
-
return ios ? "ios" : "android";
|
|
247
|
-
}
|
|
248
|
-
function mobilePlatformFromDevices(devices) {
|
|
249
|
-
const selected = devices.filter((device) => device?.selected === true);
|
|
250
|
-
const candidates = selected.length > 0 ? selected : devices;
|
|
251
|
-
const platforms = [
|
|
252
|
-
...new Set(
|
|
253
|
-
candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
|
|
254
|
-
)
|
|
255
|
-
];
|
|
256
|
-
return platforms.length === 1 ? platforms[0] : void 0;
|
|
257
|
-
}
|
|
258
230
|
function mobileTargetAmbiguityError() {
|
|
259
231
|
const choices = [
|
|
260
232
|
"1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
|
|
@@ -300,8 +272,9 @@ function mobilePlatformOption(value) {
|
|
|
300
272
|
}
|
|
301
273
|
function resolvePlatform(requested, target) {
|
|
302
274
|
const platform = requested ?? detectAdapter(target);
|
|
303
|
-
|
|
304
|
-
|
|
275
|
+
const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
|
|
276
|
+
if (surface?.prepare) {
|
|
277
|
+
return surface;
|
|
305
278
|
}
|
|
306
279
|
throw usageError(
|
|
307
280
|
`could not detect the platform of ${target}
|
|
@@ -328,21 +301,22 @@ async function handlePrepareLocked(argv) {
|
|
|
328
301
|
const { options } = parseArgs(argv, "prepare");
|
|
329
302
|
const json = optionFlag(options, "json");
|
|
330
303
|
const target = targetPath(options);
|
|
331
|
-
const
|
|
304
|
+
const surface = resolvePlatform(optionString(options, "platform"), target);
|
|
305
|
+
const platform = surface.adapter;
|
|
332
306
|
const artifactsDir = path.resolve(
|
|
333
307
|
optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
|
|
334
308
|
);
|
|
335
309
|
const executable = harnessExecutable();
|
|
336
310
|
const targetArgs = ["--target", target];
|
|
337
311
|
const clearMetro = optionFlag(options, "clearMetro");
|
|
338
|
-
if (clearMetro &&
|
|
312
|
+
if (clearMetro && !surface.prepare.clearMetro) {
|
|
339
313
|
throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
|
|
340
314
|
Next: ${USAGE}`);
|
|
341
315
|
}
|
|
342
316
|
const remoteFlag = optionString(options, "remoteFlag");
|
|
343
317
|
if (remoteFlag !== void 0) {
|
|
344
|
-
if (
|
|
345
|
-
throw usageError(`--remote-flag needs an app runtime; this checkout is
|
|
318
|
+
if (surface.headless) {
|
|
319
|
+
throw usageError(`--remote-flag needs an app runtime; this checkout is ${platform}
|
|
346
320
|
Next: ${USAGE}`);
|
|
347
321
|
}
|
|
348
322
|
try {
|
|
@@ -357,7 +331,7 @@ async function handlePrepareLocked(argv) {
|
|
|
357
331
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
358
332
|
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
359
333
|
const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
|
|
360
|
-
const chain = stepChain(
|
|
334
|
+
const chain = stepChain(surface);
|
|
361
335
|
const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
|
|
362
336
|
view?.begin();
|
|
363
337
|
const steps = [];
|
|
@@ -406,35 +380,33 @@ async function handlePrepareLocked(argv) {
|
|
|
406
380
|
try {
|
|
407
381
|
await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
408
382
|
await run("status", ["status", "--json", ...targetArgs]);
|
|
409
|
-
|
|
410
|
-
|
|
383
|
+
const { devicePlatform } = surface.prepare;
|
|
384
|
+
if (devicePlatform && !mobilePlatform && !failed()) {
|
|
385
|
+
mobilePlatform = devicePlatform(target, readDevices(artifactsDir));
|
|
411
386
|
if (!mobilePlatform) return ambiguousMobileTarget(json);
|
|
412
387
|
}
|
|
413
|
-
if (
|
|
414
|
-
const reason =
|
|
388
|
+
if (surface.headless) {
|
|
389
|
+
const reason = `${platform} is headless: no app surface to launch or seed`;
|
|
415
390
|
addStep(skippedStep("launch", `${executable} launch`, reason));
|
|
416
391
|
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
417
|
-
} else
|
|
418
|
-
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
392
|
+
} else {
|
|
393
|
+
const device = devicePlatform && mobilePlatform ? [mobilePlatform] : [];
|
|
419
394
|
await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
420
395
|
await run("fixtures", [
|
|
421
396
|
"fixtures",
|
|
422
397
|
"set",
|
|
423
|
-
...
|
|
398
|
+
...device.flatMap((platformName) => ["--platform", platformName]),
|
|
424
399
|
...remoteFlagArgs,
|
|
425
400
|
"--json",
|
|
426
401
|
...targetArgs
|
|
427
402
|
]);
|
|
428
|
-
} else {
|
|
429
|
-
await run("launch", ["launch", ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
430
|
-
await run("fixtures", ["fixtures", "set", ...remoteFlagArgs, "--json", ...targetArgs]);
|
|
431
403
|
}
|
|
432
404
|
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
433
405
|
} finally {
|
|
434
406
|
clearInterval(heartbeat);
|
|
435
407
|
view?.end();
|
|
436
408
|
}
|
|
437
|
-
const provenance = runnerProvenance(manifestPath(
|
|
409
|
+
const provenance = runnerProvenance(surface.actions.manifestPath());
|
|
438
410
|
const record = {
|
|
439
411
|
schemaVersion: 1,
|
|
440
412
|
harness: {
|
|
@@ -462,7 +434,5 @@ async function handlePrepareLocked(argv) {
|
|
|
462
434
|
}
|
|
463
435
|
export {
|
|
464
436
|
handlePrepare,
|
|
465
|
-
mobilePlatformFromConfiguredTarget,
|
|
466
|
-
mobilePlatformFromDevices,
|
|
467
437
|
mobileTargetAmbiguityError
|
|
468
438
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { detectAdapter } from "../harness.js";
|
|
2
2
|
import { assertAdapter } from "../paths.js";
|
|
3
3
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
-
import {
|
|
5
|
-
import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
|
|
4
|
+
import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
|
|
6
5
|
import {
|
|
7
6
|
applyWatcherPortOption,
|
|
8
7
|
optionFlag,
|
|
@@ -27,10 +26,7 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
27
26
|
const surface = getAdapterSurface(adapter);
|
|
28
27
|
const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
|
|
29
28
|
const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
|
|
30
|
-
|
|
31
|
-
writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
|
|
32
|
-
if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
|
|
33
|
-
}
|
|
29
|
+
surface.runwayProvision.before?.(target, { json, platform, resolveOnly: optionFlag(options, "resolveOnly") });
|
|
34
30
|
const result = await surface.runwayProvision.run(target, {
|
|
35
31
|
json,
|
|
36
32
|
platform,
|
|
@@ -50,7 +46,7 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
50
46
|
});
|
|
51
47
|
const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
52
48
|
const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
|
|
53
|
-
const next = result.status === "pass" &&
|
|
49
|
+
const next = result.status === "pass" && surface.runwayProvision.launchNext && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter ${adapter} --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
|
|
54
50
|
const output = next ? { ...result, next } : result;
|
|
55
51
|
if (json) {
|
|
56
52
|
console.log(JSON.stringify(output, null, 2));
|
package/dist/commands/reload.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { runnerDir } from "../paths.js";
|
|
3
1
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
2
|
import {
|
|
5
3
|
ADAPTER_DETECT_NEXT,
|
|
6
|
-
EXIT,
|
|
7
4
|
flag,
|
|
8
5
|
parseFlags,
|
|
9
6
|
resolveAdapter,
|
|
10
|
-
spawnScript,
|
|
11
7
|
targetOf,
|
|
12
8
|
usageOut
|
|
13
9
|
} from "./shared.js";
|
|
@@ -25,55 +21,20 @@ async function handleReload(argv) {
|
|
|
25
21
|
ADAPTER_DETECT_NEXT
|
|
26
22
|
);
|
|
27
23
|
}
|
|
28
|
-
|
|
24
|
+
const surface = getAdapterSurface(adapter);
|
|
25
|
+
if (surface.headless) {
|
|
29
26
|
return usageOut(
|
|
30
27
|
json,
|
|
31
28
|
"reload",
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
`${adapter} is headless; there is no app runtime to reload.`,
|
|
30
|
+
`mm-harness run <${adapter}-recipe>`
|
|
34
31
|
);
|
|
35
32
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!process.env.CDP_PORT) {
|
|
39
|
-
return usageOut(
|
|
40
|
-
json,
|
|
41
|
-
"reload",
|
|
42
|
-
"the Extension CDP port could not be resolved.",
|
|
43
|
-
"mm-harness launch"
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
const script2 = path.join(runnerDir, "adapters/extension/reattach.sh");
|
|
47
|
-
const args2 = ["--target", target, "--cdp-port", process.env.CDP_PORT];
|
|
48
|
-
if (process.env.WATCHER_PORT) {
|
|
49
|
-
args2.push("--watcher-port", process.env.WATCHER_PORT);
|
|
50
|
-
}
|
|
51
|
-
const result2 = spawnScript(script2, args2, target, json);
|
|
52
|
-
if (json) {
|
|
53
|
-
console.log(
|
|
54
|
-
JSON.stringify(
|
|
55
|
-
{
|
|
56
|
-
ok: result2.status === 0,
|
|
57
|
-
adapter,
|
|
58
|
-
method: "cdp-reattach",
|
|
59
|
-
command: "reload",
|
|
60
|
-
cdpPort: Number(process.env.CDP_PORT),
|
|
61
|
-
...result2.status === 0 ? {} : { error: result2.output.slice(-4e3) || "Extension reload failed." }
|
|
62
|
-
},
|
|
63
|
-
null,
|
|
64
|
-
2
|
|
65
|
-
)
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return result2.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
33
|
+
if (!surface.reload) {
|
|
34
|
+
return usageOut(json, "reload", `reload does not support the ${adapter} adapter.`, surface.hints.launch);
|
|
69
35
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
|
|
73
|
-
if (json) args.push("--json");
|
|
74
|
-
const result = spawnScript(process.execPath, [script, ...args], target, json);
|
|
75
|
-
if (json) process.stdout.write(result.output);
|
|
76
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
36
|
+
surface.resolveSlotPorts(target);
|
|
37
|
+
return surface.reload(target, json);
|
|
77
38
|
}
|
|
78
39
|
export {
|
|
79
40
|
handleReload
|
|
@@ -1,32 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { decideMobileReadiness } from "../adapters/mobile/runtime-decision.js";
|
|
1
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
3
2
|
import {
|
|
4
3
|
adapterOption,
|
|
5
4
|
applyRuntimeDirOption,
|
|
6
5
|
optionFlag,
|
|
7
|
-
optionString,
|
|
8
|
-
parsePort,
|
|
9
6
|
targetPath
|
|
10
7
|
} from "./parse-args.js";
|
|
11
8
|
async function handleRuntimeDecision({ options }) {
|
|
12
9
|
applyRuntimeDirOption(options);
|
|
13
10
|
const adapter = adapterOption(options);
|
|
14
11
|
const target = targetPath(options);
|
|
15
|
-
|
|
16
|
-
|
|
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") {
|
|
12
|
+
const decide = getAdapterSurface(adapter).runtimeDecision;
|
|
13
|
+
if (!decide) {
|
|
30
14
|
const report2 = {
|
|
31
15
|
schemaVersion: 1,
|
|
32
16
|
adapter,
|
|
@@ -42,15 +26,9 @@ async function handleRuntimeDecision({ options }) {
|
|
|
42
26
|
else console.log(`unknown adapter-unsupported \u2014 ${report2.reasons[0]}`);
|
|
43
27
|
return 0;
|
|
44
28
|
}
|
|
45
|
-
const
|
|
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
|
-
});
|
|
29
|
+
const { report, line } = await decide(target, options);
|
|
52
30
|
if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
|
|
53
|
-
else console.log(
|
|
31
|
+
else console.log(line);
|
|
54
32
|
return 0;
|
|
55
33
|
}
|
|
56
34
|
export {
|
|
@@ -25,9 +25,7 @@ async function runSelfTest(options) {
|
|
|
25
25
|
for (const adapter of ["mobile", "extension"]) {
|
|
26
26
|
const manifest = loadActionManifest(adapter);
|
|
27
27
|
const manifestValidation = await validateManifest(manifest);
|
|
28
|
-
const smokeRecipe = recipePath(
|
|
29
|
-
adapter === "mobile" ? "mobile/runner/smoke.recipe.json" : "extension/runner/smoke.recipe.json"
|
|
30
|
-
);
|
|
28
|
+
const smokeRecipe = recipePath(`${adapter}/runner/smoke.recipe.json`);
|
|
31
29
|
const artifactsDir = path.join(root, adapter);
|
|
32
30
|
const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
|
|
33
31
|
skipExtensionRuntimePrepare: true
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
4
3
|
import { featureFlagReport } from "../feature-flags.js";
|
|
5
4
|
import { color } from "../cli-color.js";
|
|
6
5
|
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
@@ -10,15 +9,6 @@ import { getAdapterSurface } from "../adapters/surface.js";
|
|
|
10
9
|
import {
|
|
11
10
|
applyDeviceTargeting
|
|
12
11
|
} from "./device-target.js";
|
|
13
|
-
import {
|
|
14
|
-
mobileDeviceView,
|
|
15
|
-
mobileDeviceLiveView,
|
|
16
|
-
nextForLive,
|
|
17
|
-
renderAndroidPortHints,
|
|
18
|
-
renderMobileDeviceList,
|
|
19
|
-
renderAdditionalReachableDevices,
|
|
20
|
-
renderMobileLiveBlock
|
|
21
|
-
} from "./mobile-device-view.js";
|
|
22
12
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
23
13
|
import { handleStatusTaskView, renderAcceptanceLines } from "./status-watch.js";
|
|
24
14
|
import { collectTaskView, findTaskDir } from "../task-view.js";
|
|
@@ -45,23 +35,25 @@ async function handleStatus({ options }) {
|
|
|
45
35
|
const view = collectCheckoutView(target, adapter);
|
|
46
36
|
const surface = getAdapterSurface(adapter);
|
|
47
37
|
surface.resolveSlotPorts(target);
|
|
48
|
-
|
|
38
|
+
const { readiness } = surface;
|
|
39
|
+
const deviceSource = await readiness.devices?.();
|
|
40
|
+
if (deviceSource) {
|
|
49
41
|
const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
|
|
50
42
|
if ("code" in dtResult) {
|
|
51
43
|
return usageOut(json, "status", dtResult.message, dtResult.userAction);
|
|
52
44
|
}
|
|
53
45
|
}
|
|
54
46
|
const next = surface.hints.relaunch;
|
|
55
|
-
const
|
|
56
|
-
const staticFeatureFlags =
|
|
57
|
-
const deviceView =
|
|
47
|
+
const pinnedFlags = readiness.pinnedFlags?.(target) ?? null;
|
|
48
|
+
const staticFeatureFlags = pinnedFlags ? featureFlagReport(pinnedFlags.overrides, pinnedFlags) : featureFlagReport(null);
|
|
49
|
+
const deviceView = deviceSource ? deviceSource.view(allDevices) : null;
|
|
58
50
|
const devices = deviceView?.devices ?? [];
|
|
59
51
|
const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
|
|
60
52
|
const discoveryNext = discovery.deviceDiscoveryErrors?.[0]?.userAction;
|
|
61
53
|
if (json) {
|
|
62
|
-
const doProbe = !fast &&
|
|
54
|
+
const doProbe = !fast && deviceSource && deviceView !== null && deviceView.allConnectedDevices.length > 0;
|
|
63
55
|
if (doProbe) {
|
|
64
|
-
const liveView = await
|
|
56
|
+
const liveView = await deviceSource.live(target, deviceView);
|
|
65
57
|
console.log(
|
|
66
58
|
JSON.stringify(
|
|
67
59
|
{
|
|
@@ -75,14 +67,14 @@ async function handleStatus({ options }) {
|
|
|
75
67
|
...discovery,
|
|
76
68
|
...liveView.additionalReachableDevices.length > 0 ? { additionalReachableDevices: liveView.additionalReachableDevices } : {},
|
|
77
69
|
...liveView.androidPortHints.length > 0 ? { androidPortHints: liveView.androidPortHints } : {},
|
|
78
|
-
next: discoveryNext ?? nextForLive(next, liveView.liveMap)
|
|
70
|
+
next: discoveryNext ?? deviceSource.nextForLive(next, liveView.liveMap)
|
|
79
71
|
},
|
|
80
72
|
null,
|
|
81
73
|
2
|
|
82
74
|
)
|
|
83
75
|
);
|
|
84
76
|
} else {
|
|
85
|
-
const runtime = !fast &&
|
|
77
|
+
const runtime = !fast && readiness.statusRuntime ? await surface.runtimeStatus(target) : void 0;
|
|
86
78
|
console.log(
|
|
87
79
|
JSON.stringify(
|
|
88
80
|
{
|
|
@@ -113,21 +105,21 @@ async function handleStatus({ options }) {
|
|
|
113
105
|
console.log(renderAcceptanceLines(taskView).join("\n"));
|
|
114
106
|
}
|
|
115
107
|
}
|
|
116
|
-
if (
|
|
117
|
-
if (deviceView)
|
|
118
|
-
if (!fast &&
|
|
119
|
-
const liveView = await
|
|
120
|
-
if (liveView.additionalReachableDevices.length > 0)
|
|
108
|
+
if (!deviceSource) renderFeatureFlagLine(staticFeatureFlags, out);
|
|
109
|
+
if (deviceSource && deviceView) deviceSource.renderList(deviceView, out);
|
|
110
|
+
if (!fast && deviceSource && deviceView !== null && deviceView.allConnectedDevices.length > 0) {
|
|
111
|
+
const liveView = await deviceSource.live(target, deviceView);
|
|
112
|
+
if (liveView.additionalReachableDevices.length > 0) deviceSource.renderAdditional(liveView.additionalReachableDevices, out);
|
|
121
113
|
renderFeatureFlagLine(featureFlagReport(liveView.featureFlagOverrides), out);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? nextForLive(next, liveView.liveMap))}`);
|
|
125
|
-
} else if (!fast &&
|
|
114
|
+
deviceSource.renderLive(devices, liveView.liveMap, out);
|
|
115
|
+
deviceSource.renderPortHints(liveView.androidPortHints, out);
|
|
116
|
+
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? deviceSource.nextForLive(next, liveView.liveMap))}`);
|
|
117
|
+
} else if (!fast && readiness.statusRuntime) {
|
|
126
118
|
const runtime = await surface.runtimeStatus(target);
|
|
127
119
|
renderRuntimeStatus(runtime, out);
|
|
128
120
|
console.log(`${out("label", "Next:")} ${out("cmd", nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime))}`);
|
|
129
121
|
} else {
|
|
130
|
-
if (
|
|
122
|
+
if (deviceSource) renderFeatureFlagLine(featureFlagReport(null), out);
|
|
131
123
|
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? next)}`);
|
|
132
124
|
}
|
|
133
125
|
return EXIT.ok;
|
package/dist/commands/stop.js
CHANGED
|
@@ -27,7 +27,7 @@ async function handleStop(argv) {
|
|
|
27
27
|
const collector = await stopProjectCollector(target);
|
|
28
28
|
const signalled = (stop.signalled ?? 0) + (collector.stopped ? 1 : 0);
|
|
29
29
|
const summary = collector.stopped ? `${stop.summary}; stopped analytics collector ${collector.pid}` : stop.summary;
|
|
30
|
-
const next =
|
|
30
|
+
const next = surface.devServer.afterStop?.(target);
|
|
31
31
|
const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
|
|
32
32
|
if (json) {
|
|
33
33
|
console.log(
|
package/dist/doctor.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { color } from "./cli-color.js";
|
|
6
4
|
import { normalizeFeatureFlagRecord } from "./feature-flags.js";
|
|
7
|
-
import {
|
|
8
|
-
mobileToolRecovery,
|
|
9
|
-
resolveMobileToolPath
|
|
10
|
-
} from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
11
|
-
import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
12
6
|
import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
|
|
13
7
|
import { isPin } from "./harness-pin.js";
|
|
14
|
-
import { harnessExecutable,
|
|
8
|
+
import { harnessExecutable, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
15
9
|
function requiredDoctorCheckSummary(checks) {
|
|
16
10
|
const required = checks.filter((check) => check.required);
|
|
17
11
|
const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
|
|
@@ -42,21 +36,7 @@ function repoShape(target) {
|
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
38
|
function compatibilityMode(adapter, target) {
|
|
45
|
-
|
|
46
|
-
if (adapter === "core") {
|
|
47
|
-
return fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge";
|
|
48
|
-
}
|
|
49
|
-
if (adapter === "terminal") {
|
|
50
|
-
return fs.existsSync(path.join(target, "src/features/perpetuals")) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge";
|
|
51
|
-
}
|
|
52
|
-
if (adapter === "mobile") {
|
|
53
|
-
if (shape.injectedHarness && shape.agenticService) return "runner bridge with injected app bridge";
|
|
54
|
-
if (shape.agenticService) return "runner bridge with app bridge";
|
|
55
|
-
return "runner bridge available; app bridge not installed";
|
|
56
|
-
}
|
|
57
|
-
if (!shape.extensionProject) return "unsupported/no bridge";
|
|
58
|
-
if (shape.extensionRuntime || shape.injectedHarness) return "bridge present";
|
|
59
|
-
return "bridge injectable";
|
|
39
|
+
return getAdapterSurface(adapter).readiness.compatibilityMode(target, repoShape(target));
|
|
60
40
|
}
|
|
61
41
|
function readPackageInfo(target) {
|
|
62
42
|
const packageJsonPath = path.join(target, "package.json");
|
|
@@ -97,7 +77,8 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
|
97
77
|
);
|
|
98
78
|
const flagKeys = Object.keys(remoteFeatureFlags);
|
|
99
79
|
return {
|
|
100
|
-
|
|
80
|
+
// A headless platform signs with the accounts alone; nothing unlocks a UI.
|
|
81
|
+
status: hasAccounts && (adapter !== void 0 && getAdapterSurface(adapter).headless || hasPassword) ? "ready" : "incomplete",
|
|
101
82
|
path: reportedPath,
|
|
102
83
|
accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
|
|
103
84
|
hasPassword,
|
|
@@ -155,7 +136,8 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
155
136
|
}
|
|
156
137
|
return lines.join("\n");
|
|
157
138
|
}
|
|
158
|
-
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath =
|
|
139
|
+
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = getAdapterSurface(adapter).actions.manifestPath(), platform) {
|
|
140
|
+
const readiness = getAdapterSurface(adapter).readiness;
|
|
159
141
|
const mode = compatibilityMode(adapter, target);
|
|
160
142
|
const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
|
|
161
143
|
const checks = [
|
|
@@ -171,8 +153,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
171
153
|
required: false,
|
|
172
154
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
173
155
|
},
|
|
174
|
-
...
|
|
175
|
-
...adapter === "extension" ? [extensionBrowserDoctorCheck(target), extensionBrowserResolutionDoctorCheck(target)] : []
|
|
156
|
+
...readiness.checks?.(target, platform) ?? []
|
|
176
157
|
];
|
|
177
158
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
178
159
|
return {
|
|
@@ -188,99 +169,11 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
188
169
|
compatibilityMode: mode,
|
|
189
170
|
shape: repoShape(target),
|
|
190
171
|
fixture: fixtureSummary(target, adapter),
|
|
191
|
-
environment:
|
|
172
|
+
environment: readiness.environment?.(target) ?? {},
|
|
192
173
|
runtimeContext: runtimeContextSummary(target, adapter),
|
|
193
174
|
manifestValidation: manifestValidation.summary
|
|
194
175
|
};
|
|
195
176
|
}
|
|
196
|
-
function extensionBrowserDoctorCheck(target) {
|
|
197
|
-
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
198
|
-
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
199
|
-
try {
|
|
200
|
-
const playwright = requireFromTarget(packageName);
|
|
201
|
-
const executable = playwright.chromium?.executablePath?.();
|
|
202
|
-
if (executable && fs.existsSync(executable)) {
|
|
203
|
-
return {
|
|
204
|
-
id: "playwright-chromium",
|
|
205
|
-
status: "pass",
|
|
206
|
-
required: true,
|
|
207
|
-
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
208
|
-
detail: executable
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
id: "playwright-chromium",
|
|
216
|
-
status: "fail",
|
|
217
|
-
required: true,
|
|
218
|
-
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
219
|
-
userAction: "yarn playwright install chromium"
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function extensionBrowserResolutionDoctorCheck(target) {
|
|
223
|
-
const resolver = path.join(runnerDir, "adapters/shared/browser-resolver.cjs");
|
|
224
|
-
const runtime = path.join(path.resolve(target), recipeRuntimeDir());
|
|
225
|
-
const record = path.join(runtime, "browser-resolution.json");
|
|
226
|
-
let profile = process.env.CHROME_USER_DATA_DIR || path.join(runtime, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
227
|
-
while (profile.length > 1 && profile.endsWith("/")) profile = profile.slice(0, -1);
|
|
228
|
-
const profileExists = fs.existsSync(path.join(profile, "Local State"));
|
|
229
|
-
const result = spawnSync(process.execPath, [
|
|
230
|
-
resolver,
|
|
231
|
-
"resolve",
|
|
232
|
-
"--target",
|
|
233
|
-
path.resolve(target),
|
|
234
|
-
"--launch-method",
|
|
235
|
-
"launch-services",
|
|
236
|
-
"--display",
|
|
237
|
-
"headful",
|
|
238
|
-
...profileExists && fs.existsSync(record) ? ["--recorded", record] : [],
|
|
239
|
-
"--json"
|
|
240
|
-
], {
|
|
241
|
-
cwd: path.resolve(target),
|
|
242
|
-
encoding: "utf8",
|
|
243
|
-
timeout: 135e3
|
|
244
|
-
});
|
|
245
|
-
if (result.status === 0) {
|
|
246
|
-
const resolution = JSON.parse(result.stdout);
|
|
247
|
-
const managed = resolution.managedChrome;
|
|
248
|
-
const managedNote = managed && !resolution.warning?.includes("managed by policy") ? ` Google Chrome is managed by policy (${managed.forcedExtensions.length} forced extension${managed.forcedExtensions.length === 1 ? "" : "s"}: ${managed.forcedExtensions.map(({ id, name }) => name ? `${id} (${name})` : id).join(", ")}${managed.signinForced ? "; browser sign-in forced" : ""}); slots use Chrome for Testing.` : "";
|
|
249
|
-
const probe = resolution.probe ? `; Chrome for Testing probe (${resolution.probe.launchMethod}, ${resolution.probe.display}${resolution.probe.cached ? ", cached" : ""}${resolution.probe.transient ? `, retried after ${resolution.probe.expiresAt}` : ""}): ${resolution.probe.reason}` : resolution.recordedFrom ? `; kept from the slot profile record ${resolution.recordedFrom}` : "";
|
|
250
|
-
return {
|
|
251
|
-
id: "extension-browser",
|
|
252
|
-
status: "pass",
|
|
253
|
-
required: false,
|
|
254
|
-
message: `Launch browser: ${resolution.browser} ${resolution.version ?? "(unknown version)"} via ${resolution.source} (${resolution.mode}); extension loading: ${resolution.extensionLoading}${probe}.${managedNote}${resolution.warning ? ` Warning: ${resolution.warning}` : ""}`,
|
|
255
|
-
detail: resolution.bin
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
const message = (result.stderr || result.error?.message || "browser resolution failed").trim();
|
|
259
|
-
return {
|
|
260
|
-
id: "extension-browser",
|
|
261
|
-
status: "fail",
|
|
262
|
-
required: message.includes("BROWSER_UNLAUNCHABLE:"),
|
|
263
|
-
message,
|
|
264
|
-
userAction: "install Google Chrome, or set RECIPE_HARNESS_BROWSER / RECIPE_HARNESS_CHROME_BIN to a browser that starts"
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function mobileToolDoctorChecks(platform) {
|
|
268
|
-
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
269
|
-
{ tool: "adb", platform: "Android" },
|
|
270
|
-
{ tool: "idb", platform: "iOS" }
|
|
271
|
-
];
|
|
272
|
-
return specs.map(({ tool, platform: platformName }) => {
|
|
273
|
-
const resolved = resolveMobileToolPath(tool);
|
|
274
|
-
return {
|
|
275
|
-
id: `device-tool-${tool}`,
|
|
276
|
-
status: resolved ? "pass" : "fail",
|
|
277
|
-
required: platform !== void 0,
|
|
278
|
-
message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
|
|
279
|
-
...resolved ? { detail: resolved } : {},
|
|
280
|
-
...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
|
|
281
|
-
};
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
177
|
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
285
178
|
if (isPin(runnerRoot)) return "pinned-install";
|
|
286
179
|
const normalizedRoot = path.normalize(runnerRoot);
|
|
@@ -344,11 +237,8 @@ function readJsonObject(file) {
|
|
|
344
237
|
export {
|
|
345
238
|
compatibilityMode,
|
|
346
239
|
createDoctorReport,
|
|
347
|
-
extensionBrowserDoctorCheck,
|
|
348
|
-
extensionBrowserResolutionDoctorCheck,
|
|
349
240
|
fixtureFileSummary,
|
|
350
241
|
fixtureSummary,
|
|
351
|
-
mobileToolDoctorChecks,
|
|
352
242
|
renderRuntimeContext,
|
|
353
243
|
repoShape,
|
|
354
244
|
requiredDoctorCheckSummary,
|