@deeeed/metamask-harness 0.4.0 → 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 +19 -0
- 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/CLI-SPEC.md +26 -3
- package/package.json +5 -1
- package/src/adapters/core/surface.ts +15 -0
- package/src/adapters/extension/surface.ts +20 -3
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/surface.ts +16 -4
- package/src/adapters/slot-ports.ts +1 -1
- package/src/adapters/surface.ts +35 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +149 -6
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +52 -5
package/src/cli-commands.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const SPEC: CommandSpecMap = {
|
|
|
33
33
|
{ name: 'logs', aliases: ['tail'], desc: 'Compact build events or full log', flags: ['--full', '-f'] },
|
|
34
34
|
{ name: 'debug', aliases: ['devtools', 'inspect'], desc: 'Open DevTools UI', flags: ['--json', '--no-open'] },
|
|
35
35
|
{ name: 'actions', desc: 'List runnable recipe actions', flags: ['--json'] },
|
|
36
|
-
{ name: 'doctor', desc: 'Check harness/orchestration health', flags: ['--json'] },
|
|
36
|
+
{ name: 'doctor', desc: 'Check harness/orchestration health', flags: ['--json', '--target', '--adapter', '--runtime-dir'] },
|
|
37
37
|
{ name: 'run', desc: 'Execute a proof recipe', args: ['recipe.json'] },
|
|
38
38
|
{ name: 'interactive', aliases: ['menu'], desc: 'Interactive command menu' },
|
|
39
39
|
{ name: 'prepare', desc: 'Install harness (+ optional validate)', flags: ['--target', '--runtime-dir', '--json'] },
|
package/src/cli.ts
CHANGED
|
@@ -68,6 +68,7 @@ type CliOptions = Record<string, CliOptionValue>;
|
|
|
68
68
|
interface ParsedArgs {
|
|
69
69
|
positional: string[];
|
|
70
70
|
options: CliOptions;
|
|
71
|
+
rawArgv: string[];
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
interface RuntimeOptions {
|
|
@@ -105,6 +106,7 @@ const COMMANDS: Record<string, (args: ParsedArgs) => Promise<number>> = {
|
|
|
105
106
|
'runtime-health': handleRuntimeHealth,
|
|
106
107
|
'runtime-decision': handleRuntimeDecision,
|
|
107
108
|
'runtime-launch': handleRuntimeLaunch,
|
|
109
|
+
provision: handleProvision,
|
|
108
110
|
'resolve-extension': handleResolveExtension,
|
|
109
111
|
'ensure-ready': handleEnsureReady,
|
|
110
112
|
run: handleRun,
|
|
@@ -114,7 +116,7 @@ const COMMANDS: Record<string, (args: ParsedArgs) => Promise<number>> = {
|
|
|
114
116
|
// Top-level runtime-overlay commands (the tool installs a runtime overlay).
|
|
115
117
|
// Top-level overlay commands route to handleHarness; the `harness <sub>`
|
|
116
118
|
// subcommand form has been removed from the CLI surface.
|
|
117
|
-
const OVERLAY_COMMANDS: readonly string[] = ['install', 'verify', 'cleanup', 'live'];
|
|
119
|
+
const OVERLAY_COMMANDS: readonly string[] = ['install', 'provision', 'verify', 'cleanup', 'live'];
|
|
118
120
|
|
|
119
121
|
function usage() {
|
|
120
122
|
// Help is organized by the harness mental model: mm-harness IS the tool; a
|
|
@@ -176,7 +178,7 @@ See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).
|
|
|
176
178
|
function parseArgs(argv: string[], command?: string): ParsedArgs {
|
|
177
179
|
const positional: string[] = [];
|
|
178
180
|
const options: CliOptions = {};
|
|
179
|
-
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']);
|
|
180
182
|
for (let i = 0; i < argv.length; i += 1) {
|
|
181
183
|
const arg = argv[i];
|
|
182
184
|
if (!arg.startsWith('--')) {
|
|
@@ -216,7 +218,7 @@ function parseArgs(argv: string[], command?: string): ParsedArgs {
|
|
|
216
218
|
options[key] = argv[i + 1];
|
|
217
219
|
i += 1;
|
|
218
220
|
}
|
|
219
|
-
return { positional, options };
|
|
221
|
+
return { positional, options, rawArgv: [...argv] };
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
function parseRecordVideoMode(value: string | undefined): false | 'full-run' {
|
|
@@ -249,6 +251,20 @@ function optionFlag(options: CliOptions, key: string): boolean {
|
|
|
249
251
|
return value === true;
|
|
250
252
|
}
|
|
251
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
|
+
|
|
252
268
|
function requiredOption(options: CliOptions, key: string, message: string): string {
|
|
253
269
|
const value = optionString(options, key);
|
|
254
270
|
if (!value) throw usageError(message);
|
|
@@ -450,6 +466,7 @@ async function handleActions({ options }: ParsedArgs): Promise<number> {
|
|
|
450
466
|
}
|
|
451
467
|
|
|
452
468
|
async function handleDoctor({ options }: ParsedArgs): Promise<number> {
|
|
469
|
+
applyRuntimeDirOption(options);
|
|
453
470
|
const target = targetPath(options);
|
|
454
471
|
// Share the exact detect-from-target logic the overlay commands (verify/install/
|
|
455
472
|
// cleanup) use: when --adapter/--platform is omitted, auto-detect from the
|
|
@@ -494,12 +511,17 @@ async function handleDoctor({ options }: ParsedArgs): Promise<number> {
|
|
|
494
511
|
const stateStyle = (value: string | undefined, good: string) => (value === good ? 'ok' : 'warn');
|
|
495
512
|
console.log(`${out(result.status === 'pass' ? 'ok' : 'err', result.status)} ${out('bold', adapter)} ${result.compatibilityMode} ${out('dim', `manifest=${actionManifestPath}`)}`);
|
|
496
513
|
if (runtime) {
|
|
497
|
-
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;
|
|
498
520
|
const devServer = runtime.devServer
|
|
499
|
-
? ` ${runtime.devServer.label}=${out(stateStyle(runtime.devServer.status, 'up'), runtime.devServer.status)}`
|
|
521
|
+
? ` ${runtime.devServer.label}=${out(devServerStyle ?? stateStyle(runtime.devServer.status, 'up'), runtime.devServer.status)}`
|
|
500
522
|
: '';
|
|
501
523
|
console.log(
|
|
502
|
-
`${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}`,
|
|
503
525
|
);
|
|
504
526
|
for (const reason of runtime.reasons) console.log(` ${out('dim', reason)}`);
|
|
505
527
|
}
|
|
@@ -609,6 +631,126 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
609
631
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
610
632
|
}
|
|
611
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
|
+
|
|
612
754
|
async function handleRuntimeHealth({ options }: ParsedArgs): Promise<number> {
|
|
613
755
|
const adapter = adapterOption(options);
|
|
614
756
|
if (adapter !== 'extension') throw new Error('runtime-health currently applies to the extension adapter.');
|
|
@@ -769,6 +911,7 @@ function readJsonIfExists(file: string): unknown {
|
|
|
769
911
|
}
|
|
770
912
|
|
|
771
913
|
async function handleRuntimeDecision({ options }: ParsedArgs): Promise<number> {
|
|
914
|
+
applyRuntimeDirOption(options);
|
|
772
915
|
const adapter = adapterOption(options);
|
|
773
916
|
const target = targetPath(options);
|
|
774
917
|
if (adapter === 'mobile') {
|
package/src/harness.ts
CHANGED
|
@@ -16,15 +16,15 @@ import { prepareMobile } from './adapters/mobile/prepare.ts';
|
|
|
16
16
|
// core delegate, extension runtime-context env, and verbatim arg passthrough —
|
|
17
17
|
// is reproduced exactly so the skill can become a thin caller.
|
|
18
18
|
|
|
19
|
-
type HarnessAction = 'install' | 'verify' | 'cleanup' | 'live';
|
|
19
|
+
type HarnessAction = 'install' | 'provision' | 'verify' | 'cleanup' | 'live';
|
|
20
20
|
|
|
21
|
-
const HARNESS_ACTIONS: readonly HarnessAction[] = ['install', 'verify', 'cleanup', 'live'];
|
|
21
|
+
const HARNESS_ACTIONS: readonly HarnessAction[] = ['install', 'provision', 'verify', 'cleanup', 'live'];
|
|
22
22
|
const ADAPTERS: readonly MetaMaskRecipeAdapter[] = ['mobile', 'extension', 'core'];
|
|
23
23
|
|
|
24
24
|
// Mirror of scripts/lib/cli-common.sh valid_adapter_action, restricted to the
|
|
25
25
|
// subcommands this command exposes (core has no app/live lifecycle).
|
|
26
26
|
function isValidAdapterAction(adapter: MetaMaskRecipeAdapter, action: HarnessAction): boolean {
|
|
27
|
-
if (adapter === 'core') return action === 'install' || action === 'verify' || action === 'cleanup';
|
|
27
|
+
if (adapter === 'core') return action === 'install' || action === 'provision' || action === 'verify' || action === 'cleanup';
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -37,6 +37,8 @@ is auto-detected from the repo. Pass --platform only to override.
|
|
|
37
37
|
Commands (one copy-pasteable example each):
|
|
38
38
|
install Install the recipe harness runtime overlay into the checkout.
|
|
39
39
|
mm-harness install
|
|
40
|
+
provision Install the cached Runway mobile dev client (same path as install --runway).
|
|
41
|
+
mm-harness provision runway ios --adapter mobile
|
|
40
42
|
verify Check the harness/runtime is present and healthy (no app launch).
|
|
41
43
|
mm-harness verify
|
|
42
44
|
cleanup Remove the installed harness overlay and restore the checkout.
|
|
@@ -83,6 +85,10 @@ function argValue(args: string[], needle: string): string | undefined {
|
|
|
83
85
|
return undefined;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
function shellQuote(value: string): string {
|
|
89
|
+
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
function isAdapter(value: string | undefined): value is MetaMaskRecipeAdapter {
|
|
87
93
|
return value === 'mobile' || value === 'extension' || value === 'core';
|
|
88
94
|
}
|
|
@@ -530,6 +536,15 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
530
536
|
// Forward args carry --target so the orchestration script resolves the same
|
|
531
537
|
// checkout; inject the resolved path only when the caller omitted it.
|
|
532
538
|
let forwardArgs = hasArg(forward, '--target') ? [...forward] : ['--target', target, ...forward];
|
|
539
|
+
|
|
540
|
+
if (harnessAction === 'provision') {
|
|
541
|
+
return handleRunwayInstall(adapter, target, provisionRunwayForward(forward), json, 'provision');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (harnessAction === 'install' && hasArg(forward, '--runway')) {
|
|
545
|
+
return handleRunwayInstall(adapter, target, forward, json, 'install');
|
|
546
|
+
}
|
|
547
|
+
|
|
533
548
|
if (adapter === 'extension' && (harnessAction === 'live' || harnessAction === 'verify')) {
|
|
534
549
|
forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
|
|
535
550
|
}
|
|
@@ -631,6 +646,128 @@ export async function handleHarness(argv: string[]): Promise<number> {
|
|
|
631
646
|
return exitCode;
|
|
632
647
|
}
|
|
633
648
|
|
|
649
|
+
async function handleRunwayInstall(
|
|
650
|
+
adapter: MetaMaskRecipeAdapter,
|
|
651
|
+
target: string,
|
|
652
|
+
forward: string[],
|
|
653
|
+
json: boolean,
|
|
654
|
+
action: 'install' | 'provision',
|
|
655
|
+
): Promise<number> {
|
|
656
|
+
const { getAdapterSurface } = await import('./adapters/surface.ts');
|
|
657
|
+
const rerunCommand = action === 'provision'
|
|
658
|
+
? runwayProvisionRerunCommand(adapter, target, forward, json)
|
|
659
|
+
: runwayInstallRerunCommand(adapter, target, forward, json);
|
|
660
|
+
const surface = getAdapterSurface(adapter);
|
|
661
|
+
const runtimeDir = argValue(forward, '--runtime-dir');
|
|
662
|
+
const watcherPort = argValue(forward, '--watcher-port');
|
|
663
|
+
const result = await surface.runwayProvision.run(target, {
|
|
664
|
+
json,
|
|
665
|
+
platform: argValue(forward, '--platform') ?? 'ios',
|
|
666
|
+
branch: argValue(forward, '--branch'),
|
|
667
|
+
defaultBranch: argValue(forward, '--default-branch'),
|
|
668
|
+
run: argValue(forward, '--run'),
|
|
669
|
+
cacheRoot: argValue(forward, '--cache-root'),
|
|
670
|
+
simulator: argValue(forward, '--simulator') ?? argValue(forward, '--device'),
|
|
671
|
+
runtime: argValue(forward, '--runtime'),
|
|
672
|
+
deviceType: argValue(forward, '--device-type'),
|
|
673
|
+
slot: argValue(forward, '--slot'),
|
|
674
|
+
watcherPort,
|
|
675
|
+
runtimeDir,
|
|
676
|
+
force: hasArg(forward, '--force'),
|
|
677
|
+
resolveOnly: hasArg(forward, '--resolve-only'),
|
|
678
|
+
rerunCommand,
|
|
679
|
+
});
|
|
680
|
+
if (json) {
|
|
681
|
+
console.log(JSON.stringify(result, null, 2));
|
|
682
|
+
} else if (result.status === 'pass') {
|
|
683
|
+
const cache = typeof result.cache === 'object' && result.cache ? result.cache as Record<string, unknown> : undefined;
|
|
684
|
+
const simulator = typeof result.simulator === 'object' && result.simulator ? result.simulator as Record<string, unknown> : undefined;
|
|
685
|
+
const artifact = typeof result.artifact === 'object' && result.artifact ? result.artifact as Record<string, unknown> : undefined;
|
|
686
|
+
if (result.resolveOnly) {
|
|
687
|
+
console.error(`✓ resolved Runway app for ${adapter} ${result.platform ?? ''} run=${artifact?.runId ?? 'unknown'} revision=${artifact?.revision ?? 'unknown'} artifact=${artifact?.artifactName ?? 'unknown'}`);
|
|
688
|
+
} else {
|
|
689
|
+
const action = result.skipped ? 'already provisioned' : 'installed Runway app';
|
|
690
|
+
console.error(`✓ ${action} for ${adapter} ${result.platform ?? ''} simulator=${simulator?.name ?? 'unknown'} cache=${cache?.status ?? 'skip'}`);
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
const label = action === 'provision' ? 'provision' : 'install --runway';
|
|
694
|
+
console.error(`✗ mm-harness ${label}: ${result.error?.message ?? 'runway install failed'}\n Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
695
|
+
}
|
|
696
|
+
return result.exitCode;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function provisionRunwayForward(forward: string[]): string[] {
|
|
700
|
+
const normalized: string[] = [];
|
|
701
|
+
let index = 0;
|
|
702
|
+
if (forward[index] === 'runway') index += 1;
|
|
703
|
+
if (forward[index] && !forward[index].startsWith('-')) {
|
|
704
|
+
if (!hasArg(forward, '--platform')) normalized.push('--platform', forward[index]);
|
|
705
|
+
index += 1;
|
|
706
|
+
}
|
|
707
|
+
return [...normalized, ...forward.slice(index)];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function runwayInstallRerunCommand(
|
|
711
|
+
adapter: MetaMaskRecipeAdapter,
|
|
712
|
+
target: string,
|
|
713
|
+
forward: string[],
|
|
714
|
+
json: boolean,
|
|
715
|
+
): string {
|
|
716
|
+
const parts = ['mm-harness', 'install', '--runway', '--adapter', adapter, '--target', shellQuote(target)];
|
|
717
|
+
const valueFlags = [
|
|
718
|
+
'--platform',
|
|
719
|
+
'--branch',
|
|
720
|
+
'--default-branch',
|
|
721
|
+
'--run',
|
|
722
|
+
'--cache-root',
|
|
723
|
+
'--simulator',
|
|
724
|
+
'--device',
|
|
725
|
+
'--slot',
|
|
726
|
+
'--watcher-port',
|
|
727
|
+
'--runtime-dir',
|
|
728
|
+
'--runtime',
|
|
729
|
+
'--device-type',
|
|
730
|
+
];
|
|
731
|
+
for (const flag of valueFlags) {
|
|
732
|
+
const value = argValue(forward, flag);
|
|
733
|
+
if (value) parts.push(flag, shellQuote(value));
|
|
734
|
+
}
|
|
735
|
+
if (hasArg(forward, '--force')) parts.push('--force');
|
|
736
|
+
if (hasArg(forward, '--resolve-only')) parts.push('--resolve-only');
|
|
737
|
+
if (json) parts.push('--json');
|
|
738
|
+
return parts.join(' ');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function runwayProvisionRerunCommand(
|
|
742
|
+
adapter: MetaMaskRecipeAdapter,
|
|
743
|
+
target: string,
|
|
744
|
+
forward: string[],
|
|
745
|
+
json: boolean,
|
|
746
|
+
): string {
|
|
747
|
+
const parts = ['mm-harness', 'provision', 'runway', shellQuote(argValue(forward, '--platform') ?? 'ios'), '--adapter', adapter, '--target', shellQuote(target)];
|
|
748
|
+
const valueFlags = [
|
|
749
|
+
'--branch',
|
|
750
|
+
'--default-branch',
|
|
751
|
+
'--run',
|
|
752
|
+
'--cache-root',
|
|
753
|
+
'--simulator',
|
|
754
|
+
'--device',
|
|
755
|
+
'--slot',
|
|
756
|
+
'--watcher-port',
|
|
757
|
+
'--runtime-dir',
|
|
758
|
+
'--runtime',
|
|
759
|
+
'--device-type',
|
|
760
|
+
];
|
|
761
|
+
for (const flag of valueFlags) {
|
|
762
|
+
const value = argValue(forward, flag);
|
|
763
|
+
if (value) parts.push(flag, shellQuote(value));
|
|
764
|
+
}
|
|
765
|
+
if (hasArg(forward, '--force')) parts.push('--force');
|
|
766
|
+
if (hasArg(forward, '--resolve-only')) parts.push('--resolve-only');
|
|
767
|
+
if (json) parts.push('--json');
|
|
768
|
+
return parts.join(' ');
|
|
769
|
+
}
|
|
770
|
+
|
|
634
771
|
// userAction is required whenever an error object is present so every --json
|
|
635
772
|
// failure carries a machine-readable escape path — parallel to the `usageOut`
|
|
636
773
|
// enforcement on the CLI layer. Omitting userAction is a compile-time error.
|
package/src/mm-harness-cli.ts
CHANGED
|
@@ -159,21 +159,68 @@ Example:
|
|
|
159
159
|
mm-harness doctor --fix --json
|
|
160
160
|
mm-harness doctor --adapter mobile --target /path/to/checkout`,
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: 'provision',
|
|
164
|
+
summary: 'Install the cached Runway iOS dev client on a prepared mobile slot (no deps, no Metro).',
|
|
165
|
+
example: 'mm-harness provision runway ios --adapter mobile',
|
|
166
|
+
helpText: `mm-harness provision [runway ios] [flags]
|
|
167
|
+
|
|
168
|
+
Install the cached Runway iOS dev client on the slot simulator. This is a thin
|
|
169
|
+
provisioning path only: artifact cache + simulator create + simctl install.
|
|
170
|
+
JavaScript dependencies and Metro remain dispatch-time launch concerns.
|
|
171
|
+
|
|
172
|
+
--adapter <mobile|extension|core> Target adapter (mobile supported; extension/core teach)
|
|
173
|
+
--target <path> Slot checkout path (default: cwd)
|
|
174
|
+
--platform <ios> Platform (default ios)
|
|
175
|
+
--simulator <name|udid> Override agentic-runtime.json simulator (alias: --device)
|
|
176
|
+
--device <name|udid> Alias for --simulator
|
|
177
|
+
--slot <id> Farm slot id recorded in the provision baseline
|
|
178
|
+
--watcher-port <port> Farm Metro/watcher port carried through context and Next:
|
|
179
|
+
--runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
|
|
180
|
+
--runtime <id> iOS runtime id used if simulator must be created
|
|
181
|
+
--device-type <id> Device type id used if simulator must be created
|
|
182
|
+
--branch <ref> Probe this ref before default branch
|
|
183
|
+
--default-branch <ref> Fallback ref (default main)
|
|
184
|
+
--run <id> Exact GitHub Actions run id
|
|
185
|
+
--cache-root <dir> Override shared runway cache root
|
|
186
|
+
--force Reinstall even when the app is already present
|
|
187
|
+
--resolve-only Resolve artifact metadata only; no simulator/cache/install
|
|
188
|
+
--json Machine-readable envelope; progress stays stderr
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
mm-harness provision runway ios --adapter mobile --target /path/to/slot
|
|
192
|
+
mm-harness provision runway ios --adapter mobile --slot scratch-1 --runtime-dir temp/recipe/runtime-8081
|
|
193
|
+
mm-harness provision runway ios --run 28676856835 --resolve-only --json`,
|
|
194
|
+
},
|
|
162
195
|
{
|
|
163
196
|
name: 'install',
|
|
164
|
-
summary: 'Install the per-checkout runtime overlay
|
|
197
|
+
summary: 'Install the per-checkout runtime overlay, or --runway to install a cached mobile dev client.',
|
|
165
198
|
example: 'mm-harness install',
|
|
166
199
|
helpText: `mm-harness install [flags]
|
|
167
200
|
|
|
168
201
|
Install the per-checkout runtime overlay (for CI / agents).
|
|
169
|
-
|
|
202
|
+
Add --runway on a mobile slot to install the cached Runway iOS dev client only:
|
|
203
|
+
artifact cache + simulator create + simctl install. JavaScript dependencies
|
|
204
|
+
and Metro remain dispatch-time launch concerns.
|
|
170
205
|
|
|
171
206
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
172
207
|
--target <path> Checkout path (default: cwd)
|
|
208
|
+
--runway Thin mobile Runway artifact install instead of overlay install
|
|
209
|
+
--platform <ios> Runway platform (default ios)
|
|
210
|
+
--simulator <name|udid> Override agentic-runtime.json simulator
|
|
211
|
+
--runtime <id> iOS runtime id used if simulator must be created
|
|
212
|
+
--device-type <id> Device type id used if simulator must be created
|
|
213
|
+
--branch <ref> Probe this ref before default branch
|
|
214
|
+
--default-branch <ref> Fallback ref (default main)
|
|
215
|
+
--run <id> Exact GitHub Actions run id
|
|
216
|
+
--cache-root <dir> Override shared runway cache root
|
|
217
|
+
--force Reinstall even when the app is already present
|
|
218
|
+
--resolve-only Runway metadata only; no simulator/cache/install
|
|
173
219
|
|
|
174
220
|
Example:
|
|
175
221
|
mm-harness install
|
|
176
|
-
mm-harness install --adapter extension --target /path/to/checkout
|
|
222
|
+
mm-harness install --adapter extension --target /path/to/checkout
|
|
223
|
+
mm-harness install --runway --adapter mobile --target /path/to/slot`,
|
|
177
224
|
},
|
|
178
225
|
{
|
|
179
226
|
name: 'verify',
|
|
@@ -386,8 +433,8 @@ const HELP_GROUPS: HelpGroup[] = [
|
|
|
386
433
|
},
|
|
387
434
|
{
|
|
388
435
|
title: 'RUNTIME OVERLAY',
|
|
389
|
-
blurb: 'install/verify/clean the
|
|
390
|
-
commands: ['install', 'verify', 'cleanup'],
|
|
436
|
+
blurb: 'install/verify/clean the overlay, plus thin mobile slot provisioning',
|
|
437
|
+
commands: ['provision', 'install', 'verify', 'cleanup'],
|
|
391
438
|
},
|
|
392
439
|
{
|
|
393
440
|
title: 'MAINTAIN',
|