@deeeed/metamask-harness 0.2.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,354 @@
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
+ });
@@ -0,0 +1,252 @@
1
+ # cli-ux.sh — shared recipe CLI UX helpers for mm-harness porcelain wrappers.
2
+ # Sourced, not executed.
3
+
4
+ recipe_color_enabled() {
5
+ [ -n "${NO_COLOR:-}" ] && [ "${NO_COLOR:-}" != "0" ] && return 1
6
+ [ "${RECIPE_NO_COLOR:-}" = "1" ] && return 1
7
+ [ "${RECIPE_COLOR:-}" = "1" ] || [ "${FORCE_COLOR:-}" = "1" ] || [ -t 2 ]
8
+ }
9
+
10
+ recipe_color() {
11
+ local style="$1" text="$2"
12
+ if ! recipe_color_enabled; then
13
+ printf '%s' "$text"
14
+ return 0
15
+ fi
16
+ case "$style" in
17
+ bold) printf '\033[1m%s\033[0m' "$text" ;;
18
+ dim|comment|path) printf '\033[2m%s\033[0m' "$text" ;;
19
+ label) printf '\033[1;36m%s\033[0m' "$text" ;;
20
+ cmd) printf '\033[1m%s\033[0m' "$text" ;;
21
+ ok) printf '\033[1;32m%s\033[0m' "$text" ;;
22
+ warn) printf '\033[1;33m%s\033[0m' "$text" ;;
23
+ err) printf '\033[1;31m%s\033[0m' "$text" ;;
24
+ *) printf '%s' "$text" ;;
25
+ esac
26
+ }
27
+
28
+ recipe_progress_script() {
29
+ printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/progress.mjs}"
30
+ }
31
+
32
+ recipe_home_script() {
33
+ printf '%s' "${RECIPE_HOME_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-home.mjs}"
34
+ }
35
+
36
+ recipe_log_tui_script() {
37
+ printf '%s' "${RECIPE_LOG_TUI_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/log-tui.mjs}"
38
+ }
39
+
40
+ recipe_version_script() {
41
+ printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-version.mjs}"
42
+ }
43
+
44
+ recipe_log_ui_mode() {
45
+ printf '%s' "${RECIPE_LOG_UI:-compact}"
46
+ }
47
+
48
+ recipe_log_events_limit() {
49
+ printf '%s' "${RECIPE_LOG_EVENTS:-10}"
50
+ }
51
+
52
+ recipe_watch_build_log() {
53
+ local log_path="$1" label="${2:-build}" success_pattern="${3:-}"
54
+ local args=(
55
+ watch --log "$log_path"
56
+ --events "$(recipe_log_events_limit)"
57
+ --mode "$(recipe_log_ui_mode)"
58
+ --label "$label"
59
+ )
60
+ [ -n "$success_pattern" ] && args+=(--success-pattern "$success_pattern")
61
+ node "$(recipe_log_tui_script)" "${args[@]}"
62
+ }
63
+
64
+ recipe_watch_rebuild_log() {
65
+ local log_path="$1"
66
+ recipe_watch_build_log "$log_path" "runtime rebuild" \
67
+ 'PASS runtime-launch|Extension harness verify pass|runtime rebuild complete'
68
+ }
69
+
70
+ recipe_instant_hint() {
71
+ local message="$1"
72
+ [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && return 0
73
+ [ -t 2 ] || return 0
74
+ local prefix="${RECIPE_CLI_PREFIX:-recipe}"
75
+ printf '%s: %s\n' \
76
+ "$(recipe_color label "$prefix")" \
77
+ "$(recipe_color dim "$message")" >&2
78
+ }
79
+
80
+ recipe_progress() {
81
+ local verb="$1" phase="$2" message="$3" elapsed="${4:-}"
82
+ local args=(emit --verb "$verb" --phase "$phase" --message "$message")
83
+ [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
84
+ [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
85
+ node "$(recipe_progress_script)" "${args[@]}" || true
86
+ }
87
+
88
+ recipe_done() {
89
+ local verb="$1" status="$2" message="$3" elapsed="${4:-}"
90
+ local args=(done --verb "$verb" --status "$status" --message "$message")
91
+ [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
92
+ [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
93
+ node "$(recipe_progress_script)" "${args[@]}" || true
94
+ }
95
+
96
+ recipe_failure_playbook() {
97
+ local verb="$1" reason="$2"
98
+ shift 2
99
+ local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-recipe}"
100
+ while [ "$#" -gt 0 ]; do
101
+ case "$1" in
102
+ --log) log_path="$2"; shift 2 ;;
103
+ --artifact) artifact="$2"; shift 2 ;;
104
+ --prefix) prefix="$2"; shift 2 ;;
105
+ *) shift ;;
106
+ esac
107
+ done
108
+ printf '%s: %s %s - %s\n' \
109
+ "$(recipe_color label "$prefix")" \
110
+ "$(recipe_color cmd "$verb")" \
111
+ "$(recipe_color err failed)" \
112
+ "$(recipe_color warn "$reason")" >&2
113
+ printf '%s\n' "$(recipe_color label next:)" >&2
114
+ case "$verb" in
115
+ rebuild|up|watch)
116
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
117
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
118
+ printf ' %s\n' "$(recipe_color cmd "$prefix doctor")" >&2
119
+ printf ' %s\n' "$(recipe_color cmd "$prefix sync")" >&2
120
+ ;;
121
+ refresh)
122
+ printf ' %s\n' "$(recipe_color cmd "$prefix watch")" >&2
123
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
124
+ printf ' %s\n' "$(recipe_color cmd "$prefix rebuild")" >&2
125
+ ;;
126
+ metro|ios|android|setup-wallet)
127
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
128
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
129
+ printf ' %s\n' "$(recipe_color cmd "$prefix up")" >&2
130
+ ;;
131
+ *)
132
+ printf ' %s\n' "$(recipe_color cmd "$prefix status")" >&2
133
+ printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
134
+ ;;
135
+ esac
136
+ [ -n "$log_path" ] && printf '%s\n' "$(recipe_color path "log: $log_path")" >&2
137
+ [ -n "$artifact" ] && printf '%s\n' "$(recipe_color path "artifact: $artifact")" >&2
138
+ }
139
+
140
+ recipe_check_harness_stale() {
141
+ local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
142
+ local manifest="" sha=""
143
+ case "$adapter" in
144
+ extension) manifest="$root/$harness_root/extension/manifest.json" ;;
145
+ mobile) manifest="$root/$harness_root/mobile/manifest.json" ;;
146
+ *) return 0 ;;
147
+ esac
148
+ [ -f "$manifest" ] || return 0
149
+ sha="${manifest%/*}/installed-scripts.sha256"
150
+ node "$(recipe_version_script)" check \
151
+ --runner-root "$runner_root" \
152
+ --manifest "$manifest" \
153
+ ${sha:+--installed-sha "$sha"} \
154
+ --json 2>/dev/null | grep -q '"stale"[[:space:]]*:[[:space:]]*true'
155
+ }
156
+
157
+ recipe_warn_harness_stale() {
158
+ local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
159
+ local prefix="${RECIPE_CLI_PREFIX:-recipe}"
160
+ if recipe_check_harness_stale "$adapter" "$root" "$harness_root" "$runner_root"; then
161
+ printf '%s: %s - run: %s\n' \
162
+ "$(recipe_color label "$prefix")" \
163
+ "$(recipe_color warn 'harness scripts are stale')" \
164
+ "$(recipe_color cmd "$prefix sync")" >&2
165
+ return 1
166
+ fi
167
+ return 0
168
+ }
169
+
170
+ recipe_infer_farmslot_root() {
171
+ if [ -n "${FARMSLOT_ROOT:-}" ] && [ -d "${FARMSLOT_ROOT}" ]; then
172
+ printf '%s' "${FARMSLOT_ROOT}"
173
+ return 0
174
+ fi
175
+ local candidate=""
176
+ for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
177
+ [ -d "$candidate/scripts" ] || continue
178
+ printf '%s' "$candidate"
179
+ return 0
180
+ done
181
+ return 1
182
+ }
183
+
184
+ recipe_try_farmslot_fixture_sync() {
185
+ local slot_id="$1"
186
+ local farmslot_root=""
187
+ [ -n "$slot_id" ] || return 1
188
+ farmslot_root="$(recipe_infer_farmslot_root || true)"
189
+ [ -n "$farmslot_root" ] || return 1
190
+ [ -f "$farmslot_root/scripts/sync-fixtures.sh" ] || return 1
191
+ bash "$farmslot_root/scripts/sync-fixtures.sh" --slot "$slot_id"
192
+ }
193
+
194
+ recipe_sync_wallet_fixture() {
195
+ local root="$1" runtime_dir="$2" cdp_port="${3:-}" seed_script="${4:-}" farmslot_slot="${5:-}"
196
+ local canonical="$root/$runtime_dir/wallet-fixture.json"
197
+ local candidate resolved prefix="${RECIPE_CLI_PREFIX:-recipe}"
198
+
199
+ mkdir -p "$root/$runtime_dir"
200
+ if [ -f "$canonical" ]; then
201
+ return 0
202
+ fi
203
+
204
+ if [ -n "${RECIPE_WALLET_FIXTURE:-}" ] && [ -f "${RECIPE_WALLET_FIXTURE}" ]; then
205
+ cp "${RECIPE_WALLET_FIXTURE}" "$canonical"
206
+ chmod 600 "$canonical" 2>/dev/null || true
207
+ printf '%s: fixture: linked %s/wallet-fixture.json from RECIPE_WALLET_FIXTURE\n' "$prefix" "$runtime_dir" >&2
208
+ return 0
209
+ fi
210
+
211
+ if [ -n "$seed_script" ] && [ -f "$seed_script" ]; then
212
+ resolved="$(cd "$root" && bash "$seed_script" resolve --target "$root" ${cdp_port:+--cdp-port "$cdp_port"} 2>/dev/null || true)"
213
+ if [ -n "$resolved" ] && [ -f "$resolved" ] && [ "$resolved" != "$canonical" ]; then
214
+ cp "$resolved" "$canonical"
215
+ chmod 600 "$canonical" 2>/dev/null || true
216
+ printf '%s: fixture: copied to %s/wallet-fixture.json\n' "$prefix" "$runtime_dir" >&2
217
+ return 0
218
+ fi
219
+ fi
220
+
221
+ for candidate in \
222
+ "$root/temp/runtime/wallet-fixture.json" \
223
+ ${cdp_port:+"$root/temp/.recipe-validation-${cdp_port}/wallet-fixture.json"} \
224
+ "$root/temp/.agent-validation/wallet-fixture.json"
225
+ do
226
+ [ -f "$candidate" ] || continue
227
+ cp "$candidate" "$canonical"
228
+ chmod 600 "$canonical" 2>/dev/null || true
229
+ printf '%s: fixture: copied to %s/wallet-fixture.json\n' "$prefix" "$runtime_dir" >&2
230
+ return 0
231
+ done
232
+
233
+ if [ -n "$farmslot_slot" ]; then
234
+ printf '%s: fixture: trying farmslot sync-fixtures --slot %s\n' "$prefix" "$farmslot_slot" >&2
235
+ if recipe_try_farmslot_fixture_sync "$farmslot_slot"; then
236
+ [ -f "$canonical" ] && return 0
237
+ fi
238
+ fi
239
+
240
+ return 1
241
+ }
242
+
243
+ recipe_speed_ladder() {
244
+ cat >&2 <<'EOF'
245
+ Speed ladder:
246
+ FAST (seconds): recipe watch -> recipe refresh
247
+ SLOW (minutes): recipe rebuild
248
+ INSPECT: recipe status | recipe ports | recipe logs | recipe logs --full | recipe debug
249
+ PROOF: recipe run <recipe.json>
250
+ SYNC CONTEXT: recipe sync
251
+ EOF
252
+ }