@deeeed/metamask-harness 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +47 -17
  3. package/{orchestration → adapters}/core/cleanup.sh +2 -2
  4. package/{orchestration → adapters}/core/inject.sh +8 -8
  5. package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
  6. package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
  7. package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
  8. package/{orchestration → adapters}/extension/inject.mjs +27 -31
  9. package/{orchestration → adapters}/extension/launch.sh +4 -4
  10. package/{orchestration → adapters}/extension/live.sh +5 -5
  11. package/{orchestration → adapters}/extension/readiness.mjs +12 -2
  12. package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
  13. package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
  14. package/{orchestration → adapters}/extension/start-watch.sh +7 -7
  15. package/{runner → adapters}/extension/verify.sh +11 -11
  16. package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
  17. package/{orchestration → adapters}/manifest.json +96 -96
  18. package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
  19. package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
  20. package/{orchestration → adapters}/mobile/inject.sh +22 -24
  21. package/adapters/mobile/open-device.sh +260 -0
  22. package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
  23. package/adapters/mobile/prewarm-bundle.sh +95 -0
  24. package/adapters/mobile/start-metro.sh +228 -0
  25. package/{runner → adapters}/mobile/verify.sh +6 -6
  26. package/adapters/mobile/wait-for-bridge.sh +108 -0
  27. package/adapters/mobile/yarn-setup.sh +20 -0
  28. package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
  29. package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
  30. package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
  31. package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
  32. package/adapters/shared/sync-wallet-fixture.sh +78 -0
  33. package/bin/mm-harness +37 -6
  34. package/docs/CLI-SPEC.md +41 -2
  35. package/docs/architecture.md +69 -38
  36. package/docs/runtime-file-conventions.md +3 -3
  37. package/library/actions/core/perps/_controller.mjs +1 -1
  38. package/library/actions/extension/platform/cdp.mjs +2 -2
  39. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  40. package/library/actions/mobile/platform/bridge.mjs +1 -1
  41. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  42. package/library/actions/mobile/wallet/setup.mjs +2 -2
  43. package/library/library.json +1 -1
  44. package/package.json +8 -8
  45. package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
  46. package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
  47. package/src/adapters/mobile/prepare.ts +210 -0
  48. package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
  49. package/{runner/src → src}/adapters.ts +3 -3
  50. package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
  51. package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
  52. package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
  53. package/{runner/src → src}/cli.ts +48 -40
  54. package/src/commands/debug.ts +63 -0
  55. package/src/commands/fixtures.ts +193 -0
  56. package/{runner/src → src}/commands/launch.ts +200 -54
  57. package/src/commands/logs.ts +83 -0
  58. package/{runner/src → src}/commands/shared.ts +40 -30
  59. package/src/commands/update.ts +316 -0
  60. package/{runner/src → src}/harness.ts +119 -16
  61. package/{runner/src → src}/heal-bounds.ts +20 -1
  62. package/src/index.ts +15 -0
  63. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  64. package/{runner/src → src}/mm-harness-cli.ts +64 -3
  65. package/{runner/src → src}/paths.ts +2 -2
  66. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  67. package/docs/extension-runtime-commands.md +0 -60
  68. package/docs/harness-cli.md +0 -43
  69. package/orchestration/compat-overlays/mobile/README.md +0 -13
  70. package/orchestration/doctor.mjs +0 -72
  71. package/orchestration/lib/cli-home.mjs +0 -354
  72. package/orchestration/lib/recipe-paths.mjs +0 -26
  73. package/orchestration/mobile/launch.sh +0 -137
  74. package/orchestration/mobile/live.sh +0 -125
  75. package/orchestration/porcelain/metamask-recipe +0 -99
  76. package/orchestration/porcelain/mm-recipe +0 -1591
  77. package/orchestration/porcelain/mme-recipe +0 -1181
  78. package/runner/src/commands/debug.ts +0 -44
  79. package/runner/src/commands/fixtures.ts +0 -99
  80. package/runner/src/commands/logs.ts +0 -60
  81. package/runner/src/index.ts +0 -6
  82. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  83. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  84. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  85. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  90. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  91. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  92. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  93. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  94. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  95. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  99. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  100. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  101. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  102. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  103. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  104. /package/{runner/src → src}/completions-cache.ts +0 -0
  105. /package/{runner/src → src}/doctor.ts +0 -0
  106. /package/{runner/src → src}/manifest.ts +0 -0
  107. /package/{runner/src → src}/recording-target.ts +0 -0
  108. /package/{runner/src → src}/run-recording.ts +0 -0
  109. /package/{runner/src → src}/runner.ts +0 -0
  110. /package/{runner/src → src}/types.ts +0 -0
