@deeeed/metamask-harness 0.2.0 → 0.3.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 +17 -0
- package/README.md +20 -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 +250 -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 +5 -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 +11 -3
- 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 +25 -18
- package/src/commands/debug.ts +61 -0
- package/src/commands/fixtures.ts +196 -0
- package/{runner/src → src}/commands/launch.ts +185 -41
- package/src/commands/logs.ts +86 -0
- package/{runner/src → src}/commands/shared.ts +26 -26
- package/{runner/src → src}/harness.ts +97 -10
- package/src/index.ts +15 -0
- package/{runner/src → src}/live-adapter-contract.ts +1 -1
- package/{runner/src → src}/mm-harness-cli.ts +19 -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}/heal-bounds.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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Shared helpers for the mm-harness command modules: flag parsing, adapter
|
|
2
|
-
// resolution,
|
|
3
|
-
// module
|
|
4
|
-
// that live under orchestration/porcelain/; these helpers are the common seam.
|
|
2
|
+
// resolution, script composition, and the teaching-error emitter. Each command
|
|
3
|
+
// module composes adapters/ scripts directly via spawnScript.
|
|
5
4
|
//
|
|
6
|
-
// Composition
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// Composition seam (overridable for contract tests):
|
|
6
|
+
// MM_HARNESS_SCRIPT_BIN_<STEM> — override a specific script by its basename.
|
|
7
|
+
// For node invocations (bin === process.execPath) the stem is derived from the
|
|
8
|
+
// script path in args[0], e.g. MM_HARNESS_SCRIPT_BIN_OPEN_DEBUG_MJS.
|
|
10
9
|
|
|
11
10
|
import { spawnSync } from 'node:child_process';
|
|
12
11
|
import path from 'node:path';
|
|
@@ -89,36 +88,37 @@ export function resolveAdapter(
|
|
|
89
88
|
return detectAdapter(target);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
if (adapter === 'mobile') {
|
|
94
|
-
return process.env.MM_HARNESS_MM_RECIPE_BIN ?? path.join(runnerDir, 'orchestration', 'porcelain', 'mm-recipe');
|
|
95
|
-
}
|
|
96
|
-
return process.env.MM_HARNESS_MME_RECIPE_BIN ?? path.join(runnerDir, 'orchestration', 'porcelain', 'mme-recipe');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface PorcelainResult {
|
|
91
|
+
export interface ScriptResult {
|
|
100
92
|
status: number;
|
|
101
93
|
output: string;
|
|
102
94
|
}
|
|
103
95
|
|
|
104
|
-
// Compose
|
|
105
|
-
// classification) and, in human mode, forwarded to
|
|
106
|
-
//
|
|
107
|
-
export function
|
|
108
|
-
|
|
96
|
+
// Compose an adapters/ script directly. Output is always captured (needed for
|
|
97
|
+
// heal classification) and, in human mode, forwarded to stderr. --json keeps
|
|
98
|
+
// stdout clean for the machine summary.
|
|
99
|
+
export function spawnScript(
|
|
100
|
+
script: string,
|
|
109
101
|
args: string[],
|
|
110
|
-
|
|
102
|
+
cwd: string,
|
|
111
103
|
json: boolean,
|
|
112
|
-
):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
104
|
+
): ScriptResult {
|
|
105
|
+
// For node invocations (script === process.execPath) the seam stem is derived
|
|
106
|
+
// from args[0] so each spawned script has its own override key.
|
|
107
|
+
const isNodeScript = script === process.execPath && args.length > 0;
|
|
108
|
+
const stem = isNodeScript
|
|
109
|
+
? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
|
|
110
|
+
: path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
|
|
111
|
+
const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
|
|
112
|
+
const bin = override ?? script;
|
|
113
|
+
const spawnArgs = override !== undefined && isNodeScript ? args.slice(1) : args;
|
|
114
|
+
const result = spawnSync(bin, spawnArgs, {
|
|
115
|
+
cwd,
|
|
116
116
|
encoding: 'utf8',
|
|
117
117
|
env: process.env,
|
|
118
118
|
maxBuffer: 64 * 1024 * 1024,
|
|
119
119
|
});
|
|
120
120
|
if (result.error) {
|
|
121
|
-
return { status: 1, output: `
|
|
121
|
+
return { status: 1, output: `script could not start (${path.basename(script)}): ${result.error.message}` };
|
|
122
122
|
}
|
|
123
123
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
124
124
|
if (!json && output) process.stderr.write(output);
|
|
@@ -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)
|
|
@@ -277,11 +278,14 @@ function resolveEntry(base: string, candidates: string[], mode: 'exec' | 'file')
|
|
|
277
278
|
|
|
278
279
|
// Content of an installed overlay's runner source pointer, if present. Command
|
|
279
280
|
// substitution in the skill stripped trailing newlines, so trim here too.
|
|
281
|
+
// Validates the path exists so stale stamps (e.g. from a renamed package dir)
|
|
282
|
+
// fall back to the current runnerDir rather than producing ENOENT at cleanup.
|
|
280
283
|
function installedRunnerSource(target: string, adapter: MetaMaskRecipeAdapter): string | undefined {
|
|
281
284
|
const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
|
|
282
285
|
if (!fs.existsSync(pointer)) return undefined;
|
|
283
286
|
const value = fs.readFileSync(pointer, 'utf8').trim();
|
|
284
|
-
|
|
287
|
+
if (!value || !fs.existsSync(value)) return undefined;
|
|
288
|
+
return value;
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
function isExecutable(file: string): boolean {
|
|
@@ -299,15 +303,15 @@ interface HarnessDispatch {
|
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
const INJECT_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
302
|
-
mobile: { entry: '
|
|
303
|
-
extension: { entry: '
|
|
304
|
-
core: { entry: '
|
|
306
|
+
mobile: { entry: 'adapters/mobile/inject.sh', fallback: 'scripts/inject-mobile-harness.sh' },
|
|
307
|
+
extension: { entry: 'adapters/extension/inject.mjs', fallback: 'scripts/inject-extension-harness.mjs' },
|
|
308
|
+
core: { entry: 'adapters/core/inject.sh', fallback: 'scripts/inject-core-harness.sh' },
|
|
305
309
|
};
|
|
306
310
|
|
|
307
311
|
const CLEANUP_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
308
|
-
mobile: { entry: '
|
|
309
|
-
extension: { entry: '
|
|
310
|
-
core: { entry: '
|
|
312
|
+
mobile: { entry: 'adapters/mobile/cleanup.sh', fallback: 'scripts/cleanup-mobile-harness.sh' },
|
|
313
|
+
extension: { entry: 'adapters/extension/cleanup.mjs', fallback: 'scripts/cleanup-extension-harness.mjs' },
|
|
314
|
+
core: { entry: 'adapters/core/cleanup.sh', fallback: 'scripts/cleanup-core-harness.sh' },
|
|
311
315
|
};
|
|
312
316
|
|
|
313
317
|
// Resolve the exact command + fixed prefix args to exec, mirroring
|
|
@@ -337,7 +341,7 @@ function resolveHarnessDispatch(
|
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
if (adapter === 'core' && action === 'verify') {
|
|
340
|
-
const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', '
|
|
344
|
+
const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', 'mm-harness');
|
|
341
345
|
if (!isExecutable(delegate)) {
|
|
342
346
|
return {
|
|
343
347
|
error:
|
|
@@ -351,19 +355,97 @@ function resolveHarnessDispatch(
|
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
// mobile/extension live|verify: prefer the installed helper, else the runner's
|
|
354
|
-
// own
|
|
358
|
+
// own adapters/scripts helper (first executable candidate wins).
|
|
355
359
|
const installedScript = path.join(recipeHarnessPath(target, adapter), 'scripts', `${action}.sh`);
|
|
356
360
|
if (isExecutable(installedScript)) {
|
|
357
361
|
return { command: installedScript, prefixArgs: [] };
|
|
358
362
|
}
|
|
359
363
|
const command = resolveEntry(
|
|
360
364
|
runnerDir,
|
|
361
|
-
[`
|
|
365
|
+
[`adapters/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
|
|
362
366
|
'exec',
|
|
363
367
|
);
|
|
364
368
|
return { command, prefixArgs: [] };
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
// Mobile live: prepare device in-process then run the verify leaf.
|
|
372
|
+
// Replaces the retired live.sh → launch.sh → mm-harness-mobile-prepare chain.
|
|
373
|
+
// Control flows down only: TS calls prepareMobile(), then spawns verify.sh.
|
|
374
|
+
async function handleMobileLive(
|
|
375
|
+
target: string,
|
|
376
|
+
forwardArgs: string[],
|
|
377
|
+
json: boolean,
|
|
378
|
+
autoDetected: boolean,
|
|
379
|
+
): Promise<number> {
|
|
380
|
+
const platform = argValue(forwardArgs, '--platform') ?? 'ios';
|
|
381
|
+
const watcherPortStr = argValue(forwardArgs, '--watcher-port') ?? process.env.WATCHER_PORT;
|
|
382
|
+
const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : undefined;
|
|
383
|
+
const start = Date.now();
|
|
384
|
+
|
|
385
|
+
if (!json) {
|
|
386
|
+
const detected = autoDetected ? ', auto-detected' : '';
|
|
387
|
+
console.error(`→ live (mobile${detected}) — target: ${target}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const prepResult = await prepareMobile(target, { platform, json, watcherPort });
|
|
391
|
+
if (prepResult.status !== 0) {
|
|
392
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
393
|
+
if (json) {
|
|
394
|
+
console.log(
|
|
395
|
+
harnessSummary('live', 'mobile', target, 'fail', prepResult.status, autoDetected, {
|
|
396
|
+
code: 'MOBILE_PREPARE_FAILED',
|
|
397
|
+
message: `mobile prepare failed (exit ${prepResult.status})`,
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
console.error(
|
|
402
|
+
`✗ live mobile failed (exit ${prepResult.status}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return prepResult.status;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Verify leaf: --no-auto-start because launch already started the app.
|
|
409
|
+
const installedVerify = path.join(recipeHarnessPath(target, 'mobile'), 'scripts', 'verify.sh');
|
|
410
|
+
const verifySh = isExecutable(installedVerify)
|
|
411
|
+
? installedVerify
|
|
412
|
+
: resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'exec');
|
|
413
|
+
|
|
414
|
+
const verifyArgs = hasArg(forwardArgs, '--no-auto-start')
|
|
415
|
+
? [...forwardArgs]
|
|
416
|
+
: ['--no-auto-start', ...forwardArgs];
|
|
417
|
+
|
|
418
|
+
const result = spawnSync(verifySh, verifyArgs, {
|
|
419
|
+
stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
|
|
420
|
+
env: process.env,
|
|
421
|
+
});
|
|
422
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
423
|
+
const exitCode = result.status ?? 1;
|
|
424
|
+
|
|
425
|
+
if (json) {
|
|
426
|
+
console.log(
|
|
427
|
+
harnessSummary(
|
|
428
|
+
'live',
|
|
429
|
+
'mobile',
|
|
430
|
+
target,
|
|
431
|
+
exitCode === 0 ? 'pass' : 'fail',
|
|
432
|
+
exitCode,
|
|
433
|
+
autoDetected,
|
|
434
|
+
exitCode === 0
|
|
435
|
+
? undefined
|
|
436
|
+
: { code: 'MOBILE_VERIFY_FAILED', message: `mobile live verify failed (exit ${exitCode})` },
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
} else if (exitCode === 0) {
|
|
440
|
+
console.error(`✓ live mobile passed (${elapsed}s)`);
|
|
441
|
+
} else {
|
|
442
|
+
console.error(
|
|
443
|
+
`✗ live mobile failed (exit ${exitCode}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return exitCode;
|
|
447
|
+
}
|
|
448
|
+
|
|
367
449
|
export async function handleHarness(argv: string[]): Promise<number> {
|
|
368
450
|
const action = argv[0];
|
|
369
451
|
if (!action || action === '-h' || action === '--help') {
|
|
@@ -425,6 +507,11 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
425
507
|
forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
|
|
426
508
|
}
|
|
427
509
|
|
|
510
|
+
// Mobile live is orchestrated in-process (prepareMobile → verify.sh leaf).
|
|
511
|
+
if (adapter === 'mobile' && harnessAction === 'live') {
|
|
512
|
+
return handleMobileLive(target, forwardArgs, json, autoDetected);
|
|
513
|
+
}
|
|
514
|
+
|
|
428
515
|
const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
|
|
429
516
|
if ('error' in dispatch) {
|
|
430
517
|
if (json) {
|
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,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
15
|
+
// …/src → its parent is the package root that holds scripts/.
|
|
16
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
17
|
|
|
18
18
|
// Signal cli.ts NOT to auto-run its own dispatch when we import it below: we
|
|
19
19
|
// reuse its `main` in-process. Must be set BEFORE the dynamic import runs the
|
|
@@ -474,7 +474,23 @@ for (const retired of RETIRED) {
|
|
|
474
474
|
.allowUnknownOption()
|
|
475
475
|
.helpOption(false)
|
|
476
476
|
.argument('[args...]')
|
|
477
|
-
.action(() => {
|
|
477
|
+
.action(async () => {
|
|
478
|
+
// `live --adapter mobile` (or android) is handled in-process via handleMobileLive.
|
|
479
|
+
// All other `live` invocations still teach the replacement.
|
|
480
|
+
if (retired.name === 'live') {
|
|
481
|
+
const adapterIdx = rawArgv.indexOf('--adapter');
|
|
482
|
+
const aliasIdx = rawArgv.indexOf('-a');
|
|
483
|
+
const adapterVal =
|
|
484
|
+
adapterIdx !== -1
|
|
485
|
+
? rawArgv[adapterIdx + 1]
|
|
486
|
+
: aliasIdx !== -1
|
|
487
|
+
? rawArgv[aliasIdx + 1]
|
|
488
|
+
: undefined;
|
|
489
|
+
if (adapterVal === 'mobile' || adapterVal === 'android') {
|
|
490
|
+
process.exit(await delegate(rawArgv));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
478
494
|
console.error(retired.message);
|
|
479
495
|
process.exit(2);
|
|
480
496
|
});
|
|
@@ -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.
|
package/docs/harness-cli.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# `mm-harness` — Command Reference
|
|
2
|
-
|
|
3
|
-
> This page is a pointer. The full, authoritative command contract lives in
|
|
4
|
-
> [CLI-SPEC.md](CLI-SPEC.md); the before→after overview is in
|
|
5
|
-
> [MENTAL-MODEL.md](MENTAL-MODEL.md). This file exists only so links to
|
|
6
|
-
> `harness-cli.md` keep resolving.
|
|
7
|
-
|
|
8
|
-
`mm-harness` is the MetaMask recipe harness — the single front door for launching
|
|
9
|
-
the app, proving behavior, and managing the per-checkout **runtime overlay**. It
|
|
10
|
-
is the only bin: platform (`mobile | extension | core`) is auto-detected from the
|
|
11
|
-
checkout, a positional target forces it (`mm-harness launch ios`), and depth lives
|
|
12
|
-
in flags (`--json`, ports, build tiers), never in extra command names.
|
|
13
|
-
|
|
14
|
-
Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
|
|
15
|
-
(`@farmslot/recipe-harness`). The files the tool installs into a target checkout
|
|
16
|
-
are the **runtime overlay** — never "the harness".
|
|
17
|
-
|
|
18
|
-
## Command surface (all shipped)
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
DAILY LOOP launch · logs · debug · fixtures what a teammate runs many times a day
|
|
22
|
-
DISCOVER actions · call · flows compose recipes from the vocabulary + flow library
|
|
23
|
-
PROVE run · doctor run recipes, check readiness
|
|
24
|
-
RUNTIME OVERLAY install · verify · cleanup install/verify/clean the per-checkout overlay
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Grammar: `mm-harness <command> [target] [flags]`. `--json` is the agent contract on
|
|
28
|
-
every command; the bare command is the human happy path.
|
|
29
|
-
|
|
30
|
-
- `run` validates first, then executes; `run --plan` validates and prints the plan
|
|
31
|
-
without touching a device.
|
|
32
|
-
- `call <action>` runs one action from the adapter vocabulary through the same
|
|
33
|
-
engine path `run` uses.
|
|
34
|
-
- `doctor` is read-only; `doctor --fix` runs the self-healing steps (overlay
|
|
35
|
-
auto-ensure; never a fixture reseed) without launching the app and reports
|
|
36
|
-
`fixed[]` / `failed[]`.
|
|
37
|
-
- `flows` lists the recipe library (bare = `flows list`); `flows promote` keeps a
|
|
38
|
-
proven flow.
|
|
39
|
-
- `install` / `verify` / `cleanup` manage the runtime overlay; the DAILY LOOP
|
|
40
|
-
commands auto-ensure it.
|
|
41
|
-
|
|
42
|
-
For flags, exit-code taxonomy, `--json` shapes, and per-command detail, see
|
|
43
|
-
[CLI-SPEC.md](CLI-SPEC.md).
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Mobile historical compatibility overlays
|
|
2
|
-
|
|
3
|
-
These patch files document temporary source overlays used by Recipe v1 historical
|
|
4
|
-
injection evals when an older MetaMask Mobile checkout cannot boot under the
|
|
5
|
-
current local Xcode/Hermes toolchain.
|
|
6
|
-
|
|
7
|
-
They are intentionally kept in the external runner repository, not Farmslot root
|
|
8
|
-
scripts, so a `/recipe-harness` skill can apply the same reversible overlay before
|
|
9
|
-
a historical rebuild and record the overlay path in validation evidence.
|
|
10
|
-
|
|
11
|
-
- `rn81-message-event-source.patch`: adds a read-only `MessageEvent.prototype.source`
|
|
12
|
-
getter for React Native 0.81 historical checkouts whose bundled polyfill omits
|
|
13
|
-
it while CDP websocket clients expect browser-compatible `MessageEvent` shape.
|
package/orchestration/doctor.mjs
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// doctor.mjs — orchestration feature-surface doctor
|
|
3
|
-
//
|
|
4
|
-
// Purpose:
|
|
5
|
-
// Verifies orchestration/manifest.json against reality: every entry point
|
|
6
|
-
// exists; every script entry answers --help with exit 0; and every script
|
|
7
|
-
// under orchestration/{mobile,extension,core} is listed (no doc drift).
|
|
8
|
-
//
|
|
9
|
-
// Inputs (flags): --json (machine-readable report)
|
|
10
|
-
// Outputs: per-feature lines (or JSON report) on stdout.
|
|
11
|
-
// Exit 0 — surface healthy; 1 — missing entry, failing --help, or
|
|
12
|
-
// unlisted script; 2 — bad args.
|
|
13
|
-
// Never touches: anything (read-only + --help subprocesses).
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
|
|
19
|
-
let json = false;
|
|
20
|
-
for (const arg of process.argv.slice(2)) {
|
|
21
|
-
if (arg === '--json') json = true;
|
|
22
|
-
else if (arg === '-h' || arg === '--help') {
|
|
23
|
-
console.log('Usage: doctor.mjs [--json]');
|
|
24
|
-
process.exit(0);
|
|
25
|
-
} else {
|
|
26
|
-
console.error(`Unknown arg: ${arg}`);
|
|
27
|
-
process.exit(2);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const orchestrationDir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const runnerRoot = path.resolve(orchestrationDir, '..');
|
|
33
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(orchestrationDir, 'manifest.json'), 'utf8'));
|
|
34
|
-
|
|
35
|
-
const results = [];
|
|
36
|
-
for (const feature of manifest.features) {
|
|
37
|
-
const entryAbs = path.join(runnerRoot, feature.entry);
|
|
38
|
-
const checks = { exists: fs.existsSync(entryAbs), help: null };
|
|
39
|
-
if (checks.exists && (feature.kind === 'bash' || feature.kind === 'node')) {
|
|
40
|
-
const runner = feature.kind === 'bash' ? 'bash' : process.execPath;
|
|
41
|
-
const run = spawnSync(runner, [entryAbs, '--help'], { encoding: 'utf8', timeout: 30000 });
|
|
42
|
-
checks.help = run.status === 0;
|
|
43
|
-
}
|
|
44
|
-
const ok = checks.exists && (checks.help === null || checks.help === true);
|
|
45
|
-
results.push({ id: feature.id, entry: feature.entry, kind: feature.kind, ...checks, ok });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// drift guard: every script in the orchestration trees must be listed
|
|
49
|
-
const listed = new Set(manifest.features.map((feature) => feature.entry));
|
|
50
|
-
const unlisted = [];
|
|
51
|
-
for (const platform of ['mobile', 'extension', 'core', 'lib']) {
|
|
52
|
-
const dir = path.join(orchestrationDir, platform);
|
|
53
|
-
if (!fs.existsSync(dir)) continue;
|
|
54
|
-
for (const entry of fs.readdirSync(dir)) {
|
|
55
|
-
const rel = `orchestration/${platform}/${entry}`;
|
|
56
|
-
if (!/\.(?:sh|mjs|cjs|json)$/u.test(entry)) continue;
|
|
57
|
-
if (!listed.has(rel)) unlisted.push(rel);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const failed = results.filter((result) => !result.ok);
|
|
62
|
-
const status = failed.length === 0 && unlisted.length === 0 ? 'pass' : 'fail';
|
|
63
|
-
if (json) {
|
|
64
|
-
console.log(JSON.stringify({ status, features: results, unlisted }, null, 2));
|
|
65
|
-
} else {
|
|
66
|
-
for (const result of results) {
|
|
67
|
-
console.log(`${result.ok ? 'ok ' : 'FAIL'} ${result.id} (${result.entry})${result.help === false ? ' [--help failed]' : ''}${result.exists ? '' : ' [missing]'}`);
|
|
68
|
-
}
|
|
69
|
-
for (const rel of unlisted) console.log(`FAIL unlisted script not in manifest: ${rel}`);
|
|
70
|
-
console.log(`orchestration surface: ${status} (${results.length - failed.length}/${results.length} features, ${unlisted.length} unlisted)`);
|
|
71
|
-
}
|
|
72
|
-
process.exit(status === 'pass' ? 0 : 1);
|