@deeeed/metamask-harness 0.2.0 → 0.3.1
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 +33 -0
- package/README.md +47 -17
- package/{orchestration → adapters}/core/cleanup.sh +2 -2
- package/{orchestration → adapters}/core/inject.sh +8 -8
- package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
- package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
- package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
- package/{orchestration → adapters}/extension/inject.mjs +27 -31
- package/{orchestration → adapters}/extension/launch.sh +4 -4
- package/{orchestration → adapters}/extension/live.sh +5 -5
- package/{orchestration → adapters}/extension/readiness.mjs +12 -2
- package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
- package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
- package/{orchestration → adapters}/extension/start-watch.sh +7 -7
- package/{runner → adapters}/extension/verify.sh +11 -11
- package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
- package/{orchestration → adapters}/manifest.json +96 -96
- package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
- package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
- package/{orchestration → adapters}/mobile/inject.sh +22 -24
- package/adapters/mobile/open-device.sh +260 -0
- package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
- package/adapters/mobile/prewarm-bundle.sh +95 -0
- package/adapters/mobile/start-metro.sh +228 -0
- package/{runner → adapters}/mobile/verify.sh +6 -6
- package/adapters/mobile/wait-for-bridge.sh +108 -0
- package/adapters/mobile/yarn-setup.sh +20 -0
- package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
- package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
- package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
- package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
- package/adapters/shared/sync-wallet-fixture.sh +78 -0
- package/bin/mm-harness +37 -6
- package/docs/CLI-SPEC.md +41 -2
- package/docs/architecture.md +69 -38
- package/docs/runtime-file-conventions.md +3 -3
- 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 +1 -1
- package/library/actions/mobile/platform/bridge.mjs +1 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +2 -2
- package/library/library.json +1 -1
- package/package.json +8 -8
- package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
- package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
- package/src/adapters/mobile/prepare.ts +210 -0
- package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
- package/{runner/src → src}/adapters.ts +3 -3
- package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
- package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
- package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
- package/{runner/src → src}/cli.ts +48 -40
- package/src/commands/debug.ts +63 -0
- package/src/commands/fixtures.ts +193 -0
- package/{runner/src → src}/commands/launch.ts +200 -54
- package/src/commands/logs.ts +83 -0
- package/{runner/src → src}/commands/shared.ts +40 -30
- package/src/commands/update.ts +316 -0
- package/{runner/src → src}/harness.ts +119 -16
- package/{runner/src → src}/heal-bounds.ts +20 -1
- package/src/index.ts +15 -0
- package/{runner/src → src}/live-adapter-contract.ts +1 -1
- package/{runner/src → src}/mm-harness-cli.ts +64 -3
- package/{runner/src → src}/paths.ts +2 -2
- package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
- package/docs/extension-runtime-commands.md +0 -60
- package/docs/harness-cli.md +0 -43
- package/orchestration/compat-overlays/mobile/README.md +0 -13
- package/orchestration/doctor.mjs +0 -72
- package/orchestration/lib/cli-home.mjs +0 -354
- package/orchestration/lib/recipe-paths.mjs +0 -26
- package/orchestration/mobile/launch.sh +0 -137
- package/orchestration/mobile/live.sh +0 -125
- package/orchestration/porcelain/metamask-recipe +0 -99
- package/orchestration/porcelain/mm-recipe +0 -1591
- package/orchestration/porcelain/mme-recipe +0 -1181
- package/runner/src/commands/debug.ts +0 -44
- package/runner/src/commands/fixtures.ts +0 -99
- package/runner/src/commands/logs.ts +0 -60
- package/runner/src/index.ts +0 -6
- /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
- /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
- /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
- /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
- /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
- /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
- /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
- /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
- /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
- /package/{runner/src → src}/completions-cache.ts +0 -0
- /package/{runner/src → src}/doctor.ts +0 -0
- /package/{runner/src → src}/manifest.ts +0 -0
- /package/{runner/src → src}/recording-target.ts +0 -0
- /package/{runner/src → src}/run-recording.ts +0 -0
- /package/{runner/src → src}/runner.ts +0 -0
- /package/{runner/src → src}/types.ts +0 -0
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from './paths.ts';
|
|
6
6
|
import type { MetaMaskRecipeAdapter } from './types.ts';
|
|
7
|
+
import { prepareMobile } from './adapters/mobile/prepare.ts';
|
|
7
8
|
|
|
8
9
|
// The `harness` command moves the recipe-harness skill's runner-locate + dispatch
|
|
9
10
|
// bash (resolve-runner-source.sh + recipe-harness.sh dispatch_adapter_action)
|
|
@@ -155,6 +156,12 @@ function parseHarnessArgs(args: string[]): ParsedHarnessArgs {
|
|
|
155
156
|
// Mirror of scripts/lib/cli-common.sh detect_adapter for the auto-detect path.
|
|
156
157
|
// Exported so `doctor` shares the exact same detect-from-target logic as the
|
|
157
158
|
// overlay commands (verify/install/cleanup) — one auto-detect story, no drift.
|
|
159
|
+
// Both escapes from a failed repo-type detection: where to run it (checkout/target)
|
|
160
|
+
// and how to force it (adapter). A single source so every detect-failure teaches
|
|
161
|
+
// the same reachable next step.
|
|
162
|
+
export const ADAPTER_DETECT_NEXT =
|
|
163
|
+
'cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>';
|
|
164
|
+
|
|
158
165
|
export function detectAdapter(target: string): MetaMaskRecipeAdapter | undefined {
|
|
159
166
|
let remote = '';
|
|
160
167
|
try {
|
|
@@ -277,11 +284,14 @@ function resolveEntry(base: string, candidates: string[], mode: 'exec' | 'file')
|
|
|
277
284
|
|
|
278
285
|
// Content of an installed overlay's runner source pointer, if present. Command
|
|
279
286
|
// substitution in the skill stripped trailing newlines, so trim here too.
|
|
287
|
+
// Validates the path exists so stale stamps (e.g. from a renamed package dir)
|
|
288
|
+
// fall back to the current runnerDir rather than producing ENOENT at cleanup.
|
|
280
289
|
function installedRunnerSource(target: string, adapter: MetaMaskRecipeAdapter): string | undefined {
|
|
281
290
|
const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
|
|
282
291
|
if (!fs.existsSync(pointer)) return undefined;
|
|
283
292
|
const value = fs.readFileSync(pointer, 'utf8').trim();
|
|
284
|
-
|
|
293
|
+
if (!value || !fs.existsSync(value)) return undefined;
|
|
294
|
+
return value;
|
|
285
295
|
}
|
|
286
296
|
|
|
287
297
|
function isExecutable(file: string): boolean {
|
|
@@ -299,15 +309,15 @@ interface HarnessDispatch {
|
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
const INJECT_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
302
|
-
mobile: { entry: '
|
|
303
|
-
extension: { entry: '
|
|
304
|
-
core: { entry: '
|
|
312
|
+
mobile: { entry: 'adapters/mobile/inject.sh', fallback: 'scripts/inject-mobile-harness.sh' },
|
|
313
|
+
extension: { entry: 'adapters/extension/inject.mjs', fallback: 'scripts/inject-extension-harness.mjs' },
|
|
314
|
+
core: { entry: 'adapters/core/inject.sh', fallback: 'scripts/inject-core-harness.sh' },
|
|
305
315
|
};
|
|
306
316
|
|
|
307
317
|
const CLEANUP_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
308
|
-
mobile: { entry: '
|
|
309
|
-
extension: { entry: '
|
|
310
|
-
core: { entry: '
|
|
318
|
+
mobile: { entry: 'adapters/mobile/cleanup.sh', fallback: 'scripts/cleanup-mobile-harness.sh' },
|
|
319
|
+
extension: { entry: 'adapters/extension/cleanup.mjs', fallback: 'scripts/cleanup-extension-harness.mjs' },
|
|
320
|
+
core: { entry: 'adapters/core/cleanup.sh', fallback: 'scripts/cleanup-core-harness.sh' },
|
|
311
321
|
};
|
|
312
322
|
|
|
313
323
|
// Resolve the exact command + fixed prefix args to exec, mirroring
|
|
@@ -337,7 +347,7 @@ function resolveHarnessDispatch(
|
|
|
337
347
|
}
|
|
338
348
|
|
|
339
349
|
if (adapter === 'core' && action === 'verify') {
|
|
340
|
-
const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', '
|
|
350
|
+
const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', 'mm-harness');
|
|
341
351
|
if (!isExecutable(delegate)) {
|
|
342
352
|
return {
|
|
343
353
|
error:
|
|
@@ -351,19 +361,98 @@ function resolveHarnessDispatch(
|
|
|
351
361
|
}
|
|
352
362
|
|
|
353
363
|
// mobile/extension live|verify: prefer the installed helper, else the runner's
|
|
354
|
-
// own
|
|
364
|
+
// own adapters/scripts helper (first executable candidate wins).
|
|
355
365
|
const installedScript = path.join(recipeHarnessPath(target, adapter), 'scripts', `${action}.sh`);
|
|
356
366
|
if (isExecutable(installedScript)) {
|
|
357
367
|
return { command: installedScript, prefixArgs: [] };
|
|
358
368
|
}
|
|
359
369
|
const command = resolveEntry(
|
|
360
370
|
runnerDir,
|
|
361
|
-
[`
|
|
371
|
+
[`adapters/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
|
|
362
372
|
'exec',
|
|
363
373
|
);
|
|
364
374
|
return { command, prefixArgs: [] };
|
|
365
375
|
}
|
|
366
376
|
|
|
377
|
+
// Mobile live: prepare device in-process then run the verify leaf.
|
|
378
|
+
// Replaces the retired live.sh → launch.sh → mm-harness-mobile-prepare chain.
|
|
379
|
+
// Control flows down only: TS calls prepareMobile(), then spawns verify.sh.
|
|
380
|
+
async function handleMobileLive(
|
|
381
|
+
target: string,
|
|
382
|
+
forwardArgs: string[],
|
|
383
|
+
json: boolean,
|
|
384
|
+
autoDetected: boolean,
|
|
385
|
+
): Promise<number> {
|
|
386
|
+
const platform = argValue(forwardArgs, '--platform') ?? 'ios';
|
|
387
|
+
const watcherPortStr = argValue(forwardArgs, '--watcher-port') ?? process.env.WATCHER_PORT;
|
|
388
|
+
const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : undefined;
|
|
389
|
+
const start = Date.now();
|
|
390
|
+
|
|
391
|
+
if (!json) {
|
|
392
|
+
const detected = autoDetected ? ', auto-detected' : '';
|
|
393
|
+
console.error(`→ live (mobile${detected}) — target: ${target}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const prepResult = await prepareMobile(target, { platform, json, watcherPort });
|
|
397
|
+
if (prepResult.status !== 0) {
|
|
398
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
399
|
+
if (json) {
|
|
400
|
+
console.log(
|
|
401
|
+
harnessSummary('live', 'mobile', target, 'fail', prepResult.status, autoDetected, {
|
|
402
|
+
code: 'MOBILE_PREPARE_FAILED',
|
|
403
|
+
message: `mobile prepare failed (exit ${prepResult.status})`,
|
|
404
|
+
userAction: failureHint('mobile', 'live'),
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
console.error(
|
|
409
|
+
`✗ live mobile failed (exit ${prepResult.status}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return prepResult.status;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Verify leaf: --no-auto-start because launch already started the app.
|
|
416
|
+
const installedVerify = path.join(recipeHarnessPath(target, 'mobile'), 'scripts', 'verify.sh');
|
|
417
|
+
const verifySh = isExecutable(installedVerify)
|
|
418
|
+
? installedVerify
|
|
419
|
+
: resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'exec');
|
|
420
|
+
|
|
421
|
+
const verifyArgs = hasArg(forwardArgs, '--no-auto-start')
|
|
422
|
+
? [...forwardArgs]
|
|
423
|
+
: ['--no-auto-start', ...forwardArgs];
|
|
424
|
+
|
|
425
|
+
const result = spawnSync(verifySh, verifyArgs, {
|
|
426
|
+
stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
|
|
427
|
+
env: process.env,
|
|
428
|
+
});
|
|
429
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
430
|
+
const exitCode = result.status ?? 1;
|
|
431
|
+
|
|
432
|
+
if (json) {
|
|
433
|
+
console.log(
|
|
434
|
+
harnessSummary(
|
|
435
|
+
'live',
|
|
436
|
+
'mobile',
|
|
437
|
+
target,
|
|
438
|
+
exitCode === 0 ? 'pass' : 'fail',
|
|
439
|
+
exitCode,
|
|
440
|
+
autoDetected,
|
|
441
|
+
exitCode === 0
|
|
442
|
+
? undefined
|
|
443
|
+
: { code: 'MOBILE_VERIFY_FAILED', message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint('mobile', 'live') },
|
|
444
|
+
),
|
|
445
|
+
);
|
|
446
|
+
} else if (exitCode === 0) {
|
|
447
|
+
console.error(`✓ live mobile passed (${elapsed}s)`);
|
|
448
|
+
} else {
|
|
449
|
+
console.error(
|
|
450
|
+
`✗ live mobile failed (exit ${exitCode}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return exitCode;
|
|
454
|
+
}
|
|
455
|
+
|
|
367
456
|
export async function handleHarness(argv: string[]): Promise<number> {
|
|
368
457
|
const action = argv[0];
|
|
369
458
|
if (!action || action === '-h' || action === '--help') {
|
|
@@ -388,15 +477,15 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
388
477
|
// parsedAdapter is string|undefined here; not yet validated as a real
|
|
389
478
|
// adapter, so pass undefined rather than forwarding a garbage string.
|
|
390
479
|
const detectError = adapter
|
|
391
|
-
? { code: 'UNSUPPORTED_PLATFORM', message: `unsupported platform: ${adapter}
|
|
392
|
-
: { code: 'ADAPTER_DETECTION_FAILED', message: `could not detect the MetaMask repo type for ${target}
|
|
480
|
+
? { code: 'UNSUPPORTED_PLATFORM', message: `unsupported platform: ${adapter}`, userAction: 'pass --adapter <mobile|extension|core> to specify a supported adapter' }
|
|
481
|
+
: { code: 'ADAPTER_DETECTION_FAILED', message: `could not detect the MetaMask repo type for ${target}`, userAction: ADAPTER_DETECT_NEXT };
|
|
393
482
|
console.log(harnessSummary(harnessAction, undefined, target, 'fail', 2, false, detectError));
|
|
394
483
|
} else {
|
|
395
484
|
harnessUsage();
|
|
396
485
|
console.error(
|
|
397
486
|
adapter
|
|
398
487
|
? `\n✗ unsupported platform: ${adapter}`
|
|
399
|
-
: `\n✗ could not detect the MetaMask repo type for ${target}\n Next:
|
|
488
|
+
: `\n✗ could not detect the MetaMask repo type for ${target}\n Next: ${ADAPTER_DETECT_NEXT}`,
|
|
400
489
|
);
|
|
401
490
|
}
|
|
402
491
|
return 2;
|
|
@@ -407,6 +496,7 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
407
496
|
harnessSummary(harnessAction, adapter, target, 'fail', 2, parsedAdapter === undefined, {
|
|
408
497
|
code: 'UNSUPPORTED_ACTION',
|
|
409
498
|
message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
|
|
499
|
+
userAction: 'run mm-harness doctor to see which actions are supported for this adapter',
|
|
410
500
|
}),
|
|
411
501
|
);
|
|
412
502
|
} else {
|
|
@@ -425,6 +515,11 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
425
515
|
forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
|
|
426
516
|
}
|
|
427
517
|
|
|
518
|
+
// Mobile live is orchestrated in-process (prepareMobile → verify.sh leaf).
|
|
519
|
+
if (adapter === 'mobile' && harnessAction === 'live') {
|
|
520
|
+
return handleMobileLive(target, forwardArgs, json, autoDetected);
|
|
521
|
+
}
|
|
522
|
+
|
|
428
523
|
const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
|
|
429
524
|
if ('error' in dispatch) {
|
|
430
525
|
if (json) {
|
|
@@ -432,6 +527,9 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
432
527
|
harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
|
|
433
528
|
code: 'DISPATCH_UNAVAILABLE',
|
|
434
529
|
message: dispatch.error,
|
|
530
|
+
// The dispatch error embeds "Next: <hint>" — extract it so --json consumers
|
|
531
|
+
// get a clean programmatic escape without parsing the human message.
|
|
532
|
+
userAction: dispatch.error.split('\nNext: ')[1] ?? 'run mm-harness install to complete setup, then retry',
|
|
435
533
|
}),
|
|
436
534
|
);
|
|
437
535
|
} else {
|
|
@@ -460,6 +558,7 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
460
558
|
harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
|
|
461
559
|
code: 'HARNESS_SPAWN_FAILED',
|
|
462
560
|
message: `${harnessAction} ${adapter} could not start: ${result.error.message}`,
|
|
561
|
+
userAction: failureHint(adapter, harnessAction),
|
|
463
562
|
}),
|
|
464
563
|
);
|
|
465
564
|
} else {
|
|
@@ -480,7 +579,7 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
480
579
|
autoDetected,
|
|
481
580
|
exitCode === 0
|
|
482
581
|
? undefined
|
|
483
|
-
: { code: 'HARNESS_FAILED', message: `${harnessAction} ${adapter} failed (exit ${exitCode})
|
|
582
|
+
: { code: 'HARNESS_FAILED', message: `${harnessAction} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(adapter, harnessAction) },
|
|
484
583
|
),
|
|
485
584
|
);
|
|
486
585
|
} else if (exitCode === 0) {
|
|
@@ -491,6 +590,9 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
491
590
|
return exitCode;
|
|
492
591
|
}
|
|
493
592
|
|
|
593
|
+
// userAction is required whenever an error object is present so every --json
|
|
594
|
+
// failure carries a machine-readable escape path — parallel to the `usageOut`
|
|
595
|
+
// enforcement on the CLI layer. Omitting userAction is a compile-time error.
|
|
494
596
|
function harnessSummary(
|
|
495
597
|
action: HarnessAction,
|
|
496
598
|
adapter: MetaMaskRecipeAdapter | undefined,
|
|
@@ -498,7 +600,7 @@ function harnessSummary(
|
|
|
498
600
|
status: 'pass' | 'fail',
|
|
499
601
|
exitCode: number,
|
|
500
602
|
autoDetected: boolean,
|
|
501
|
-
error?: { code: string; message: string },
|
|
603
|
+
error?: { code: string; message: string; userAction: string },
|
|
502
604
|
): string {
|
|
503
605
|
return JSON.stringify({
|
|
504
606
|
schemaVersion: 1,
|
|
@@ -510,7 +612,8 @@ function harnessSummary(
|
|
|
510
612
|
status,
|
|
511
613
|
exitCode,
|
|
512
614
|
// Error contract: every --json failure carries a stable machine code + human
|
|
513
|
-
// message (CLI-SPEC.md §5.1).
|
|
615
|
+
// message (CLI-SPEC.md §5.1). userAction is included when present so callers
|
|
616
|
+
// can surface the reachable escape without parsing the human message.
|
|
514
617
|
...(status === 'fail' && error ? { error } : {}),
|
|
515
618
|
});
|
|
516
619
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
10
11
|
|
|
11
12
|
import { handleHarness } from './harness.ts';
|
|
12
13
|
import { recipeHarnessPath, recipeRuntimePath } from './paths.ts';
|
|
@@ -54,6 +55,24 @@ function overlayPresent(target: string, adapter: MetaMaskRecipeAdapter): boolean
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// Validates that the installed overlay's runner-source stamp points to an existing
|
|
59
|
+
// runner directory whose binary is still present. Two failure modes are detected:
|
|
60
|
+
// 1. Stale stamp — runner dir moved or renamed since install (dir absent).
|
|
61
|
+
// 2. Stale exec target — runner dir exists but its bin/mm-harness was removed
|
|
62
|
+
// (e.g. the package was unlinked/purged inside an otherwise-live dir).
|
|
63
|
+
// Either way the delegate binary will fail to exec; treat the overlay as absent
|
|
64
|
+
// so auto-ensure re-installs it from the current runner.
|
|
65
|
+
function overlayDelegateValid(target: string, adapter: MetaMaskRecipeAdapter): boolean {
|
|
66
|
+
if (adapter === 'core') return true;
|
|
67
|
+
const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
|
|
68
|
+
if (!fs.existsSync(pointer)) return true; // no stamp = running from local runner, valid
|
|
69
|
+
const runnerPath = fs.readFileSync(pointer, 'utf8').trim();
|
|
70
|
+
if (!Boolean(runnerPath) || !fs.existsSync(runnerPath)) return false;
|
|
71
|
+
// Runner dir is present; verify the binary inside still exists — the directory
|
|
72
|
+
// can survive a package removal while the binary inside is deleted.
|
|
73
|
+
return fs.existsSync(path.join(runnerPath, 'bin', 'mm-harness'));
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
// Auto-ensure the runtime overlay (install phase). Missing + heal != off →
|
|
58
77
|
// install inline, emit a first-install notice to stderr, and record the mutation.
|
|
59
78
|
// heal === off disables auto-install (repro-preserving); we proceed and let the
|
|
@@ -67,7 +86,7 @@ export async function ensureOverlay(
|
|
|
67
86
|
): Promise<{ ok: boolean; error?: string }> {
|
|
68
87
|
// core is headless — the engine path used by run/call needs no launch overlay.
|
|
69
88
|
if (adapter === 'core') return { ok: true };
|
|
70
|
-
if (overlayPresent(target, adapter)) return { ok: true };
|
|
89
|
+
if (overlayPresent(target, adapter) && overlayDelegateValid(target, adapter)) return { ok: true };
|
|
71
90
|
if (heal === 'off') return { ok: true };
|
|
72
91
|
|
|
73
92
|
const installBin = process.env.MM_HARNESS_INSTALL_BIN;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Keep the package entrypoint narrow: runner factory, manifest, doctor, and the
|
|
2
|
+
// path/import helpers that library actions are allowed to consume (R9 — no
|
|
3
|
+
// deep-importing src internals past this file).
|
|
4
|
+
export { createDoctorReport } from './doctor.ts';
|
|
5
|
+
export { loadActionManifest, validateManifest } from './manifest.ts';
|
|
6
|
+
export { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from './runner.ts';
|
|
7
|
+
export type { CreateMetaMaskRunnerOptions, MetaMaskDoctorReport, MetaMaskRecipeAdapter } from './types.ts';
|
|
8
|
+
export {
|
|
9
|
+
extensionIdPath,
|
|
10
|
+
importRecipeHarnessRuntimeBrowserExtension,
|
|
11
|
+
importRecipeHarnessRuntimeCdp,
|
|
12
|
+
recipeHarnessPath,
|
|
13
|
+
walletFixturePath,
|
|
14
|
+
} from './paths.ts';
|
|
15
|
+
export { captureActiveRecipeRecordingSnapshot } from './run-recording.ts';
|
|
@@ -223,7 +223,7 @@ export async function runLiveAdapterScript({ platform, action, node, context }:
|
|
|
223
223
|
const script = await resolveLiveAdapter(platform, action);
|
|
224
224
|
if (!script) return null;
|
|
225
225
|
|
|
226
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(), '
|
|
226
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'mm-harness-live-adapter-'));
|
|
227
227
|
const inputPath = path.join(tempDir, 'input.json');
|
|
228
228
|
const outputPath = path.join(tempDir, 'output.json');
|
|
229
229
|
const input = {
|
|
@@ -12,8 +12,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { handleUpdate, maybeNudge } from './commands/update.ts';
|
|
16
|
+
|
|
17
|
+
// …/src → its parent is the package root that holds scripts/.
|
|
18
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
19
|
|
|
18
20
|
// Signal cli.ts NOT to auto-run its own dispatch when we import it below: we
|
|
19
21
|
// reuse its `main` in-process. Must be set BEFORE the dynamic import runs the
|
|
@@ -245,6 +247,26 @@ Example:
|
|
|
245
247
|
mm-harness debug
|
|
246
248
|
mm-harness debug --worker`,
|
|
247
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: 'update',
|
|
252
|
+
summary: 'Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).',
|
|
253
|
+
example: 'mm-harness update',
|
|
254
|
+
helpText: `mm-harness update [flags]
|
|
255
|
+
|
|
256
|
+
Update the globally-installed mm-harness to the npm registry's latest.
|
|
257
|
+
Bare form upgrades (npm i -g @deeeed/metamask-harness@latest) and prints old → new;
|
|
258
|
+
already-current exits 0.
|
|
259
|
+
|
|
260
|
+
--check Report only — exit 0 up-to-date / exit 1 update available (no install)
|
|
261
|
+
--json Machine-readable { current, latest, updateAvailable }
|
|
262
|
+
|
|
263
|
+
A passive once-a-day nudge also prints one stderr line when a newer version exists.
|
|
264
|
+
Silence it with MM_HARNESS_NO_UPDATE_CHECK=1 (auto-off in CI).
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
mm-harness update
|
|
268
|
+
mm-harness update --check --json`,
|
|
269
|
+
},
|
|
248
270
|
{
|
|
249
271
|
name: 'fixtures',
|
|
250
272
|
summary: 'Manage the canonical wallet fixture (wallet DATA only) — sync files / set the wallet.',
|
|
@@ -340,6 +362,11 @@ const HELP_GROUPS: HelpGroup[] = [
|
|
|
340
362
|
blurb: 'install/verify/clean the per-checkout overlay (the everyday commands auto-ensure it)',
|
|
341
363
|
commands: ['install', 'verify', 'cleanup'],
|
|
342
364
|
},
|
|
365
|
+
{
|
|
366
|
+
title: 'MAINTAIN',
|
|
367
|
+
blurb: 'keep the installed harness current with the npm registry',
|
|
368
|
+
commands: ['update'],
|
|
369
|
+
},
|
|
343
370
|
];
|
|
344
371
|
|
|
345
372
|
function commandMeta(name: string): { summary: string; example: string; planned: boolean } {
|
|
@@ -371,6 +398,9 @@ function groupedHelp(): string {
|
|
|
371
398
|
lines.push('SETUP — tab-completion (zsh + bash), sourced from the bundled scripts:');
|
|
372
399
|
lines.push(' mm-harness completions install # print the lines to add to ~/.zshrc / ~/.bashrc');
|
|
373
400
|
lines.push('');
|
|
401
|
+
lines.push('DEV/PROD — the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:');
|
|
402
|
+
lines.push(' MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin');
|
|
403
|
+
lines.push('');
|
|
374
404
|
lines.push('See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).');
|
|
375
405
|
return `${lines.join('\n')}\n`;
|
|
376
406
|
}
|
|
@@ -430,6 +460,11 @@ for (const command of REAL) {
|
|
|
430
460
|
process.stdout.write(`${command.helpText}\n`);
|
|
431
461
|
process.exit(0);
|
|
432
462
|
}
|
|
463
|
+
// `update` is self-contained registry tooling (no engine, no overlay), so it
|
|
464
|
+
// runs directly rather than through the dispatch in cli.ts.
|
|
465
|
+
if (command.name === 'update') {
|
|
466
|
+
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
467
|
+
}
|
|
433
468
|
const argv = command.name === 'actions' && rawArgv.includes('--raw') ? translateActionsRaw(rawArgv) : rawArgv;
|
|
434
469
|
process.exit(await delegate(argv));
|
|
435
470
|
});
|
|
@@ -474,12 +509,38 @@ for (const retired of RETIRED) {
|
|
|
474
509
|
.allowUnknownOption()
|
|
475
510
|
.helpOption(false)
|
|
476
511
|
.argument('[args...]')
|
|
477
|
-
.action(() => {
|
|
512
|
+
.action(async () => {
|
|
513
|
+
// `live --adapter mobile` (or android) is handled in-process via handleMobileLive.
|
|
514
|
+
// All other `live` invocations still teach the replacement.
|
|
515
|
+
if (retired.name === 'live') {
|
|
516
|
+
const adapterIdx = rawArgv.indexOf('--adapter');
|
|
517
|
+
const aliasIdx = rawArgv.indexOf('-a');
|
|
518
|
+
const adapterVal =
|
|
519
|
+
adapterIdx !== -1
|
|
520
|
+
? rawArgv[adapterIdx + 1]
|
|
521
|
+
: aliasIdx !== -1
|
|
522
|
+
? rawArgv[aliasIdx + 1]
|
|
523
|
+
: undefined;
|
|
524
|
+
if (adapterVal === 'mobile' || adapterVal === 'android') {
|
|
525
|
+
process.exit(await delegate(rawArgv));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
478
529
|
console.error(retired.message);
|
|
479
530
|
process.exit(2);
|
|
480
531
|
});
|
|
481
532
|
}
|
|
482
533
|
|
|
534
|
+
// Passive update nudge: one throttled stderr line if a newer version is published.
|
|
535
|
+
// Skipped for update itself (redundant) and for shell/completion tooling (whose
|
|
536
|
+
// stdout is consumed by the shell). stderr-only keeps every --json stdout clean.
|
|
537
|
+
const NUDGE_SKIP: readonly string[] = ['update', 'completions', 'completion-candidates'];
|
|
538
|
+
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
539
|
+
// Fully deferred: no nudge code runs before parseAsync starts. The exit hook
|
|
540
|
+
// registered inside maybeNudge prints the line after command output.
|
|
541
|
+
setImmediate(() => void maybeNudge());
|
|
542
|
+
}
|
|
543
|
+
|
|
483
544
|
if (rawArgv.length === 0) {
|
|
484
545
|
process.stdout.write(groupedHelp());
|
|
485
546
|
process.exit(0);
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
MetaMaskRecipeAdapter,
|
|
12
12
|
} from './types.ts';
|
|
13
13
|
|
|
14
|
-
export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
|
|
14
|
+
export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
15
|
const pathDefaults = readPathDefaults();
|
|
16
16
|
export const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
|
|
17
17
|
export const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
|
|
@@ -26,7 +26,7 @@ export function recipeHarnessRoot() {
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
function readPathDefaults(): { recipeRuntimeDir: string; recipeHarnessRoot: string } {
|
|
29
|
-
const defaultsPath = path.join(runnerDir, '
|
|
29
|
+
const defaultsPath = path.join(runnerDir, 'adapters/shared/path-defaults.json');
|
|
30
30
|
const parsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')) as Partial<{ recipeRuntimeDir: string; recipeHarnessRoot: string }>;
|
|
31
31
|
return {
|
|
32
32
|
recipeRuntimeDir: validateRelativeRecipePath('recipeRuntimeDir', parsed.recipeRuntimeDir || ''),
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
* progress.
|
|
2
|
+
* progress.ts — structured progress events for long recipe CLI operations.
|
|
4
3
|
*
|
|
5
4
|
* Human mode: short lines on stderr.
|
|
6
5
|
* Machine mode: JSONL on stderr when --json or RECIPE_PROGRESS_JSON=1.
|
|
7
6
|
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
|
-
import { color } from './cli-color.
|
|
9
|
+
import { color } from './cli-color.ts';
|
|
10
10
|
|
|
11
|
-
function shouldEmitJson(explicit) {
|
|
11
|
+
function shouldEmitJson(explicit: boolean): boolean {
|
|
12
12
|
if (explicit) return true;
|
|
13
13
|
const env = process.env.RECIPE_PROGRESS_JSON;
|
|
14
14
|
return env === '1' || env === 'true';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface ProgressEvent {
|
|
18
|
+
command?: string;
|
|
19
|
+
phase?: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
elapsedMs?: number;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function emitProgress(event: ProgressEvent, { json = false }: { json?: boolean } = {}): void {
|
|
18
27
|
const payload = {
|
|
19
28
|
schemaVersion: 1,
|
|
20
29
|
type: 'progress',
|
|
@@ -30,28 +39,28 @@ export function emitProgress(event, { json = false } = {}) {
|
|
|
30
39
|
if (payload.phase === 'done') {
|
|
31
40
|
const statusStyle = payload.status === 'pass' ? 'ok' : 'err';
|
|
32
41
|
const statusWord = payload.status === 'pass' ? 'done' : 'failed';
|
|
33
|
-
const
|
|
42
|
+
const cmd = payload.command ? color('cmd', `${payload.command}`, { stream }) : '';
|
|
34
43
|
const elapsed = Number.isFinite(payload.elapsedMs)
|
|
35
|
-
? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
|
|
44
|
+
? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
|
|
36
45
|
: '';
|
|
37
|
-
process.stderr.write(`${prefix}: ${
|
|
46
|
+
process.stderr.write(`${prefix}: ${cmd} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
|
|
38
47
|
return;
|
|
39
48
|
}
|
|
40
|
-
const
|
|
49
|
+
const cmd = payload.command ? `${color('cmd', payload.command, { stream })}: ` : '';
|
|
41
50
|
const phase = payload.phase ? `${color('accent', `[${payload.phase}]`, { stream })} ` : '';
|
|
42
51
|
const elapsed = Number.isFinite(payload.elapsedMs)
|
|
43
|
-
? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
|
|
52
|
+
? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
|
|
44
53
|
: '';
|
|
45
|
-
process.stderr.write(`${prefix}: ${
|
|
54
|
+
process.stderr.write(`${prefix}: ${cmd}${phase}${payload.message || ''}${elapsed}\n`);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
function usage() {
|
|
57
|
+
function usage(): void {
|
|
49
58
|
console.error(`Usage:
|
|
50
|
-
progress
|
|
51
|
-
progress
|
|
59
|
+
progress emit --command <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
|
|
60
|
+
progress done --command <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
async function main() {
|
|
63
|
+
async function main(): Promise<void> {
|
|
55
64
|
const [command, ...rest] = process.argv.slice(2);
|
|
56
65
|
if (!command || command === '--help' || command === '-h') {
|
|
57
66
|
usage();
|
|
@@ -61,7 +70,7 @@ async function main() {
|
|
|
61
70
|
const { values } = parseArgs({
|
|
62
71
|
args: rest,
|
|
63
72
|
options: {
|
|
64
|
-
|
|
73
|
+
command: { type: 'string' },
|
|
65
74
|
phase: { type: 'string' },
|
|
66
75
|
message: { type: 'string' },
|
|
67
76
|
status: { type: 'string' },
|
|
@@ -76,7 +85,7 @@ async function main() {
|
|
|
76
85
|
|
|
77
86
|
if (command === 'emit') {
|
|
78
87
|
emitProgress({
|
|
79
|
-
|
|
88
|
+
command: values.command || 'recipe',
|
|
80
89
|
phase: values.phase || 'working',
|
|
81
90
|
message: values.message || '',
|
|
82
91
|
elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
|
|
@@ -86,7 +95,7 @@ async function main() {
|
|
|
86
95
|
|
|
87
96
|
if (command === 'done') {
|
|
88
97
|
emitProgress({
|
|
89
|
-
|
|
98
|
+
command: values.command || 'recipe',
|
|
90
99
|
phase: 'done',
|
|
91
100
|
status: values.status || 'pass',
|
|
92
101
|
message: values.message || '',
|
|
@@ -99,9 +108,10 @@ async function main() {
|
|
|
99
108
|
process.exit(2);
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? '');
|
|
112
|
+
if (isMain) {
|
|
113
|
+
main().catch((error: unknown) => {
|
|
104
114
|
console.error(error instanceof Error ? error.message : String(error));
|
|
105
115
|
process.exit(1);
|
|
106
116
|
});
|
|
107
|
-
}
|
|
117
|
+
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# Extension runtime lifecycle (internal notes)
|
|
2
|
-
|
|
3
|
-
Internal implementation notes for the MetaMask Extension runtime lifecycle — the
|
|
4
|
-
self-healing path that gets a checkout from "installed" to "a live, verified
|
|
5
|
-
extension under CDP control". These are **not** a public command surface: the
|
|
6
|
-
probes below are internal functions, composed for you by the daily-loop commands
|
|
7
|
-
(`launch --verify`) and the readiness commands (`doctor --fix`, `verify`). The
|
|
8
|
-
public surface is defined in [CLI-SPEC.md](CLI-SPEC.md); this file explains what
|
|
9
|
-
happens underneath.
|
|
10
|
-
|
|
11
|
-
The runner is the source of truth for Extension harness injection, readiness
|
|
12
|
-
decisions, extension-id resolution, and live CDP health checks.
|
|
13
|
-
|
|
14
|
-
## Public entry points
|
|
15
|
-
|
|
16
|
-
- **`install`** writes the Extension runtime overlay under the configured harness
|
|
17
|
-
root. CI/agents run it explicitly; the daily-loop commands auto-ensure it.
|
|
18
|
-
- **`verify`** confirms the overlay is present and healthy — a live CDP health
|
|
19
|
-
probe when a runtime is reachable, no launch.
|
|
20
|
-
- **`launch --verify`** installs the overlay if missing, launches/attaches the
|
|
21
|
-
extension, and polls CDP until the runtime is ready.
|
|
22
|
-
- **`doctor` / `doctor --fix`** report readiness and (with `--fix`) run the
|
|
23
|
-
healing steps below WITHOUT starting the app.
|
|
24
|
-
|
|
25
|
-
## Internal lifecycle stages
|
|
26
|
-
|
|
27
|
-
The healing path composes these internal functions in order. Each is idempotent
|
|
28
|
-
and reports the cheapest next action, so the composed path converges rather than
|
|
29
|
-
rebuilding from scratch.
|
|
30
|
-
|
|
31
|
-
### Extension-id resolution
|
|
32
|
-
|
|
33
|
-
Resolves the deterministic unpacked extension id from
|
|
34
|
-
`dist/chrome/manifest.json`, and can verify it against a live CDP target. Every
|
|
35
|
-
later stage keys off this id, so it runs first.
|
|
36
|
-
|
|
37
|
-
### Readiness decision
|
|
38
|
-
|
|
39
|
-
Inspects the checkout + live browser and returns the cheapest next action:
|
|
40
|
-
`install`, `build`, `relaunch`, or `ready`, plus the concrete `actions[]` to
|
|
41
|
-
execute. The healing path branches on this decision instead of always launching.
|
|
42
|
-
|
|
43
|
-
### Runtime launch
|
|
44
|
-
|
|
45
|
-
Launches Chrome with the installed harness helper, seeds the wallet fixture, and
|
|
46
|
-
runs live smoke verification. Two tiers: reuse the existing dist (quick relaunch),
|
|
47
|
-
or the clean-build path — clear the webpack cache, start the harness-owned
|
|
48
|
-
watcher, wait for a clean compile, then launch and verify.
|
|
49
|
-
|
|
50
|
-
### Convergence
|
|
51
|
-
|
|
52
|
-
Converges the live browser to exactly one healthy `home.html` tab (closing
|
|
53
|
-
strays, reopening if needed) and confirms it with the health probe. This is the
|
|
54
|
-
step that makes "launch" deterministic across a messy pre-existing browser state.
|
|
55
|
-
|
|
56
|
-
### Health probe
|
|
57
|
-
|
|
58
|
-
Read-only liveness probe for the running extension over CDP. Used both as the
|
|
59
|
-
final gate of the convergence step and standalone by `verify` to answer "is the
|
|
60
|
-
runtime healthy right now?" without mutating anything.
|