@deeeed/metamask-harness 0.3.9 → 0.5.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 +45 -1
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +315 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/slot-ports.js +146 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/ADAPTER-SURFACE.md +119 -0
- package/docs/CLI-SPEC.md +26 -3
- package/docs/UX-PRINCIPLES.md +3 -0
- package/package.json +10 -2
- package/src/adapters/core/surface.ts +71 -0
- package/src/adapters/extension/surface.ts +88 -0
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/surface.ts +71 -0
- package/src/adapters/slot-ports.ts +165 -0
- package/src/adapters/surface.ts +117 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +239 -49
- package/src/commands/debug.ts +3 -1
- package/src/commands/fixtures.ts +13 -8
- package/src/commands/launch.ts +7 -156
- package/src/commands/logs.ts +29 -13
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +71 -18
package/src/cli.ts
CHANGED
|
@@ -28,6 +28,8 @@ import { ensureExtensionReady } from './adapters/extension/ensure-ready.ts';
|
|
|
28
28
|
import { resolveExtensionId } from './adapters/extension/extension-id.ts';
|
|
29
29
|
import { decideExtensionReadiness } from './adapters/extension/runtime-decision.ts';
|
|
30
30
|
import { decideMobileReadiness } from './adapters/mobile/runtime-decision.ts';
|
|
31
|
+
import { getAdapterSurface } from './adapters/surface.ts';
|
|
32
|
+
import type { AdapterRuntimeStatus } from './adapters/surface.ts';
|
|
31
33
|
// NOTE: extension-runtime.ts loads the recipe harness at module scope, so it
|
|
32
34
|
// is imported LAZILY (dynamic import) only inside the handlers that drive a live
|
|
33
35
|
// runtime. Static-import it here and every command — manifest, doctor,
|
|
@@ -66,6 +68,7 @@ type CliOptions = Record<string, CliOptionValue>;
|
|
|
66
68
|
interface ParsedArgs {
|
|
67
69
|
positional: string[];
|
|
68
70
|
options: CliOptions;
|
|
71
|
+
rawArgv: string[];
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
interface RuntimeOptions {
|
|
@@ -103,6 +106,7 @@ const COMMANDS: Record<string, (args: ParsedArgs) => Promise<number>> = {
|
|
|
103
106
|
'runtime-health': handleRuntimeHealth,
|
|
104
107
|
'runtime-decision': handleRuntimeDecision,
|
|
105
108
|
'runtime-launch': handleRuntimeLaunch,
|
|
109
|
+
provision: handleProvision,
|
|
106
110
|
'resolve-extension': handleResolveExtension,
|
|
107
111
|
'ensure-ready': handleEnsureReady,
|
|
108
112
|
run: handleRun,
|
|
@@ -112,7 +116,7 @@ const COMMANDS: Record<string, (args: ParsedArgs) => Promise<number>> = {
|
|
|
112
116
|
// Top-level runtime-overlay commands (the tool installs a runtime overlay).
|
|
113
117
|
// Top-level overlay commands route to handleHarness; the `harness <sub>`
|
|
114
118
|
// subcommand form has been removed from the CLI surface.
|
|
115
|
-
const OVERLAY_COMMANDS: readonly string[] = ['install', 'verify', 'cleanup', 'live'];
|
|
119
|
+
const OVERLAY_COMMANDS: readonly string[] = ['install', 'provision', 'verify', 'cleanup', 'live'];
|
|
116
120
|
|
|
117
121
|
function usage() {
|
|
118
122
|
// Help is organized by the harness mental model: mm-harness IS the tool; a
|
|
@@ -174,7 +178,7 @@ See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).
|
|
|
174
178
|
function parseArgs(argv: string[], command?: string): ParsedArgs {
|
|
175
179
|
const positional: string[] = [];
|
|
176
180
|
const options: CliOptions = {};
|
|
177
|
-
const booleanOptions = new Set(['json', 'launchExistingDist', 'startWatch', 'record', 'plan', 'raw', 'fix']);
|
|
181
|
+
const booleanOptions = new Set(['json', 'launchExistingDist', 'startWatch', 'record', 'plan', 'raw', 'fix', 'force', 'resolveOnly']);
|
|
178
182
|
for (let i = 0; i < argv.length; i += 1) {
|
|
179
183
|
const arg = argv[i];
|
|
180
184
|
if (!arg.startsWith('--')) {
|
|
@@ -214,7 +218,7 @@ function parseArgs(argv: string[], command?: string): ParsedArgs {
|
|
|
214
218
|
options[key] = argv[i + 1];
|
|
215
219
|
i += 1;
|
|
216
220
|
}
|
|
217
|
-
return { positional, options };
|
|
221
|
+
return { positional, options, rawArgv: [...argv] };
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
function parseRecordVideoMode(value: string | undefined): false | 'full-run' {
|
|
@@ -247,6 +251,20 @@ function optionFlag(options: CliOptions, key: string): boolean {
|
|
|
247
251
|
return value === true;
|
|
248
252
|
}
|
|
249
253
|
|
|
254
|
+
|
|
255
|
+
function applyRuntimeDirOption(options: CliOptions): void {
|
|
256
|
+
const runtimeDir = optionString(options, 'runtimeDir');
|
|
257
|
+
if (runtimeDir) process.env.RECIPE_RUNTIME_DIR = runtimeDir;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function applyWatcherPortOption(options: CliOptions): void {
|
|
261
|
+
const watcherPort = optionString(options, 'watcherPort');
|
|
262
|
+
if (!watcherPort) return;
|
|
263
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
264
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
265
|
+
process.env.METRO_PORT = watcherPort;
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
function requiredOption(options: CliOptions, key: string, message: string): string {
|
|
251
269
|
const value = optionString(options, key);
|
|
252
270
|
if (!value) throw usageError(message);
|
|
@@ -448,6 +466,7 @@ async function handleActions({ options }: ParsedArgs): Promise<number> {
|
|
|
448
466
|
}
|
|
449
467
|
|
|
450
468
|
async function handleDoctor({ options }: ParsedArgs): Promise<number> {
|
|
469
|
+
applyRuntimeDirOption(options);
|
|
451
470
|
const target = targetPath(options);
|
|
452
471
|
// Share the exact detect-from-target logic the overlay commands (verify/install/
|
|
453
472
|
// cleanup) use: when --adapter/--platform is omitted, auto-detect from the
|
|
@@ -476,35 +495,33 @@ async function handleDoctor({ options }: ParsedArgs): Promise<number> {
|
|
|
476
495
|
}
|
|
477
496
|
|
|
478
497
|
const result = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
479
|
-
// Live runtime state, per platform
|
|
480
|
-
// the
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const report = await mobileRuntimeStatus(target, { watcherPort });
|
|
490
|
-
runtime = {
|
|
491
|
-
decision: report.decision,
|
|
492
|
-
reasonCode: report.reasonCode,
|
|
493
|
-
reasons: report.reasons,
|
|
494
|
-
deps: report.checks?.deps?.status,
|
|
495
|
-
metro: report.checks?.metro?.status,
|
|
496
|
-
};
|
|
497
|
-
} catch { /* readiness probe unavailable — doctor stays useful without it */ }
|
|
498
|
-
}
|
|
498
|
+
// Live runtime state, per platform, via the adapter surface: every adapter
|
|
499
|
+
// reports the same read-only readiness shape (decision + reasons, deps currency,
|
|
500
|
+
// and its dev server where it has one) so doctor renders one line the same way
|
|
501
|
+
// for mobile, extension, and core. doctor never branches on adapter for this.
|
|
502
|
+
let runtime: AdapterRuntimeStatus | undefined;
|
|
503
|
+
try {
|
|
504
|
+
const surface = getAdapterSurface(adapter);
|
|
505
|
+
surface.resolveSlotPorts(target);
|
|
506
|
+
runtime = await surface.runtimeStatus(target);
|
|
507
|
+
} catch { /* readiness probe unavailable — doctor stays useful without it */ }
|
|
499
508
|
if (json) console.log(JSON.stringify({ ...result, runtime }, null, 2));
|
|
500
509
|
else {
|
|
501
510
|
const out = (style: string, text: string) => color(style, text, { stream: process.stdout });
|
|
502
511
|
const stateStyle = (value: string | undefined, good: string) => (value === good ? 'ok' : 'warn');
|
|
503
512
|
console.log(`${out(result.status === 'pass' ? 'ok' : 'err', result.status)} ${out('bold', adapter)} ${result.compatibilityMode} ${out('dim', `manifest=${actionManifestPath}`)}`);
|
|
504
513
|
if (runtime) {
|
|
505
|
-
const
|
|
514
|
+
const provisionedDepsPending = runtime.reasonCode === 'app-installed-deps-pending';
|
|
515
|
+
const decisionStyle = provisionedDepsPending
|
|
516
|
+
? 'info'
|
|
517
|
+
: runtime.decision === 'ready' ? 'ok' : runtime.decision === 'blocked' ? 'err' : 'warn';
|
|
518
|
+
const depsStyle = provisionedDepsPending ? 'info' : stateStyle(runtime.deps, 'current');
|
|
519
|
+
const devServerStyle = provisionedDepsPending ? 'info' : undefined;
|
|
520
|
+
const devServer = runtime.devServer
|
|
521
|
+
? ` ${runtime.devServer.label}=${out(devServerStyle ?? stateStyle(runtime.devServer.status, 'up'), runtime.devServer.status)}`
|
|
522
|
+
: '';
|
|
506
523
|
console.log(
|
|
507
|
-
`${out('label', 'runtime:')} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out('dim', `(${runtime.reasonCode})`)}` : ''} deps=${out(
|
|
524
|
+
`${out('label', 'runtime:')} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out('dim', `(${runtime.reasonCode})`)}` : ''} deps=${out(depsStyle, runtime.deps ?? 'unknown')}${devServer}`,
|
|
508
525
|
);
|
|
509
526
|
for (const reason of runtime.reasons) console.log(` ${out('dim', reason)}`);
|
|
510
527
|
}
|
|
@@ -614,6 +631,126 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
614
631
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
615
632
|
}
|
|
616
633
|
|
|
634
|
+
async function handleProvision({ positional, options, rawArgv }: ParsedArgs): Promise<number> {
|
|
635
|
+
applyWatcherPortOption(options);
|
|
636
|
+
const json = optionFlag(options, 'json');
|
|
637
|
+
const { adapter, target } = resolveProvisionAdapter(options);
|
|
638
|
+
const surface = getAdapterSurface(adapter);
|
|
639
|
+
const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
|
|
640
|
+
const result = await surface.runwayProvision.run(target, {
|
|
641
|
+
json,
|
|
642
|
+
platform: optionString(options, 'platform') ?? optionString(options, 'devicePlatform') ?? (positional[0] === 'runway' ? positional[1] : positional[0]) ?? 'ios',
|
|
643
|
+
branch: optionString(options, 'branch'),
|
|
644
|
+
defaultBranch: optionString(options, 'defaultBranch'),
|
|
645
|
+
run: optionString(options, 'run'),
|
|
646
|
+
cacheRoot: optionString(options, 'cacheRoot'),
|
|
647
|
+
simulator: optionString(options, 'simulator') ?? optionString(options, 'device'),
|
|
648
|
+
runtime: optionString(options, 'runtime'),
|
|
649
|
+
deviceType: optionString(options, 'deviceType'),
|
|
650
|
+
slot: optionString(options, 'slot'),
|
|
651
|
+
watcherPort: optionString(options, 'watcherPort'),
|
|
652
|
+
runtimeDir: optionString(options, 'runtimeDir'),
|
|
653
|
+
force: optionFlag(options, 'force'),
|
|
654
|
+
resolveOnly: optionFlag(options, 'resolveOnly'),
|
|
655
|
+
rerunCommand,
|
|
656
|
+
});
|
|
657
|
+
if (json) {
|
|
658
|
+
console.log(JSON.stringify(result, null, 2));
|
|
659
|
+
} else if (result.status === 'pass') {
|
|
660
|
+
const cache = typeof result.cache === 'object' && result.cache ? result.cache as Record<string, unknown> : undefined;
|
|
661
|
+
const simulator = typeof result.simulator === 'object' && result.simulator ? result.simulator as Record<string, unknown> : undefined;
|
|
662
|
+
const artifact = typeof result.artifact === 'object' && result.artifact ? result.artifact as Record<string, unknown> : undefined;
|
|
663
|
+
if (result.resolveOnly) {
|
|
664
|
+
console.error(`✓ resolved ${adapter} ${result.platform ?? ''} run=${artifact?.runId ?? 'unknown'} revision=${artifact?.revision ?? 'unknown'} artifact=${artifact?.artifactName ?? 'unknown'}`);
|
|
665
|
+
} else {
|
|
666
|
+
const action = result.skipped ? 'already provisioned' : 'provisioned';
|
|
667
|
+
console.error(`✓ ${action} ${adapter} ${result.platform ?? ''} simulator=${simulator?.name ?? 'unknown'} cache=${cache?.status ?? 'unknown'}`);
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
console.error(`✗ mm-harness provision: ${result.error?.message ?? 'provision failed'}\n Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
671
|
+
}
|
|
672
|
+
return result.exitCode;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function resolveProvisionAdapter(options: CliOptions): { adapter: MetaMaskRecipeAdapter; target: string } {
|
|
676
|
+
const target = targetPath(options);
|
|
677
|
+
const adapter = optionString(options, 'adapter') ?? detectAdapter(target);
|
|
678
|
+
if (!adapter) {
|
|
679
|
+
throw usageError(`could not detect the MetaMask repo type for ${target}\n Next: ${ADAPTER_DETECT_NEXT}`);
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
assertAdapter(adapter);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
685
|
+
}
|
|
686
|
+
return { adapter, target };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function provisionRerunCommand(rawArgv: string[], options: CliOptions, adapter: MetaMaskRecipeAdapter, target: string): string {
|
|
690
|
+
const parts = ['mm-harness', 'provision'];
|
|
691
|
+
for (const positional of provisionPositionals(rawArgv)) parts.push(shellQuoteArg(positional));
|
|
692
|
+
parts.push('--adapter', adapter, '--target', shellQuote(target));
|
|
693
|
+
const aliases: Array<[string, string[]]> = [
|
|
694
|
+
['platform', ['--platform', '--device-platform']],
|
|
695
|
+
['branch', ['--branch']],
|
|
696
|
+
['defaultBranch', ['--default-branch']],
|
|
697
|
+
['run', ['--run']],
|
|
698
|
+
['cacheRoot', ['--cache-root']],
|
|
699
|
+
['simulator', ['--simulator', '--device']],
|
|
700
|
+
['runtime', ['--runtime']],
|
|
701
|
+
['deviceType', ['--device-type']],
|
|
702
|
+
['slot', ['--slot']],
|
|
703
|
+
['watcherPort', ['--watcher-port']],
|
|
704
|
+
['runtimeDir', ['--runtime-dir']],
|
|
705
|
+
];
|
|
706
|
+
for (const [key, flags] of aliases) {
|
|
707
|
+
const found = findRawOption(rawArgv, flags);
|
|
708
|
+
const value = found?.value ?? optionString(options, key);
|
|
709
|
+
if (value) parts.push(found?.flag ?? flags[0], shellQuoteArg(value));
|
|
710
|
+
}
|
|
711
|
+
if (optionFlag(options, 'force')) parts.push('--force');
|
|
712
|
+
if (optionFlag(options, 'resolveOnly')) parts.push('--resolve-only');
|
|
713
|
+
if (optionFlag(options, 'json')) parts.push('--json');
|
|
714
|
+
return parts.join(' ');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function provisionPositionals(rawArgv: string[]): string[] {
|
|
718
|
+
const positionals: string[] = [];
|
|
719
|
+
const valueFlags = new Set([
|
|
720
|
+
'--adapter', '--target', '--project-root', '--platform', '--device-platform', '--branch', '--default-branch',
|
|
721
|
+
'--run', '--cache-root', '--simulator', '--device', '--runtime', '--device-type', '--slot', '--watcher-port',
|
|
722
|
+
'--runtime-dir',
|
|
723
|
+
]);
|
|
724
|
+
for (let i = 0; i < rawArgv.length; i += 1) {
|
|
725
|
+
const arg = rawArgv[i];
|
|
726
|
+
if (!arg.startsWith('--')) {
|
|
727
|
+
positionals.push(arg);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const key = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
731
|
+
if (!arg.includes('=') && valueFlags.has(key)) i += 1;
|
|
732
|
+
}
|
|
733
|
+
return positionals;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function findRawOption(rawArgv: string[], flags: string[]): { flag: string; value: string } | undefined {
|
|
737
|
+
for (let i = 0; i < rawArgv.length; i += 1) {
|
|
738
|
+
const arg = rawArgv[i];
|
|
739
|
+
for (const flag of flags) {
|
|
740
|
+
if (arg === flag) {
|
|
741
|
+
const value = rawArgv[i + 1];
|
|
742
|
+
return value && !value.startsWith('--') ? { flag, value } : undefined;
|
|
743
|
+
}
|
|
744
|
+
if (arg.startsWith(`${flag}=`)) return { flag, value: arg.slice(flag.length + 1) };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function shellQuoteArg(value: string): string {
|
|
751
|
+
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : shellQuote(value);
|
|
752
|
+
}
|
|
753
|
+
|
|
617
754
|
async function handleRuntimeHealth({ options }: ParsedArgs): Promise<number> {
|
|
618
755
|
const adapter = adapterOption(options);
|
|
619
756
|
if (adapter !== 'extension') throw new Error('runtime-health currently applies to the extension adapter.');
|
|
@@ -774,6 +911,7 @@ function readJsonIfExists(file: string): unknown {
|
|
|
774
911
|
}
|
|
775
912
|
|
|
776
913
|
async function handleRuntimeDecision({ options }: ParsedArgs): Promise<number> {
|
|
914
|
+
applyRuntimeDirOption(options);
|
|
777
915
|
const adapter = adapterOption(options);
|
|
778
916
|
const target = targetPath(options);
|
|
779
917
|
if (adapter === 'mobile') {
|
|
@@ -1117,23 +1255,21 @@ async function handleStop(argv: string[]): Promise<number> {
|
|
|
1117
1255
|
const { options } = parseArgs(argv, 'stop');
|
|
1118
1256
|
const json = optionFlag(options, 'json');
|
|
1119
1257
|
const { adapter, target } = resolveAdapter(options);
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1258
|
+
const surface = getAdapterSurface(adapter);
|
|
1259
|
+
// Slot-scope the stop: an explicit --port wins, else resolve the checkout's
|
|
1260
|
+
// own dev-server port so a concurrent slot's server is never signalled.
|
|
1261
|
+
const explicitPort = optionString(options, 'port') ?? optionString(options, 'watcherPort');
|
|
1262
|
+
if (explicitPort) {
|
|
1263
|
+
process.env.WATCHER_PORT = explicitPort;
|
|
1264
|
+
process.env.METRO_PORT = explicitPort;
|
|
1265
|
+
} else {
|
|
1266
|
+
surface.resolveSlotPorts(target);
|
|
1267
|
+
}
|
|
1268
|
+
const stop = surface.devServer.stop(target);
|
|
1269
|
+
if (stop.kind === 'headless') {
|
|
1270
|
+
// core has no dev server — teach the reachable headless path (exit 2).
|
|
1271
|
+
return usageOut(json, 'stop', stop.message, stop.userAction);
|
|
1127
1272
|
}
|
|
1128
|
-
const leaf = path.join(runnerDir, 'adapters', 'mobile', 'stop-metro.sh');
|
|
1129
|
-
const args = ['--target', target];
|
|
1130
|
-
const port = optionString(options, 'port') ?? optionString(options, 'watcherPort');
|
|
1131
|
-
if (port) args.push('--port', port);
|
|
1132
|
-
const result = spawnSync('bash', [leaf, ...args], {
|
|
1133
|
-
stdio: json ? 'pipe' : 'inherit',
|
|
1134
|
-
encoding: 'utf8',
|
|
1135
|
-
});
|
|
1136
|
-
const exitCode = result.status ?? 1;
|
|
1137
1273
|
if (json) {
|
|
1138
1274
|
console.log(
|
|
1139
1275
|
JSON.stringify(
|
|
@@ -1142,16 +1278,22 @@ async function handleStop(argv: string[]): Promise<number> {
|
|
|
1142
1278
|
command: 'stop',
|
|
1143
1279
|
adapter,
|
|
1144
1280
|
target,
|
|
1145
|
-
status:
|
|
1146
|
-
|
|
1147
|
-
|
|
1281
|
+
status: stop.status === 0 ? 'pass' : 'fail',
|
|
1282
|
+
...(stop.signalled !== undefined ? { signalled: stop.signalled } : {}),
|
|
1283
|
+
exitCode: stop.status,
|
|
1284
|
+
...(stop.output ? { output: stop.output } : {}),
|
|
1148
1285
|
},
|
|
1149
1286
|
null,
|
|
1150
1287
|
2,
|
|
1151
1288
|
),
|
|
1152
1289
|
);
|
|
1290
|
+
} else {
|
|
1291
|
+
// The leaf may have captured its own detail; surface it, then the uniform
|
|
1292
|
+
// outcome line the same way for every platform.
|
|
1293
|
+
if (stop.output) process.stderr.write(`${stop.output}\n`);
|
|
1294
|
+
console.error(`${color(stop.status === 0 ? 'ok' : 'err', stop.status === 0 ? '✓' : '✗')} ${stop.summary}`);
|
|
1153
1295
|
}
|
|
1154
|
-
return
|
|
1296
|
+
return stop.status;
|
|
1155
1297
|
}
|
|
1156
1298
|
|
|
1157
1299
|
async function handleCall(argv: string[]): Promise<number> {
|
|
@@ -1168,9 +1310,23 @@ async function handleCall(argv: string[]): Promise<number> {
|
|
|
1168
1310
|
const { options } = parseArgs(rest, 'call');
|
|
1169
1311
|
const json = optionFlag(options, 'json');
|
|
1170
1312
|
if (!shortName) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1313
|
+
// Context-aware usage: the example uses the DETECTED adapter and a REAL action
|
|
1314
|
+
// from its manifest — never a hardcoded action/adapter that may not exist in
|
|
1315
|
+
// this checkout — and points at the scoped discovery command.
|
|
1316
|
+
let example = 'mm-harness call <action>';
|
|
1317
|
+
let discovery = 'mm-harness actions';
|
|
1318
|
+
try {
|
|
1319
|
+
const { adapter } = resolveAdapter(options);
|
|
1320
|
+
const manifest = loadActionManifest(adapter, optionString(options, 'actionManifest'));
|
|
1321
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
1322
|
+
const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames(manifest));
|
|
1323
|
+
example = `mm-harness call ${exampleAction} --adapter ${adapter}`;
|
|
1324
|
+
discovery = `mm-harness actions --adapter ${adapter}`;
|
|
1325
|
+
} catch { /* adapter/manifest unavailable — keep the generic example */ }
|
|
1326
|
+
const message = `call requires <action>. Example: ${example}`;
|
|
1327
|
+
const userAction = `${example} # see the vocabulary: ${discovery}`;
|
|
1328
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: 'call', error: { code: 'USAGE', message, userAction } }, null, 2));
|
|
1329
|
+
else console.error(`${message}\n See the vocabulary: ${discovery}`);
|
|
1174
1330
|
return EXIT.usage;
|
|
1175
1331
|
}
|
|
1176
1332
|
|
|
@@ -1340,6 +1496,16 @@ function resolveActionName(shortName: string, names: string[]): ActionResolution
|
|
|
1340
1496
|
return { status: 'unknown', resolved: '', candidates: [] };
|
|
1341
1497
|
}
|
|
1342
1498
|
|
|
1499
|
+
// A real action name to show in the `call` usage example, from THIS adapter's
|
|
1500
|
+
// manifest: a wallet action when the platform has one, else the universal
|
|
1501
|
+
// `command` action, else the first declared name. Never a hardcoded guess.
|
|
1502
|
+
function pickCallExampleAction(names: string[]): string {
|
|
1503
|
+
const walletish = names.find((name) => /wallet|unlock/u.test(name));
|
|
1504
|
+
if (walletish) return walletish;
|
|
1505
|
+
if (names.includes('command')) return 'command';
|
|
1506
|
+
return names[0] ?? 'command';
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1343
1509
|
function synthesizeOneNodeRecipe(action: string, args: Record<string, string>): Record<string, unknown> {
|
|
1344
1510
|
return {
|
|
1345
1511
|
schema_version: 1,
|
|
@@ -1794,6 +1960,16 @@ function serializeLibrarySources(sources: MetaMaskLibrarySource[]): string {
|
|
|
1794
1960
|
.join(':');
|
|
1795
1961
|
}
|
|
1796
1962
|
|
|
1963
|
+
// Map a resolved library source to its precedence tier for the flows legend, from
|
|
1964
|
+
// the ACTUAL resolution rather than an invented label: the canonical MetaMask
|
|
1965
|
+
// library this runner appends last, the personal library the harness names
|
|
1966
|
+
// 'personal', and any other configured library in between as the team/shared tier.
|
|
1967
|
+
function flowSourceTier(source: MetaMaskLibrarySource, index: number, total: number): string {
|
|
1968
|
+
if (index === total - 1 && source.name === 'metamask') return 'canonical';
|
|
1969
|
+
if (source.name === 'personal') return 'personal';
|
|
1970
|
+
return 'team';
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1797
1973
|
// The engine's flows subcommands (grounded in registerFlowsCommand): a bare
|
|
1798
1974
|
// `flows` (or one that leads with a flag) means list.
|
|
1799
1975
|
const FLOWS_SUBCOMMANDS: readonly string[] = ['list', 'promote'];
|
|
@@ -1824,11 +2000,25 @@ async function handleFlows(argv: string[]): Promise<number> {
|
|
|
1824
2000
|
const sources = await resolveMetaMaskLibrarySources(undefined);
|
|
1825
2001
|
if (!sources) {
|
|
1826
2002
|
throw usageError(
|
|
1827
|
-
'flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support
|
|
2003
|
+
'flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; ' +
|
|
2004
|
+
'flows resolve personal > team > canonical). ' +
|
|
1828
2005
|
'Next: update the dependency and run yarn install.',
|
|
1829
2006
|
);
|
|
1830
2007
|
}
|
|
1831
2008
|
const forwarded = stripTargetFlag(argv);
|
|
2009
|
+
const jsonMode = forwarded.includes('--json');
|
|
2010
|
+
// Provenance: flows resolve by precedence — the highest-precedence copy of a ref
|
|
2011
|
+
// wins and shadows lower ones, so a LOCAL library outranks canonical. The listing
|
|
2012
|
+
// below prints `source=<name>` per flow; this legend maps each resolved source to
|
|
2013
|
+
// its tier so that source is decodable. --json reserves stdout for the engine
|
|
2014
|
+
// envelope, so the legend rides stderr only.
|
|
2015
|
+
if (!jsonMode) {
|
|
2016
|
+
console.error(color('label', 'flows resolve by precedence (highest wins; local shadows canonical):'));
|
|
2017
|
+
sources.forEach((source, index) => {
|
|
2018
|
+
const name = source.name ?? path.basename(source.root);
|
|
2019
|
+
console.error(` ${index + 1}. ${color('cmd', name)} ${color('dim', `[${flowSourceTier(source, index, sources.length)}]`)}`);
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
1832
2022
|
// The engine requires an explicit subcommand. Inspect only the FIRST token: a
|
|
1833
2023
|
// real subcommand there is passed through; anything else (empty, or a leading
|
|
1834
2024
|
// flag whose VALUE must not be mistaken for a subcommand) defaults to list.
|
package/src/commands/debug.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
7
|
import { runnerDir } from '../paths.ts';
|
|
8
|
+
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
8
9
|
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from './shared.ts';
|
|
9
10
|
|
|
10
11
|
const DEBUG_BOOLEANS = new Set(['worker', 'devMenu', 'json']);
|
|
@@ -16,7 +17,8 @@ export async function handleDebug(argv: string[]): Promise<number> {
|
|
|
16
17
|
const adapter = resolveAdapter(options, target);
|
|
17
18
|
|
|
18
19
|
if (!adapter) return usageOut(json, 'debug', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
19
|
-
|
|
20
|
+
const surface = getAdapterSurface(adapter);
|
|
21
|
+
if (surface.headless) return usageOut(json, 'debug', 'core is headless; there is no debug console.', surface.hints.relaunch);
|
|
20
22
|
|
|
21
23
|
const worker = flag(options, 'worker');
|
|
22
24
|
const devMenu = flag(options, 'devMenu');
|
package/src/commands/fixtures.ts
CHANGED
|
@@ -6,6 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
8
|
import { runnerDir, walletFixturePath } from '../paths.ts';
|
|
9
|
+
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
9
10
|
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
10
11
|
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
11
12
|
|
|
@@ -55,9 +56,13 @@ export async function handleFixtures(argv: string[], deps: CommandDeps): Promise
|
|
|
55
56
|
if (!adapter) {
|
|
56
57
|
return usageOut(json, 'fixtures', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
const surface = getAdapterSurface(adapter);
|
|
60
|
+
if (surface.headless) return usageOut(json, 'fixtures', 'core is headless; it has no wallet fixture.', surface.hints.launch);
|
|
59
61
|
|
|
60
62
|
const canonicalFixture = walletFixturePath(target);
|
|
63
|
+
// Platform-phrased retry hint on a setup-wallet failure: the surface owns the
|
|
64
|
+
// (re)build/relaunch command; this command never spells out another platform's.
|
|
65
|
+
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
|
|
61
66
|
|
|
62
67
|
if (sub === 'sync') {
|
|
63
68
|
const exitCode = fixturesSync(adapter, target, json);
|
|
@@ -104,7 +109,7 @@ export async function handleFixtures(argv: string[], deps: CommandDeps): Promise
|
|
|
104
109
|
else process.env.APP_ROOT = previousAppRoot;
|
|
105
110
|
}
|
|
106
111
|
if (!json && status === 'fail') {
|
|
107
|
-
console.error(
|
|
112
|
+
console.error(` Next: ${retryHint}`);
|
|
108
113
|
}
|
|
109
114
|
} else {
|
|
110
115
|
// Extension has no standalone set arm — reuse call's one-node machinery via
|
|
@@ -127,7 +132,7 @@ export async function handleFixtures(argv: string[], deps: CommandDeps): Promise
|
|
|
127
132
|
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
128
133
|
}
|
|
129
134
|
if (!json && status === 'fail') {
|
|
130
|
-
console.error(
|
|
135
|
+
console.error(` Next: ${retryHint}`);
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -149,9 +154,7 @@ export async function handleFixtures(argv: string[], deps: CommandDeps): Promise
|
|
|
149
154
|
error: status === 'fail' ? {
|
|
150
155
|
code: 'SETUP_WALLET_FAILED',
|
|
151
156
|
message: 'wallet fixture setup failed',
|
|
152
|
-
userAction:
|
|
153
|
-
? 'mm-harness launch ios # relaunch the dev client, then retry: mm-harness fixtures set'
|
|
154
|
-
: 'mm-harness launch --build # rebuild and relaunch, then retry: mm-harness fixtures set',
|
|
157
|
+
userAction: retryHint,
|
|
155
158
|
} : null,
|
|
156
159
|
},
|
|
157
160
|
null,
|
|
@@ -166,8 +169,10 @@ export async function handleFixtures(argv: string[], deps: CommandDeps): Promise
|
|
|
166
169
|
|
|
167
170
|
// Sync the overlay installation + wallet fixture for the checkout.
|
|
168
171
|
// Re-runs the adapter inject to refresh patched files, then copies the wallet
|
|
169
|
-
// fixture to the canonical runtime location via sync-wallet-fixture.sh.
|
|
170
|
-
|
|
172
|
+
// fixture to the canonical runtime location via sync-wallet-fixture.sh. Reached
|
|
173
|
+
// only for device adapters (headless core returns earlier); the adapter is
|
|
174
|
+
// forwarded verbatim to `install`.
|
|
175
|
+
function fixturesSync(adapter: MetaMaskRecipeAdapter, target: string, json: boolean): number {
|
|
171
176
|
const mmHarnessBin = path.join(runnerDir, 'bin/mm-harness');
|
|
172
177
|
const installResult = spawnScript(
|
|
173
178
|
mmHarnessBin,
|