@@ -1,43 +0,0 @@
1
- # `mm-harness` — Command Reference
2
-
3
- > This page is a pointer. The full, authoritative command contract lives in
4
- > [CLI-SPEC.md](CLI-SPEC.md); the before→after overview is in
5
- > [MENTAL-MODEL.md](MENTAL-MODEL.md). This file exists only so links to
6
- > `harness-cli.md` keep resolving.
7
-
8
- `mm-harness` is the MetaMask recipe harness — the single front door for launching
9
- the app, proving behavior, and managing the per-checkout **runtime overlay**. It
10
- is the only bin: platform (`mobile | extension | core`) is auto-detected from the
11
- checkout, a positional target forces it (`mm-harness launch ios`), and depth lives
12
- in flags (`--json`, ports, build tiers), never in extra command names.
13
-
14
- Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
15
- (`@farmslot/recipe-harness`). The files the tool installs into a target checkout
16
- are the **runtime overlay** — never "the harness".
17
-
18
- ## Command surface (all shipped)
19
-
20
- ```
21
- DAILY LOOP launch · logs · debug · fixtures what a teammate runs many times a day
22
- DISCOVER actions · call · flows compose recipes from the vocabulary + flow library
23
- PROVE run · doctor run recipes, check readiness
24
- RUNTIME OVERLAY install · verify · cleanup install/verify/clean the per-checkout overlay
25
- ```
26
-
27
- Grammar: `mm-harness <command> [target] [flags]`. `--json` is the agent contract on
28
- every command; the bare command is the human happy path.
29
-
30
- - `run` validates first, then executes; `run --plan` validates and prints the plan
31
- without touching a device.
32
- - `call <action>` runs one action from the adapter vocabulary through the same
33
- engine path `run` uses.
34
- - `doctor` is read-only; `doctor --fix` runs the self-healing steps (overlay
35
- auto-ensure; never a fixture reseed) without launching the app and reports
36
- `fixed[]` / `failed[]`.
37
- - `flows` lists the recipe library (bare = `flows list`); `flows promote` keeps a
38
- proven flow.
39
- - `install` / `verify` / `cleanup` manage the runtime overlay; the DAILY LOOP
40
- commands auto-ensure it.
41
-
42
- For flags, exit-code taxonomy, `--json` shapes, and per-command detail, see
43
- [CLI-SPEC.md](CLI-SPEC.md).
@@ -1,13 +0,0 @@
1
- # Mobile historical compatibility overlays
2
-
3
- These patch files document temporary source overlays used by Recipe v1 historical
4
- injection evals when an older MetaMask Mobile checkout cannot boot under the
5
- current local Xcode/Hermes toolchain.
6
-
7
- They are intentionally kept in the external runner repository, not Farmslot root
8
- scripts, so a `/recipe-harness` skill can apply the same reversible overlay before
9
- a historical rebuild and record the overlay path in validation evidence.
10
-
11
- - `rn81-message-event-source.patch`: adds a read-only `MessageEvent.prototype.source`
12
- getter for React Native 0.81 historical checkouts whose bundled polyfill omits
13
- it while CDP websocket clients expect browser-compatible `MessageEvent` shape.
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- // doctor.mjs — orchestration feature-surface doctor
3
- //
4
- // Purpose:
5
- // Verifies orchestration/manifest.json against reality: every entry point
6
- // exists; every script entry answers --help with exit 0; and every script
7
- // under orchestration/{mobile,extension,core} is listed (no doc drift).
8
- //
9
- // Inputs (flags): --json (machine-readable report)
10
- // Outputs: per-feature lines (or JSON report) on stdout.
11
- // Exit 0 — surface healthy; 1 — missing entry, failing --help, or
12
- // unlisted script; 2 — bad args.
13
- // Never touches: anything (read-only + --help subprocesses).
14
- import fs from 'node:fs';
15
- import path from 'node:path';
16
- import { spawnSync } from 'node:child_process';
17
- import { fileURLToPath } from 'node:url';
18
-
19
- let json = false;
20
- for (const arg of process.argv.slice(2)) {
21
- if (arg === '--json') json = true;
22
- else if (arg === '-h' || arg === '--help') {
23
- console.log('Usage: doctor.mjs [--json]');
24
- process.exit(0);
25
- } else {
26
- console.error(`Unknown arg: ${arg}`);
27
- process.exit(2);
28
- }
29
- }
30
-
31
- const orchestrationDir = path.dirname(fileURLToPath(import.meta.url));
32
- const runnerRoot = path.resolve(orchestrationDir, '..');
33
- const manifest = JSON.parse(fs.readFileSync(path.join(orchestrationDir, 'manifest.json'), 'utf8'));
34
-
35
- const results = [];
36
- for (const feature of manifest.features) {
37
- const entryAbs = path.join(runnerRoot, feature.entry);
38
- const checks = { exists: fs.existsSync(entryAbs), help: null };
39
- if (checks.exists && (feature.kind === 'bash' || feature.kind === 'node')) {
40
- const runner = feature.kind === 'bash' ? 'bash' : process.execPath;
41
- const run = spawnSync(runner, [entryAbs, '--help'], { encoding: 'utf8', timeout: 30000 });
42
- checks.help = run.status === 0;
43
- }
44
- const ok = checks.exists && (checks.help === null || checks.help === true);
45
- results.push({ id: feature.id, entry: feature.entry, kind: feature.kind, ...checks, ok });
46
- }
47
-
48
- // drift guard: every script in the orchestration trees must be listed
49
- const listed = new Set(manifest.features.map((feature) => feature.entry));
50
- const unlisted = [];
51
- for (const platform of ['mobile', 'extension', 'core', 'lib']) {
52
- const dir = path.join(orchestrationDir, platform);
53
- if (!fs.existsSync(dir)) continue;
54
- for (const entry of fs.readdirSync(dir)) {
55
- const rel = `orchestration/${platform}/${entry}`;
56
- if (!/\.(?:sh|mjs|cjs|json)$/u.test(entry)) continue;
57
- if (!listed.has(rel)) unlisted.push(rel);
58
- }
59
- }
60
-
61
- const failed = results.filter((result) => !result.ok);
62
- const status = failed.length === 0 && unlisted.length === 0 ? 'pass' : 'fail';
63
- if (json) {
64
- console.log(JSON.stringify({ status, features: results, unlisted }, null, 2));
65
- } else {
66
- for (const result of results) {
67
- console.log(`${result.ok ? 'ok ' : 'FAIL'} ${result.id} (${result.entry})${result.help === false ? ' [--help failed]' : ''}${result.exists ? '' : ' [missing]'}`);
68
- }
69
- for (const rel of unlisted) console.log(`FAIL unlisted script not in manifest: ${rel}`);
70
- console.log(`orchestration surface: ${status} (${results.length - failed.length}/${results.length} features, ${unlisted.length} unlisted)`);
71
- }
72
- process.exit(status === 'pass' ? 0 : 1);
@@ -1,354 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * cli-home.mjs — recipe CLI home screen and ports output.
4
- */
5
- import fs from 'node:fs';
6
- import http from 'node:http';
7
- import path from 'node:path';
8
- import { parseArgs } from 'node:util';
9
- import {
10
- color,
11
- colorKv,
12
- colorStatusWord,
13
- } from './cli-color.mjs';
14
-
15
- const OUT = process.stdout;
16
-
17
- function readJson(file) {
18
- if (!file || !fs.existsSync(file)) return null;
19
- try {
20
- return JSON.parse(fs.readFileSync(file, 'utf8'));
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- function httpProbe(url, timeoutMs = 1500) {
27
- return new Promise((resolve) => {
28
- const req = http.get(url, { timeout: timeoutMs }, (res) => {
29
- let body = '';
30
- res.on('data', (chunk) => { body += chunk; });
31
- res.on('end', () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, body }));
32
- });
33
- req.on('timeout', () => { req.destroy(); resolve({ ok: false, body: '' }); });
34
- req.on('error', () => resolve({ ok: false, body: '' }));
35
- });
36
- }
37
-
38
- function watcherPhase(logPath) {
39
- if (!logPath || !fs.existsSync(logPath)) return 'stopped';
40
- const text = fs.readFileSync(logPath, 'utf8');
41
- if (/Module build failed|^ERROR in /m.test(text)) return 'failed';
42
- if (/compiled successfully|MetaMask .* compiled|Bundle end:/m.test(text)) return 'ready';
43
- if (/webpack.Progress|Building|Compiling|yarn start/m.test(text)) return 'compiling';
44
- return 'unknown';
45
- }
46
-
47
- function metroPhase(logPath) {
48
- if (!logPath || !fs.existsSync(logPath)) return 'stopped';
49
- const text = fs.readFileSync(logPath, 'utf8');
50
- if (/Bundl(ed|ing)|Finished/m.test(text)) {
51
- const match = text.match(/(iOS|Android).*?([0-9]+(?:\.[0-9]+)?)%/);
52
- if (match && Number(match[2]) < 100) return 'bundling';
53
- return 'ready';
54
- }
55
- return 'starting';
56
- }
57
-
58
- function mobileBridgeSnapshot(runtimeStatus) {
59
- const raw = runtimeStatus?.bridge?.status;
60
- const targets = Array.isArray(raw) ? raw : raw ? [raw] : [];
61
- const ready = targets.find((item) => item && typeof item === 'object' && item.route);
62
- if (!ready) return { route: null, account: null, deviceName: null };
63
- return {
64
- route: ready.route?.name || null,
65
- account: ready.account?.name || ready.account?.address || null,
66
- deviceName: ready.deviceName || null,
67
- };
68
- }
69
-
70
- function watcherPhaseLabel(phase) {
71
- switch (phase) {
72
- case 'ready': return 'ready';
73
- case 'compiling': return 'compiling';
74
- case 'failed': return 'failed';
75
- case 'stopped': return 'stopped';
76
- default: return phase || 'unknown';
77
- }
78
- }
79
-
80
- function metroPhaseLabel(phase) {
81
- switch (phase) {
82
- case 'running':
83
- case 'ready': return 'ready';
84
- case 'bundling': return 'bundling';
85
- case 'starting': return 'starting';
86
- case 'down': return 'down';
87
- default: return phase || 'unknown';
88
- }
89
- }
90
-
91
- function nextActions(adapter, state) {
92
- const actions = [];
93
- if (state.harnessStale) {
94
- actions.push({ command: 'recipe sync', reason: 'refresh harness scripts + fixture context' });
95
- }
96
- if (adapter === 'extension') {
97
- if (state.ready) {
98
- actions.push({ command: 'recipe refresh', reason: 'fast incremental update when watcher is active' });
99
- actions.push({ command: 'recipe debug', reason: 'open Chrome DevTools' });
100
- actions.push({ command: 'recipe logs', reason: 'compact last build events (recipe logs --full for raw tail)' });
101
- } else if (state.watcher === 'ready' && state.decision === 'build') {
102
- actions.push({ command: 'recipe refresh', reason: 'watcher is up; wait for dist to catch up' });
103
- actions.push({ command: 'recipe rebuild', reason: 'heavy clean rebuild if refresh stalls' });
104
- } else if (state.watcher === 'compiling') {
105
- actions.push({ command: 'recipe logs', reason: 'compact webpack progress' });
106
- actions.push({ command: 'recipe status', reason: 're-check readiness' });
107
- } else {
108
- actions.push({ command: 'recipe up', reason: 'decide and run the minimum path to ready' });
109
- actions.push({ command: 'recipe rebuild', reason: 'clean webpack + browser launch (2-8 min)' });
110
- actions.push({ command: 'recipe watch', reason: 'start watcher only' });
111
- }
112
- if (!state.fixturePresent) actions.unshift({ command: 'recipe sync', reason: 'install fixture + harness context' });
113
- } else {
114
- if (state.ready) {
115
- actions.push({ command: 'recipe debug', reason: 'open React Native DevTools' });
116
- actions.push({ command: 'recipe logs', reason: 'compact Metro events (recipe logs --full for raw tail)' });
117
- } else if (state.metro === 'ready' && !state.bridgeReady) {
118
- actions.push({ command: 'recipe up', reason: 'launch dev client and wait for bridge' });
119
- actions.push({ command: 'recipe logs', reason: 'compact bundle progress' });
120
- } else {
121
- actions.push({ command: 'recipe up', reason: 'start Metro + launch dev client as needed' });
122
- actions.push({ command: 'recipe ios', reason: 'launch iOS dev client' });
123
- actions.push({ command: 'recipe android', reason: 'launch Android dev client' });
124
- }
125
- if (!state.fixturePresent) actions.unshift({ command: 'recipe sync', reason: 'install fixture + harness context' });
126
- }
127
- return actions.slice(0, 5);
128
- }
129
-
130
- function renderHuman(payload) {
131
- const lines = [];
132
- const head = payload.adapter === 'extension' ? 'Extension recipe' : 'Mobile recipe';
133
- lines.push(`${color('bold', head, { stream: OUT })} ${color('dim', '|', { stream: OUT })} ${color('path', payload.root, { stream: OUT })}`);
134
- lines.push(`${colorKv('slot', payload.slotId || '?', 'info', { stream: OUT })} ${colorKv('runtime', payload.runtimeDir, 'path', { stream: OUT })}`);
135
- if (payload.harnessStale) {
136
- lines.push(color('warn', 'harness: stale - run: recipe sync', { stream: OUT }));
137
- }
138
- if (payload.adapter === 'extension') {
139
- lines.push(
140
- `${colorKv('cdp', String(payload.ports.cdp ?? '?'), 'info', { stream: OUT })} ${colorKv('watcher-port', String(payload.ports.watcher ?? '?'), 'info', { stream: OUT })}`,
141
- );
142
- lines.push([
143
- `${color('label', 'browser:', { stream: OUT })} ${colorStatusWord(payload.browser, { stream: OUT })}`,
144
- `${color('label', 'webpack:', { stream: OUT })} ${colorStatusWord(watcherPhaseLabel(payload.watcher), { stream: OUT })}`,
145
- `${color('label', 'ready:', { stream: OUT })} ${colorStatusWord(payload.ready ? 'yes' : 'no', { stream: OUT })}`,
146
- payload.reason ? color('comment', ` (${payload.reason})`, { stream: OUT }) : '',
147
- ].join(' '));
148
- lines.push(`${color('label', 'fixture:', { stream: OUT })} ${colorStatusWord(payload.fixturePresent ? 'present' : 'missing', { stream: OUT })}`);
149
- } else {
150
- const bridgeBits = [];
151
- if (payload.appRoute) bridgeBits.push(`route=${payload.appRoute}`);
152
- if (payload.appAccount) bridgeBits.push(`account=${payload.appAccount}`);
153
- const bridgeDetail = bridgeBits.length > 0 ? color('comment', ` (${bridgeBits.join(', ')})`, { stream: OUT }) : '';
154
- lines.push(
155
- `${colorKv('metro-port', String(payload.ports.watcher ?? '?'), 'info', { stream: OUT })} ${colorKv('device', payload.device || payload.appDevice || '?', 'info', { stream: OUT })} ${color('label', 'metro:', { stream: OUT })} ${colorStatusWord(metroPhaseLabel(payload.metro), { stream: OUT })} ${color('label', 'bridge:', { stream: OUT })} ${colorStatusWord(payload.bridge, { stream: OUT })}${bridgeDetail}`,
156
- );
157
- lines.push([
158
- `${color('label', 'fixture:', { stream: OUT })} ${colorStatusWord(payload.fixturePresent ? 'present' : 'missing', { stream: OUT })}`,
159
- `${color('label', 'ready:', { stream: OUT })} ${colorStatusWord(payload.ready ? 'yes' : 'no', { stream: OUT })}`,
160
- payload.reason ? color('comment', ` (${payload.reason})`, { stream: OUT }) : '',
161
- ].join(' '));
162
- }
163
- if (payload.decision) {
164
- lines.push(`${colorKv('decision', payload.decision, 'ok', { stream: OUT })}${payload.reasonCode ? color('comment', ` - ${payload.reasonCode}`, { stream: OUT }) : ''}`);
165
- }
166
- if (payload.tmux) {
167
- lines.push(`${colorKv('tmux', payload.tmux, 'accent', { stream: OUT })}${color('comment', ' (webpack window may still stream full logs)', { stream: OUT })}`);
168
- }
169
- lines.push('');
170
- lines.push(color('label', 'next:', { stream: OUT }));
171
- for (const item of payload.nextActions) {
172
- lines.push(` ${color('cmd', item.command, { stream: OUT })} ${color('comment', `# ${item.reason}`, { stream: OUT })}`);
173
- }
174
- lines.push('');
175
- lines.push(color('comment', 'speed: fast -> watch -> refresh slow -> rebuild inspect -> status | logs | logs --full | debug', { stream: OUT }));
176
- return `${lines.join('\n')}\n`;
177
- }
178
-
179
- function renderPorts(payload) {
180
- const lines = [
181
- colorKv('slot', payload.slotId || '?', 'info', { stream: OUT }),
182
- `${colorKv('cdp', String(payload.ports.cdp ?? '?'), 'info', { stream: OUT })} ${colorKv('watcher', String(payload.ports.watcher ?? '?'), 'info', { stream: OUT })}`,
183
- `${colorKv('runtime', payload.runtimeDir, 'path', { stream: OUT })} ${colorKv('harness', payload.harnessDir, 'path', { stream: OUT })}`,
184
- ];
185
- if (payload.tmux) lines.push(colorKv('tmux', payload.tmux, 'accent', { stream: OUT }));
186
- return `${lines.join('\n')}\n`;
187
- }
188
-
189
- async function buildHome({ adapter, target, runtimeDir, ports, runtimeStatus, decision, harnessStale = false }) {
190
- const runtimeAbs = path.join(target, runtimeDir);
191
- const context = readJson(path.join(runtimeAbs, 'agentic-runtime.json'))
192
- || readJson(path.join(runtimeAbs, 'recipe-runtime.json'))
193
- || {};
194
- const slotId = context.slotId || null;
195
- const fixturePresent = Boolean(
196
- runtimeStatus?.fixture?.present
197
- || fs.existsSync(path.join(runtimeAbs, 'wallet-fixture.json')),
198
- );
199
-
200
- let browser = 'down';
201
- let watcher = 'stopped';
202
- let metro = 'stopped';
203
- let bridge = 'down';
204
- let bridgeReady = false;
205
- let device = process.env.IOS_SIMULATOR || process.env.ANDROID_DEVICE || process.env.ADB_SERIAL || null;
206
- let tmux = null;
207
-
208
- if (adapter === 'extension') {
209
- const cdp = ports.cdp;
210
- if (cdp) {
211
- const probe = await httpProbe(`http://127.0.0.1:${cdp}/json/version`);
212
- browser = probe.ok ? 'up' : 'down';
213
- }
214
- const logPath = path.join(runtimeAbs, 'webpack.log');
215
- watcher = watcherPhase(logPath);
216
- const slot = (path.basename(target).match(/-(\d+)$/) || [])[1];
217
- if (slot) tmux = `mme-${slot}:webpack-${ports.watcher || '?'}`;
218
- } else {
219
- const port = ports.watcher;
220
- if (port) {
221
- const status = await httpProbe(`http://127.0.0.1:${port}/status`);
222
- metro = status.ok && status.body.includes('packager-status:running') ? 'running' : 'down';
223
- if (metro === 'running') metro = metroPhase(path.join(runtimeAbs, 'metro.log'));
224
- }
225
- bridgeReady = Boolean(runtimeStatus?.bridge?.ready);
226
- bridge = bridgeReady ? 'ready' : (runtimeStatus?.debugTarget?.present ? 'waiting' : 'down');
227
- }
228
-
229
- const ready = Boolean(runtimeStatus?.ready);
230
- const decisionValue = decision?.decision || runtimeStatus?.decision?.decision || null;
231
- const reasonCode = decision?.reasonCode || runtimeStatus?.decision?.reasonCode || runtimeStatus?.reason || null;
232
- const bridgeSnap = adapter === 'mobile' ? mobileBridgeSnapshot(runtimeStatus) : null;
233
-
234
- const payload = {
235
- schemaVersion: 1,
236
- kind: 'home',
237
- adapter,
238
- root: target,
239
- runtimeDir,
240
- slotId,
241
- ports,
242
- fixturePresent,
243
- harnessStale,
244
- ready,
245
- reason: runtimeStatus?.reason || null,
246
- appRoute: bridgeSnap?.route || null,
247
- appAccount: bridgeSnap?.account || null,
248
- appDevice: bridgeSnap?.deviceName || null,
249
- decision: decisionValue,
250
- reasonCode,
251
- browser: adapter === 'extension' ? browser : undefined,
252
- watcher: adapter === 'extension' ? watcher : undefined,
253
- metro: adapter === 'mobile' ? metro : undefined,
254
- bridge: adapter === 'mobile' ? bridge : undefined,
255
- bridgeReady: adapter === 'mobile' ? bridgeReady : undefined,
256
- device: adapter === 'mobile' ? device : undefined,
257
- tmux: adapter === 'extension' ? tmux : undefined,
258
- runtimeStatus,
259
- nextActions: [],
260
- };
261
- payload.nextActions = nextActions(adapter, {
262
- ready,
263
- watcher,
264
- metro,
265
- bridgeReady,
266
- fixturePresent,
267
- harnessStale,
268
- decision: decisionValue,
269
- });
270
- return payload;
271
- }
272
-
273
- function buildPorts({ adapter, target, runtimeDir, ports, slotId }) {
274
- return {
275
- schemaVersion: 1,
276
- kind: 'ports',
277
- adapter,
278
- root: target,
279
- runtimeDir,
280
- slotId: slotId || null,
281
- ports,
282
- tmux: adapter === 'extension' && ports.watcher
283
- ? `mme-${(path.basename(target).match(/-(\d+)$/) || ['', ''])[1]}:webpack-${ports.watcher}`
284
- : null,
285
- harnessDir: process.env.RECIPE_HARNESS_ROOT || 'temp/recipe/harness',
286
- };
287
- }
288
-
289
- async function main() {
290
- const { values } = parseArgs({
291
- args: process.argv.slice(2),
292
- options: {
293
- adapter: { type: 'string' },
294
- target: { type: 'string' },
295
- 'runtime-dir': { type: 'string' },
296
- 'cdp-port': { type: 'string' },
297
- 'watcher-port': { type: 'string' },
298
- 'slot-id': { type: 'string' },
299
- mode: { type: 'string', default: 'home' },
300
- 'runtime-status-file': { type: 'string' },
301
- 'decision-file': { type: 'string' },
302
- 'harness-stale': { type: 'boolean', default: false },
303
- json: { type: 'boolean', default: false },
304
- },
305
- allowPositionals: false,
306
- });
307
-
308
- if (!values.adapter || !values.target) {
309
- console.error('cli-home.mjs requires --adapter and --target');
310
- process.exit(2);
311
- }
312
-
313
- const ports = {
314
- cdp: values['cdp-port'] ? Number(values['cdp-port']) : null,
315
- watcher: values['watcher-port'] ? Number(values['watcher-port']) : null,
316
- };
317
- const runtimeStatus = readJson(values['runtime-status-file']);
318
- const decision = readJson(values['decision-file']);
319
-
320
- const payload = values.mode === 'ports'
321
- ? buildPorts({
322
- adapter: values.adapter,
323
- target: values.target,
324
- runtimeDir: values['runtime-dir'] || 'temp/recipe/runtime',
325
- ports,
326
- slotId: values['slot-id'],
327
- })
328
- : await buildHome({
329
- adapter: values.adapter,
330
- target: values.target,
331
- runtimeDir: values['runtime-dir'] || 'temp/recipe/runtime',
332
- ports,
333
- runtimeStatus,
334
- decision,
335
- harnessStale: values['harness-stale'],
336
- });
337
-
338
- if (values.json) {
339
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
340
- return;
341
- }
342
-
343
- if (payload.kind === 'ports') {
344
- process.stdout.write(renderPorts(payload));
345
- return;
346
- }
347
-
348
- process.stdout.write(renderHuman(payload));
349
- }
350
-
351
- main().catch((error) => {
352
- console.error(error instanceof Error ? error.message : String(error));
353
- process.exit(1);
354
- });
@@ -1,26 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- const defaultsPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'path-defaults.json');
6
- const defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8'));
7
-
8
- export const DEFAULT_RECIPE_HARNESS_ROOT = defaults.recipeHarnessRoot;
9
- export const DEFAULT_RECIPE_RUNTIME_DIR = defaults.recipeRuntimeDir;
10
-
11
- export function validateRelativeRecipePath(name, value) {
12
- if (!value || path.isAbsolute(value)) throw new Error(`${name} must be a non-empty relative path: ${value}`);
13
- if (!/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${name} contains unsupported characters: ${value}`);
14
- for (const part of value.split('/')) {
15
- if (!part || part === '.' || part === '..') throw new Error(`${name} contains unsafe path component: ${value}`);
16
- }
17
- return value;
18
- }
19
-
20
- export function recipeHarnessRoot(env = process.env) {
21
- return validateRelativeRecipePath('RECIPE_HARNESS_ROOT', env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
22
- }
23
-
24
- export function recipeRuntimeDir(env = process.env) {
25
- return validateRelativeRecipePath('RECIPE_RUNTIME_DIR', env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
26
- }
@@ -1,137 +0,0 @@
1
- #!/usr/bin/env bash
2
- # launch.sh — mobile instance identity intake + prepare/preflight launch
3
- #
4
- # Purpose:
5
- # Resolves the per-instance tuple (port, simulator/adb-serial, runtime dir,
6
- # platform, preflight mode) and runs the installed runner's mobile prepare.
7
- #
8
- # Inputs (flags / env precedence):
9
- # --target <metamask-mobile> (default $PWD)
10
- # --platform ios|android (default ios)
11
- # --preflight-mode|--mode fast|auto|default|rebuild-native|clean
12
- # (env RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE, default fast)
13
- # --port|--watcher-port|--cdp-port <port> (env WATCHER_PORT > METRO_PORT > CDP_PORT)
14
- # --simulator <sim> (env IOS_SIMULATOR > SIM_UDID)
15
- # --adb-serial <serial> (env ADB_SERIAL > ANDROID_SERIAL)
16
- # --runtime-dir <dir> (env RECIPE_RUNTIME_DIR)
17
- # --wallet-setup / --no-wallet-setup, --wallet-fixture <json>
18
- # --artifacts-dir <dir>
19
- #
20
- # Outputs:
21
- # <artifacts>/summary.json (adapter/action/status/runtime/generatedAt),
22
- # <artifacts>/runtime-status.json, <artifacts>/logs/prepare.log
23
- # Exit 0 — launch pass; 1 — runner missing or prepare failed; 2 — bad args.
24
- #
25
- # Never touches: product source files; anything outside the target's harness
26
- # dir and the artifacts dir.
27
- set -euo pipefail
28
-
29
- TARGET="$PWD"
30
- PLATFORM="ios"
31
- PREFLIGHT_MODE="${RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE:-fast}"
32
- ARTIFACTS=""
33
- PORT="${WATCHER_PORT:-${METRO_PORT:-${CDP_PORT:-}}}"
34
- SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-}}"
35
- ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
36
- RUNTIME_DIR_ARG="${RECIPE_RUNTIME_DIR:-}"
37
- WALLET_SETUP=false
38
- WALLET_FIXTURE_ARG=""
39
- require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
40
- while [ "$#" -gt 0 ]; do
41
- case "$1" in
42
- --target) require_value "$@"; TARGET="$2"; shift 2 ;;
43
- --platform) require_value "$@"; PLATFORM="$2"; shift 2 ;;
44
- --preflight-mode|--mode) require_value "$@"; PREFLIGHT_MODE="$2"; shift 2 ;;
45
- --artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
46
- --port|--watcher-port|--cdp-port) require_value "$@"; PORT="$2"; shift 2 ;;
47
- --simulator) require_value "$@"; SIMULATOR="$2"; shift 2 ;;
48
- --adb-serial) require_value "$@"; ADB_SERIAL_ARG="$2"; shift 2 ;;
49
- --runtime-dir) require_value "$@"; RUNTIME_DIR_ARG="$2"; shift 2 ;;
50
- --wallet-setup) WALLET_SETUP=true; shift ;;
51
- --no-wallet-setup) WALLET_SETUP=false; shift ;;
52
- --wallet-fixture) require_value "$@"; WALLET_FIXTURE_ARG="$2"; shift 2 ;;
53
- -h|--help)
54
- echo "Usage: launch.sh [--target <metamask-mobile>] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--port <port>] [--simulator <sim>] [--runtime-dir <dir>] [--wallet-setup] [--wallet-fixture <json>]"
55
- exit 0
56
- ;;
57
- *) echo "Unknown arg: $1" >&2; exit 2 ;;
58
- esac
59
- done
60
-
61
- case "$PLATFORM" in ios|android) ;; *) echo "Unknown --platform: $PLATFORM" >&2; exit 2 ;; esac
62
- case "$PREFLIGHT_MODE" in fast|auto|default|rebuild-native|clean) ;; *) echo "Unknown --preflight-mode: $PREFLIGHT_MODE" >&2; exit 2 ;; esac
63
-
64
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
65
- # shellcheck disable=SC1091
66
- for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
67
- [ -f "$_hp" ] && { . "$_hp"; break; }
68
- done
69
- unset _hp
70
- if ! command -v harness_root >/dev/null 2>&1; then
71
- echo "mm-harness: shared lib orchestration/lib/harness-path.sh not found; reinstall the runner." >&2
72
- exit 1
73
- fi
74
- TARGET="$(cd "$TARGET" && pwd)"
75
- HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
76
- RUNNER_BIN="$HARNESS_DIR/runner/bin/metamask-recipe"
77
- ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/launch/$(date -u +%Y%m%dT%H%M%SZ)}"
78
- mkdir -p "$ARTIFACTS/logs"
79
-
80
- if [ ! -x "$RUNNER_BIN" ]; then
81
- echo "Mobile recipe runner is not installed in $TARGET. Run metamask-recipe mobile install --target $TARGET first." >&2
82
- exit 1
83
- fi
84
-
85
- prepare_args=(mobile prepare --target "$TARGET" --platform "$PLATFORM" --preflight-mode "$PREFLIGHT_MODE")
86
- [ -n "$PORT" ] && prepare_args+=(--port "$PORT")
87
- [ -n "$SIMULATOR" ] && prepare_args+=(--simulator "$SIMULATOR")
88
- [ -n "$ADB_SERIAL_ARG" ] && prepare_args+=(--adb-serial "$ADB_SERIAL_ARG")
89
- [ -n "$RUNTIME_DIR_ARG" ] && prepare_args+=(--runtime-dir "$RUNTIME_DIR_ARG")
90
- $WALLET_SETUP && prepare_args+=(--wallet-setup)
91
- [ -n "$WALLET_FIXTURE_ARG" ] && prepare_args+=(--wallet-fixture "$WALLET_FIXTURE_ARG")
92
-
93
- status="pass"
94
- if (cd "$TARGET" && "$RUNNER_BIN" "${prepare_args[@]}") > "$ARTIFACTS/logs/prepare.log" 2>&1; then
95
- runtime_status="pass"
96
- else
97
- runtime_status="fail"
98
- status="fail"
99
- fi
100
-
101
- status_args=(mobile runtime-status --target "$TARGET" --json)
102
- [ -n "$PORT" ] && status_args+=(--port "$PORT")
103
- [ -n "$RUNTIME_DIR_ARG" ] && status_args+=(--runtime-dir "$RUNTIME_DIR_ARG")
104
- (cd "$TARGET" && "$RUNNER_BIN" "${status_args[@]}") > "$ARTIFACTS/runtime-status.json" 2> "$ARTIFACTS/logs/runtime-status.err" || true
105
-
106
- TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" STATUS_FOR_SUMMARY="$status" RUNTIME_STATUS="$runtime_status" PLATFORM_FOR_SUMMARY="$PLATFORM" MODE_FOR_SUMMARY="$PREFLIGHT_MODE" node <<'NODE'
107
- const fs = require('fs');
108
- const path = require('path');
109
- const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
110
- let runtime = null;
111
- try { runtime = JSON.parse(fs.readFileSync(path.join(artifacts, 'runtime-status.json'), 'utf8')); } catch {}
112
- fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
113
- adapter: 'mobile',
114
- action: 'launch',
115
- status: process.env.STATUS_FOR_SUMMARY,
116
- platform: process.env.PLATFORM_FOR_SUMMARY,
117
- preflightMode: process.env.MODE_FOR_SUMMARY,
118
- target: process.env.TARGET_FOR_SUMMARY,
119
- runtime: {
120
- status: process.env.RUNTIME_STATUS,
121
- ready: runtime?.ready === true,
122
- reason: runtime?.reason || null,
123
- port: runtime?.metro?.port || null,
124
- prepareLogPath: path.join(artifacts, 'logs/prepare.log'),
125
- runtimeStatusPath: path.join(artifacts, 'runtime-status.json'),
126
- },
127
- runtimePolicy: {
128
- nativeBuildPolicy: process.env.MODE_FOR_SUMMARY === 'fast'
129
- ? 'fast mode never runs a native build; it requires an installed dev client, starts/reuses Metro, prewarms the JS bundle, opens the dev client, and waits for the bridge'
130
- : 'non-fast modes may install or rebuild the native dev client when needed; then start/reuse Metro, prewarm the JS bundle, open the dev client, and wait for the bridge',
131
- },
132
- generatedAt: new Date().toISOString(),
133
- }, null, 2)}\n`);
134
- NODE
135
-
136
- echo "Mobile harness launch $status: $ARTIFACTS/summary.json"
137
- [ "$status" = "pass" ]