@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/commands/launch.ts
CHANGED
|
@@ -9,10 +9,12 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { color } from '../cli-color.ts';
|
|
12
|
-
import { handleHarness
|
|
12
|
+
import { handleHarness } from '../harness.ts';
|
|
13
13
|
import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
14
14
|
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
15
15
|
import { prepareMobile } from '../adapters/mobile/prepare.ts';
|
|
16
|
+
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
17
|
+
import { stopExtensionWatcher } from '../adapters/slot-ports.ts';
|
|
16
18
|
import {
|
|
17
19
|
ADAPTER_DETECT_NEXT,
|
|
18
20
|
EXIT,
|
|
@@ -207,111 +209,6 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
207
209
|
});
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
// Apply KEY=VALUE lines from slot resolution to process.env.
|
|
211
|
-
// overwrite=true → pool match, always overrides existing env.
|
|
212
|
-
// overwrite=false → formula match, only fills vars that are unset.
|
|
213
|
-
function applyKVLines(output: string, overwrite: boolean): void {
|
|
214
|
-
for (const line of output.split('\n')) {
|
|
215
|
-
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
216
|
-
if (!m) continue;
|
|
217
|
-
const [, key, val] = m;
|
|
218
|
-
switch (key) {
|
|
219
|
-
case 'WATCHER_PORT':
|
|
220
|
-
if (overwrite || !process.env['WATCHER_PORT']) {
|
|
221
|
-
process.env['WATCHER_PORT'] = val;
|
|
222
|
-
process.env['METRO_PORT'] = val;
|
|
223
|
-
process.env['RECIPE_WATCHER_PORT'] = val;
|
|
224
|
-
}
|
|
225
|
-
break;
|
|
226
|
-
case 'IOS_SIMULATOR':
|
|
227
|
-
if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
|
|
228
|
-
break;
|
|
229
|
-
case 'SLOT_ID':
|
|
230
|
-
if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
|
|
231
|
-
break;
|
|
232
|
-
case 'CDP_PORT':
|
|
233
|
-
if (overwrite || !process.env['CDP_PORT']) {
|
|
234
|
-
process.env['CDP_PORT'] = val;
|
|
235
|
-
process.env['RECIPE_CDP_PORT'] = val;
|
|
236
|
-
}
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Resolve mobile slot port/simulator: the slot context the orchestrator wrote
|
|
243
|
-
// into the checkout wins first, then the farmslot pool (both overwrite env),
|
|
244
|
-
// then the slot-suffix formula (only fills unset vars). Called before explicit
|
|
245
|
-
// CLI flag overrides so flags always win at the top.
|
|
246
|
-
export function resolveMobileSlotPorts(target: string): void {
|
|
247
|
-
const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
|
|
248
|
-
try {
|
|
249
|
-
// The checkout's own runtime context is authoritative — it names the exact
|
|
250
|
-
// simulator/port this slot was prepared with, surviving pool renames.
|
|
251
|
-
const ctxOut = execFileSync('bash', [
|
|
252
|
-
'-c', `source "${resolveScript}" && resolve_mobile_runtime_context "${target}"`,
|
|
253
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
254
|
-
if (ctxOut.trim()) {
|
|
255
|
-
applyKVLines(ctxOut, true);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
} catch { /* no runtime context — fall through to pool */ }
|
|
259
|
-
try {
|
|
260
|
-
// Pool match always wins — overwrite whatever is in the environment.
|
|
261
|
-
const poolOut = execFileSync('bash', [
|
|
262
|
-
'-c', `source "${resolveScript}" && resolve_farmslot_ports_by_repo "${target}"`,
|
|
263
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
264
|
-
if (poolOut.trim()) {
|
|
265
|
-
applyKVLines(poolOut, true);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
} catch { /* no pool match — fall through to formula */ }
|
|
269
|
-
try {
|
|
270
|
-
// Formula match only fills unset vars (never overrides explicit env/pool).
|
|
271
|
-
const defOut = execFileSync('bash', [
|
|
272
|
-
'-c', `source "${resolveScript}" && resolve_mobile_slot_defaults "${target}"`,
|
|
273
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
274
|
-
if (defOut.trim()) applyKVLines(defOut, false);
|
|
275
|
-
} catch { /* no slot suffix in dir name — stays at env defaults */ }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Resolve extension slot ports the same way as mobile: the checkout's runtime
|
|
279
|
-
// context first (cdpPort/devServerPort written by the orchestrator's prepare),
|
|
280
|
-
// then the farmslot pool, then the directory-suffix formula (fills unset only).
|
|
281
|
-
function resolveExtensionSlotPorts(target: string): void {
|
|
282
|
-
// The prepared checkout's context OVERWRITES inherited env (same authority as
|
|
283
|
-
// mobile's context/pool resolution): a stale CDP_PORT from the shell must not
|
|
284
|
-
// hijack the slot's browser. Explicit CLI flags are applied after and win.
|
|
285
|
-
const contextPath = resolveRuntimeContextPath(target);
|
|
286
|
-
const cdp = readRuntimeContextField(contextPath, 'cdpPort');
|
|
287
|
-
if (cdp) {
|
|
288
|
-
process.env['CDP_PORT'] = cdp;
|
|
289
|
-
process.env['RECIPE_CDP_PORT'] = cdp;
|
|
290
|
-
}
|
|
291
|
-
const dev = readRuntimeContextField(contextPath, 'devServerPort');
|
|
292
|
-
if (dev) {
|
|
293
|
-
process.env['WATCHER_PORT'] = dev;
|
|
294
|
-
process.env['RECIPE_WATCHER_PORT'] = dev;
|
|
295
|
-
}
|
|
296
|
-
if (process.env['CDP_PORT']) return;
|
|
297
|
-
const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
|
|
298
|
-
try {
|
|
299
|
-
const poolOut = execFileSync('bash', [
|
|
300
|
-
'-c', `source "${resolveScript}" && resolve_farmslot_ports_by_repo "${target}"`,
|
|
301
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
302
|
-
if (poolOut.trim()) {
|
|
303
|
-
applyKVLines(poolOut, true);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
} catch { /* no pool match — fall through to formula */ }
|
|
307
|
-
try {
|
|
308
|
-
const defOut = execFileSync('bash', [
|
|
309
|
-
'-c', `source "${resolveScript}" && resolve_default_extension_ports "${target}"`,
|
|
310
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
311
|
-
if (defOut.trim()) applyKVLines(defOut, false);
|
|
312
|
-
} catch { /* no slot suffix in dir name — stays at env defaults */ }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
212
|
// Map the env-gap flags onto the env vars the leaf scripts read. For mobile, slot
|
|
316
213
|
// port/simulator defaults are resolved first (pool-wins, then formula-fills-empty)
|
|
317
214
|
// so that the harness respects slot isolation; explicit CLI flags applied below
|
|
@@ -325,9 +222,9 @@ function applyLaunchEnvOverrides(
|
|
|
325
222
|
// Slot isolation: resolve ports/device from the checkout's own slot context,
|
|
326
223
|
// the farmslot pool, or the directory-suffix formula BEFORE applying explicit
|
|
327
224
|
// flags — extension needs this as much as mobile (CDP_PORT), so neither
|
|
328
|
-
// adapter hard-fails on a value its slot already knows.
|
|
329
|
-
|
|
330
|
-
|
|
225
|
+
// adapter hard-fails on a value its slot already knows. The surface owns the
|
|
226
|
+
// per-platform resolution; this command never branches on adapter for it.
|
|
227
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
331
228
|
|
|
332
229
|
const device = str(options, 'device');
|
|
333
230
|
if (device && adapter === 'mobile') {
|
|
@@ -421,56 +318,10 @@ async function executeComposition(
|
|
|
421
318
|
async function extensionRebuild(target: string, json: boolean): Promise<ScriptResult> {
|
|
422
319
|
const runtimeDirRel = recipeRuntimeDir();
|
|
423
320
|
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
424
|
-
const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
|
|
425
321
|
const rebuildLog = path.join(runtimeAbs, 'rebuild.log');
|
|
426
322
|
|
|
427
323
|
// E1a: Kill harness-owned watcher via pid file, then ps-scan for orphans.
|
|
428
|
-
|
|
429
|
-
const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
|
|
430
|
-
if (/^\d+$/u.test(pid)) {
|
|
431
|
-
try { process.kill(Number(pid), 'SIGTERM'); } catch { /* already dead */ }
|
|
432
|
-
}
|
|
433
|
-
fs.rmSync(webpackPidFile, { force: true });
|
|
434
|
-
} catch { /* no pid file */ }
|
|
435
|
-
// Scan for any remaining orphan webpack/yarn-start processes in this checkout.
|
|
436
|
-
try {
|
|
437
|
-
const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
|
|
438
|
-
const orphanPids: number[] = [];
|
|
439
|
-
for (const line of psOut.split('\n')) {
|
|
440
|
-
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
441
|
-
if (!match) continue;
|
|
442
|
-
const [, pidStr, cmd] = match;
|
|
443
|
-
const isWatcher =
|
|
444
|
-
cmd.includes('yarn start') ||
|
|
445
|
-
cmd.includes('webpack --watch') ||
|
|
446
|
-
cmd.includes('development/webpack/launch.ts --watch');
|
|
447
|
-
if (!isWatcher) continue;
|
|
448
|
-
if (cmd.includes(target)) {
|
|
449
|
-
orphanPids.push(Number(pidStr));
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
// lsof cwd fallback for processes that don't embed the path in argv.
|
|
453
|
-
try {
|
|
454
|
-
const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
|
|
455
|
-
encoding: 'utf8',
|
|
456
|
-
timeout: 2000,
|
|
457
|
-
});
|
|
458
|
-
if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
|
|
459
|
-
orphanPids.push(Number(pidStr));
|
|
460
|
-
}
|
|
461
|
-
} catch { /* lsof unavailable or permission denied */ }
|
|
462
|
-
}
|
|
463
|
-
if (orphanPids.length > 0) {
|
|
464
|
-
for (const pid of orphanPids) {
|
|
465
|
-
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
466
|
-
}
|
|
467
|
-
// Brief pause then force-kill survivors.
|
|
468
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
469
|
-
for (const pid of orphanPids) {
|
|
470
|
-
try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
} catch { /* ps not available */ }
|
|
324
|
+
stopExtensionWatcher(target);
|
|
474
325
|
|
|
475
326
|
// E1c: Clear the rebuild log (directory must exist for tee).
|
|
476
327
|
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
package/src/commands/logs.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { runnerDir } from '../paths.ts';
|
|
9
|
+
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
9
10
|
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
10
11
|
|
|
11
12
|
const LOGS_BOOLEANS = new Set(['full', 'json']);
|
|
@@ -19,13 +20,23 @@ export async function handleLogs(argv: string[]): Promise<number> {
|
|
|
19
20
|
if (!adapter) {
|
|
20
21
|
return usageOut(json, 'logs', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const surface = getAdapterSurface(adapter);
|
|
24
|
+
if (surface.headless) {
|
|
25
|
+
return usageOut(json, 'logs', 'core is headless; it has no dev server logs.', surface.hints.launch);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Source selection is platform-scoped: the valid names and the default come
|
|
29
|
+
// from THIS adapter's dev-server logs (mobile: metro; extension: webpack), plus
|
|
30
|
+
// the app-log source. No adapter's vocabulary is hardcoded here — every
|
|
31
|
+
// non-headless adapter provides at least one log source (headless core returned
|
|
32
|
+
// above), so the default is that adapter's first source.
|
|
33
|
+
const logSources = surface.logSources(target);
|
|
34
|
+
const sourceLabels = logSources.map((entry) => entry.label);
|
|
35
|
+
const defaultSource = sourceLabels[0];
|
|
36
|
+
const source = str(options, 'source') ?? defaultSource;
|
|
37
|
+
const validSources = [...sourceLabels, 'app'];
|
|
38
|
+
if (!validSources.includes(source)) {
|
|
39
|
+
return usageOut(json, 'logs', `--source must be one of: ${validSources.join(', ')}.`, `mm-harness logs --source ${defaultSource}`);
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
// Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
|
|
@@ -39,16 +50,21 @@ export async function handleLogs(argv: string[]): Promise<number> {
|
|
|
39
50
|
process.env.RECIPE_LOG_EVENTS = events;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
// Nothing running → teaching error pointing at launch.
|
|
43
|
-
// signal that
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Nothing running → teaching error pointing at launch. A dev-server log file is
|
|
54
|
+
// the signal that the dev server has been started for this checkout; the surface
|
|
55
|
+
// owns which files a platform writes (mobile: metro.log; extension: webpack +
|
|
56
|
+
// watcher + rebuild logs). A `--source` naming a specific dev-server log is
|
|
57
|
+
// preferred; otherwise the most-relevant existing candidate is tailed.
|
|
58
|
+
const requested = logSources.find((entry) => entry.label === source);
|
|
59
|
+
const ordered = requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
|
|
60
|
+
const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
|
|
61
|
+
if (!logFile) {
|
|
62
|
+
const names = logSources.map((entry) => path.basename(entry.path)).join(' / ');
|
|
47
63
|
return usageOut(
|
|
48
64
|
json,
|
|
49
65
|
'logs',
|
|
50
|
-
`nothing running for this checkout (no ${
|
|
51
|
-
|
|
66
|
+
`nothing running for this checkout (no ${names}).`,
|
|
67
|
+
surface.hints.launch,
|
|
52
68
|
);
|
|
53
69
|
}
|
|
54
70
|
|
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
|
@@ -61,16 +61,18 @@ Example:
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
name: 'stop',
|
|
64
|
-
summary: 'Stop the dev server this checkout owns (mobile
|
|
64
|
+
summary: 'Stop the dev server this checkout owns (mobile Metro / extension webpack watcher) and close its log window.',
|
|
65
65
|
example: 'mm-harness stop',
|
|
66
66
|
helpText: `mm-harness stop [flags]
|
|
67
67
|
|
|
68
|
-
Stop the
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
Stop the dev server this checkout owns and close its tmux log-tail window,
|
|
69
|
+
scoped to this checkout so concurrent slots are untouched. Idempotent —
|
|
70
|
+
nothing running is success, not an error. Behavior is per platform:
|
|
71
|
+
mobile stop the port-scoped Metro dev server
|
|
72
|
+
extension stop the checkout's webpack watcher (pid file + orphan scan)
|
|
73
|
+
core headless — no dev server to stop (teaching error)
|
|
72
74
|
|
|
73
|
-
--port <port>
|
|
75
|
+
--port <port> Dev-server port (default: the checkout's slot context)
|
|
74
76
|
--target <path> Checkout path (default: cwd)
|
|
75
77
|
--json Machine-readable output
|
|
76
78
|
|
|
@@ -81,11 +83,13 @@ Example:
|
|
|
81
83
|
{
|
|
82
84
|
name: 'call',
|
|
83
85
|
summary: 'Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).',
|
|
84
|
-
example: 'mm-harness call
|
|
86
|
+
example: 'mm-harness call ensure_unlocked',
|
|
85
87
|
helpText: `mm-harness call <action> [--arg k=v ...] [flags]
|
|
86
88
|
|
|
87
89
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
88
|
-
Fuzzy short-name: '
|
|
90
|
+
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
91
|
+
if unique; ambiguous = exit 2. Actions differ per adapter — list this checkout's
|
|
92
|
+
with: mm-harness actions.
|
|
89
93
|
|
|
90
94
|
--arg k=v Action field value (repeatable)
|
|
91
95
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
@@ -95,9 +99,9 @@ Example:
|
|
|
95
99
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
96
100
|
--json Machine-readable output
|
|
97
101
|
|
|
98
|
-
Example:
|
|
99
|
-
mm-harness call
|
|
100
|
-
mm-harness call command --arg cmd="echo hi" --adapter core
|
|
102
|
+
Example (real actions; run mm-harness actions for this checkout's full set):
|
|
103
|
+
mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
|
|
104
|
+
mm-harness call command --arg cmd="echo hi" --adapter core # the universal action (all adapters)`,
|
|
101
105
|
},
|
|
102
106
|
{
|
|
103
107
|
name: 'flows',
|
|
@@ -155,21 +159,68 @@ Example:
|
|
|
155
159
|
mm-harness doctor --fix --json
|
|
156
160
|
mm-harness doctor --adapter mobile --target /path/to/checkout`,
|
|
157
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
|
+
},
|
|
158
195
|
{
|
|
159
196
|
name: 'install',
|
|
160
|
-
summary: 'Install the per-checkout runtime overlay
|
|
197
|
+
summary: 'Install the per-checkout runtime overlay, or --runway to install a cached mobile dev client.',
|
|
161
198
|
example: 'mm-harness install',
|
|
162
199
|
helpText: `mm-harness install [flags]
|
|
163
200
|
|
|
164
201
|
Install the per-checkout runtime overlay (for CI / agents).
|
|
165
|
-
|
|
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.
|
|
166
205
|
|
|
167
206
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
168
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
|
|
169
219
|
|
|
170
220
|
Example:
|
|
171
221
|
mm-harness install
|
|
172
|
-
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`,
|
|
173
224
|
},
|
|
174
225
|
{
|
|
175
226
|
name: 'verify',
|
|
@@ -241,8 +292,10 @@ Example:
|
|
|
241
292
|
|
|
242
293
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
243
294
|
--events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
|
|
244
|
-
--source <
|
|
245
|
-
|
|
295
|
+
--source <label> Log source per adapter — mobile: metro|app (default metro);
|
|
296
|
+
extension: webpack|watcher|rebuild|app (default webpack).
|
|
297
|
+
Core is headless (teaching error).
|
|
298
|
+
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
246
299
|
--target <path> Checkout path (default: cwd)
|
|
247
300
|
--json Machine-readable output
|
|
248
301
|
|
|
@@ -380,8 +433,8 @@ const HELP_GROUPS: HelpGroup[] = [
|
|
|
380
433
|
},
|
|
381
434
|
{
|
|
382
435
|
title: 'RUNTIME OVERLAY',
|
|
383
|
-
blurb: 'install/verify/clean the
|
|
384
|
-
commands: ['install', 'verify', 'cleanup'],
|
|
436
|
+
blurb: 'install/verify/clean the overlay, plus thin mobile slot provisioning',
|
|
437
|
+
commands: ['provision', 'install', 'verify', 'cleanup'],
|
|
385
438
|
},
|
|
386
439
|
{
|
|
387
440
|
title: 'MAINTAIN',
|