@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.
- package/CHANGELOG.md +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* open-debug.mjs — open human-facing debugger UIs for recipe runtimes.
|
|
4
|
+
*
|
|
5
|
+
* Mobile: Metro /open-debugger or /open-dev-menu HTTP endpoints.
|
|
6
|
+
* Extension: Chrome DevTools frontend for an extension page or service worker.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.error(`Usage:
|
|
12
|
+
open-debug.mjs --adapter mobile [--port <watcher>] [--action debug|dev-menu] [--json] [--no-open]
|
|
13
|
+
open-debug.mjs --adapter extension [--cdp-port <port>] [--target page|worker] [--json] [--no-open]
|
|
14
|
+
|
|
15
|
+
Actions (mobile):
|
|
16
|
+
debug Open React Native DevTools / Hermes debugger (default)
|
|
17
|
+
dev-menu Open the React Native developer menu
|
|
18
|
+
|
|
19
|
+
Targets (extension):
|
|
20
|
+
page Extension UI page, preferring home.html (default)
|
|
21
|
+
worker Extension service worker background context`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = {
|
|
26
|
+
adapter: null,
|
|
27
|
+
port: null,
|
|
28
|
+
cdpPort: null,
|
|
29
|
+
action: 'debug',
|
|
30
|
+
target: 'page',
|
|
31
|
+
json: false,
|
|
32
|
+
open: true,
|
|
33
|
+
help: false,
|
|
34
|
+
};
|
|
35
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
36
|
+
const arg = argv[i];
|
|
37
|
+
switch (arg) {
|
|
38
|
+
case '--adapter':
|
|
39
|
+
args.adapter = argv[++i];
|
|
40
|
+
break;
|
|
41
|
+
case '--port':
|
|
42
|
+
args.port = argv[++i];
|
|
43
|
+
break;
|
|
44
|
+
case '--cdp-port':
|
|
45
|
+
args.cdpPort = argv[++i];
|
|
46
|
+
break;
|
|
47
|
+
case '--action':
|
|
48
|
+
args.action = argv[++i];
|
|
49
|
+
break;
|
|
50
|
+
case '--target':
|
|
51
|
+
args.target = argv[++i];
|
|
52
|
+
break;
|
|
53
|
+
case '--json':
|
|
54
|
+
args.json = true;
|
|
55
|
+
break;
|
|
56
|
+
case '--no-open':
|
|
57
|
+
args.open = false;
|
|
58
|
+
break;
|
|
59
|
+
case '-h':
|
|
60
|
+
case '--help':
|
|
61
|
+
args.help = true;
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function emit(result, json) {
|
|
71
|
+
if (json) {
|
|
72
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
const detail = result.method ? ` via ${result.method}` : '';
|
|
77
|
+
const where = result.port ? ` (port ${result.port})` : result.cdpPort ? ` (cdp ${result.cdpPort})` : '';
|
|
78
|
+
console.log(`Opened debugger${detail}${where}`);
|
|
79
|
+
if (result.target?.url) console.log(`Target: ${result.target.url}`);
|
|
80
|
+
if (result.configureHint) console.log(`Configure chrome://inspect target: ${result.configureHint}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.error(result.error || 'Failed to open debugger');
|
|
84
|
+
if (result.hint) console.error(result.hint);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchOk(url, init) {
|
|
89
|
+
const response = await fetch(url, init);
|
|
90
|
+
return response.ok;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchJson(url) {
|
|
94
|
+
const response = await fetch(url);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
97
|
+
}
|
|
98
|
+
const body = await response.json();
|
|
99
|
+
return body;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function openUrl(url) {
|
|
103
|
+
if (process.platform === 'darwin') {
|
|
104
|
+
if (url.startsWith('devtools://') || url.startsWith('chrome://')) {
|
|
105
|
+
const chrome = spawnSync('open', ['-a', 'Google Chrome', url], { stdio: 'ignore' });
|
|
106
|
+
if (chrome.status === 0) return 'chrome';
|
|
107
|
+
}
|
|
108
|
+
spawnSync('open', [url], { stdio: 'ignore' });
|
|
109
|
+
return 'open';
|
|
110
|
+
}
|
|
111
|
+
if (process.platform === 'linux') {
|
|
112
|
+
spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
113
|
+
return 'xdg-open';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function openMobileDebugger(port, action, shouldOpen) {
|
|
119
|
+
const normalizedAction = action === 'dev-menu' ? 'dev-menu' : 'debug';
|
|
120
|
+
const path = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
|
|
121
|
+
const numericPort = Number.parseInt(String(port), 10);
|
|
122
|
+
if (!Number.isInteger(numericPort) || numericPort <= 0) {
|
|
123
|
+
return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const opened = await fetchOk(`http://127.0.0.1:${numericPort}${path}`, { method: 'POST' });
|
|
128
|
+
if (opened) {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
adapter: 'mobile',
|
|
132
|
+
action: normalizedAction,
|
|
133
|
+
method: 'metro-http',
|
|
134
|
+
port: numericPort,
|
|
135
|
+
opened: shouldOpen,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
adapter: 'mobile',
|
|
142
|
+
action: normalizedAction,
|
|
143
|
+
method: 'metro-http',
|
|
144
|
+
port: numericPort,
|
|
145
|
+
error: error instanceof Error ? error.message : String(error),
|
|
146
|
+
hint: 'Metro may be down or the debug target is not ready. Try recipe refresh, then recipe runtime-status.',
|
|
147
|
+
fallback: 'cdp-eval',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
adapter: 'mobile',
|
|
154
|
+
action: normalizedAction,
|
|
155
|
+
method: 'metro-http',
|
|
156
|
+
port: numericPort,
|
|
157
|
+
error: 'metro-http-failed',
|
|
158
|
+
hint: 'Metro rejected the open-debugger request. Try recipe refresh, then recipe runtime-status.',
|
|
159
|
+
fallback: 'cdp-eval',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pickExtensionTarget(targets, targetKind) {
|
|
164
|
+
const extensionTargets = targets.filter((target) => String(target.url || '').startsWith('chrome-extension://'));
|
|
165
|
+
if (targetKind === 'worker') {
|
|
166
|
+
return (
|
|
167
|
+
extensionTargets.find((target) => target.type === 'service_worker')
|
|
168
|
+
|| targets.find((target) => target.type === 'service_worker' && String(target.url || '').startsWith('chrome-extension://'))
|
|
169
|
+
|| null
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pages = extensionTargets.filter((target) => target.type === 'page');
|
|
174
|
+
return (
|
|
175
|
+
pages.find((target) => String(target.url || '').includes('/home.html'))
|
|
176
|
+
|| pages.find((target) => !String(target.url || '').includes('/popup-init.html'))
|
|
177
|
+
|| pages[0]
|
|
178
|
+
|| null
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
183
|
+
const numericPort = Number.parseInt(String(cdpPort), 10);
|
|
184
|
+
if (!Number.isInteger(numericPort) || numericPort <= 0) {
|
|
185
|
+
return { ok: false, adapter: 'extension', error: 'invalid-cdp-port', cdpPort };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const inspectUrl = 'chrome://inspect/#devices';
|
|
189
|
+
const configureHint = `127.0.0.1:${numericPort}`;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await fetchJson(`http://127.0.0.1:${numericPort}/json/version`);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
adapter: 'extension',
|
|
197
|
+
targetKind,
|
|
198
|
+
cdpPort: numericPort,
|
|
199
|
+
error: 'cdp-unreachable',
|
|
200
|
+
inspectUrl,
|
|
201
|
+
configureHint,
|
|
202
|
+
hint: `Start the extension runtime first (recipe reopen). Then open chrome://inspect and add ${configureHint}.`,
|
|
203
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let targets;
|
|
208
|
+
try {
|
|
209
|
+
targets = await fetchJson(`http://127.0.0.1:${numericPort}/json/list`);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
adapter: 'extension',
|
|
214
|
+
targetKind,
|
|
215
|
+
cdpPort: numericPort,
|
|
216
|
+
error: 'cdp-list-failed',
|
|
217
|
+
inspectUrl,
|
|
218
|
+
configureHint,
|
|
219
|
+
hint: `CDP is up but /json/list failed. Open chrome://inspect and add ${configureHint}.`,
|
|
220
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!Array.isArray(targets)) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
adapter: 'extension',
|
|
228
|
+
targetKind,
|
|
229
|
+
cdpPort: numericPort,
|
|
230
|
+
error: 'cdp-list-invalid',
|
|
231
|
+
inspectUrl,
|
|
232
|
+
configureHint,
|
|
233
|
+
hint: `Open chrome://inspect and add ${configureHint}.`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const picked = pickExtensionTarget(targets, targetKind);
|
|
238
|
+
if (picked?.devtoolsFrontendUrl && shouldOpen) {
|
|
239
|
+
openUrl(picked.devtoolsFrontendUrl);
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
adapter: 'extension',
|
|
243
|
+
targetKind,
|
|
244
|
+
method: 'devtools-frontend',
|
|
245
|
+
cdpPort: numericPort,
|
|
246
|
+
target: {
|
|
247
|
+
type: picked.type,
|
|
248
|
+
url: picked.url,
|
|
249
|
+
title: picked.title || null,
|
|
250
|
+
},
|
|
251
|
+
devtoolsUrl: picked.devtoolsFrontendUrl,
|
|
252
|
+
inspectUrl,
|
|
253
|
+
configureHint,
|
|
254
|
+
opened: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (shouldOpen) {
|
|
259
|
+
openUrl(inspectUrl);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
adapter: 'extension',
|
|
265
|
+
targetKind,
|
|
266
|
+
method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'chrome-inspect',
|
|
267
|
+
cdpPort: numericPort,
|
|
268
|
+
target: picked
|
|
269
|
+
? { type: picked.type, url: picked.url, title: picked.title || null }
|
|
270
|
+
: null,
|
|
271
|
+
devtoolsUrl: picked?.devtoolsFrontendUrl || null,
|
|
272
|
+
inspectUrl,
|
|
273
|
+
configureHint,
|
|
274
|
+
opened: shouldOpen,
|
|
275
|
+
hint: picked
|
|
276
|
+
? 'Opened chrome://inspect because no devtools frontend URL was available for the selected target.'
|
|
277
|
+
: `No extension targets are registered yet. Open chrome://inspect and add ${configureHint}.`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function main() {
|
|
282
|
+
const args = parseArgs(process.argv.slice(2));
|
|
283
|
+
if (args.help) {
|
|
284
|
+
usage();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
if (!args.adapter) {
|
|
288
|
+
usage();
|
|
289
|
+
process.exit(2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let result;
|
|
293
|
+
if (args.adapter === 'mobile') {
|
|
294
|
+
result = await openMobileDebugger(args.port || process.env.WATCHER_PORT || '8081', args.action, args.open);
|
|
295
|
+
} else if (args.adapter === 'extension') {
|
|
296
|
+
result = await openExtensionDevtools(
|
|
297
|
+
args.cdpPort || process.env.CDP_PORT || process.env.RECIPE_CDP_PORT,
|
|
298
|
+
args.target === 'worker' ? 'worker' : 'page',
|
|
299
|
+
args.open,
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
throw new Error(`Unsupported adapter: ${args.adapter}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
emit(result, args.json);
|
|
306
|
+
if (!result.ok) process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
main().catch((error) => {
|
|
310
|
+
const payload = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
311
|
+
if (process.argv.includes('--json')) {
|
|
312
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
313
|
+
} else {
|
|
314
|
+
console.error(payload.error);
|
|
315
|
+
}
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* progress.mjs — structured progress events for long recipe CLI operations.
|
|
4
|
+
*
|
|
5
|
+
* Human mode: short lines on stderr.
|
|
6
|
+
* Machine mode: JSONL on stderr when --json or RECIPE_PROGRESS_JSON=1.
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { color } from './cli-color.mjs';
|
|
10
|
+
|
|
11
|
+
function shouldEmitJson(explicit) {
|
|
12
|
+
if (explicit) return true;
|
|
13
|
+
const env = process.env.RECIPE_PROGRESS_JSON;
|
|
14
|
+
return env === '1' || env === 'true';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function emitProgress(event, { json = false } = {}) {
|
|
18
|
+
const payload = {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
type: 'progress',
|
|
21
|
+
ts: new Date().toISOString(),
|
|
22
|
+
...event,
|
|
23
|
+
};
|
|
24
|
+
if (json || shouldEmitJson(false)) {
|
|
25
|
+
process.stderr.write(`${JSON.stringify(payload)}\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const stream = process.stderr;
|
|
29
|
+
const prefix = color('label', 'recipe', { stream });
|
|
30
|
+
if (payload.phase === 'done') {
|
|
31
|
+
const statusStyle = payload.status === 'pass' ? 'ok' : 'err';
|
|
32
|
+
const statusWord = payload.status === 'pass' ? 'done' : 'failed';
|
|
33
|
+
const verb = payload.verb ? color('cmd', `${payload.verb}`, { stream }) : '';
|
|
34
|
+
const elapsed = Number.isFinite(payload.elapsedMs)
|
|
35
|
+
? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
|
|
36
|
+
: '';
|
|
37
|
+
process.stderr.write(`${prefix}: ${verb} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const verb = payload.verb ? `${color('cmd', payload.verb, { stream })}: ` : '';
|
|
41
|
+
const phase = payload.phase ? `${color('accent', `[${payload.phase}]`, { stream })} ` : '';
|
|
42
|
+
const elapsed = Number.isFinite(payload.elapsedMs)
|
|
43
|
+
? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
|
|
44
|
+
: '';
|
|
45
|
+
process.stderr.write(`${prefix}: ${verb}${phase}${payload.message || ''}${elapsed}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function usage() {
|
|
49
|
+
console.error(`Usage:
|
|
50
|
+
progress.mjs emit --verb <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
|
|
51
|
+
progress.mjs done --verb <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
56
|
+
if (!command || command === '--help' || command === '-h') {
|
|
57
|
+
usage();
|
|
58
|
+
process.exit(command ? 0 : 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { values } = parseArgs({
|
|
62
|
+
args: rest,
|
|
63
|
+
options: {
|
|
64
|
+
verb: { type: 'string' },
|
|
65
|
+
phase: { type: 'string' },
|
|
66
|
+
message: { type: 'string' },
|
|
67
|
+
status: { type: 'string' },
|
|
68
|
+
'elapsed-ms': { type: 'string' },
|
|
69
|
+
json: { type: 'boolean', default: false },
|
|
70
|
+
},
|
|
71
|
+
allowPositionals: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const elapsedMs = values['elapsed-ms'] ? Number.parseInt(values['elapsed-ms'], 10) : undefined;
|
|
75
|
+
const json = values.json || shouldEmitJson(false);
|
|
76
|
+
|
|
77
|
+
if (command === 'emit') {
|
|
78
|
+
emitProgress({
|
|
79
|
+
verb: values.verb || 'recipe',
|
|
80
|
+
phase: values.phase || 'working',
|
|
81
|
+
message: values.message || '',
|
|
82
|
+
elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
|
|
83
|
+
}, { json });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (command === 'done') {
|
|
88
|
+
emitProgress({
|
|
89
|
+
verb: values.verb || 'recipe',
|
|
90
|
+
phase: 'done',
|
|
91
|
+
status: values.status || 'pass',
|
|
92
|
+
message: values.message || '',
|
|
93
|
+
elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
|
|
94
|
+
}, { json });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
usage();
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (import.meta.url === new URL(process.argv[1], 'file:').href) {
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# resolve-farmslot-ports.sh — resolve CDP + watcher ports for an extension checkout
|
|
2
|
+
#
|
|
3
|
+
# Farmslot pool JSON is authoritative when a slot repo matches; otherwise fall back
|
|
4
|
+
# to the local-extension-N formula (6660+N / 9010+N).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# . resolve-farmslot-ports.sh
|
|
8
|
+
# resolve_extension_runtime_ports /path/to/metamask-extension-N
|
|
9
|
+
# -> prints CDP_PORT=... WATCHER_PORT=... SLOT_ID=... lines
|
|
10
|
+
|
|
11
|
+
resolve_farmslot_ports_by_repo() {
|
|
12
|
+
local repo="$1"
|
|
13
|
+
local farmslot_root="${FARMSLOT_ROOT:-}"
|
|
14
|
+
[ -n "$repo" ] || return 1
|
|
15
|
+
repo="$(cd "$repo" && pwd -P)"
|
|
16
|
+
|
|
17
|
+
local root candidate match
|
|
18
|
+
for root in "$farmslot_root" "$HOME/dev/farmslot"; do
|
|
19
|
+
[ -n "$root" ] && [ -d "$root/pool" ] || continue
|
|
20
|
+
match="$(POOL_ROOT="$root" REPO_TARGET="$repo" python3 - <<'PY'
|
|
21
|
+
import glob, json, os, re, sys
|
|
22
|
+
|
|
23
|
+
pool_root = os.environ["POOL_ROOT"]
|
|
24
|
+
repo = os.path.realpath(os.environ["REPO_TARGET"])
|
|
25
|
+
base = os.path.basename(repo)
|
|
26
|
+
slot_suffix = None
|
|
27
|
+
m = re.search(r"-(\d+)$", base)
|
|
28
|
+
if m:
|
|
29
|
+
slot_suffix = m.group(1)
|
|
30
|
+
|
|
31
|
+
def score_slot(slot):
|
|
32
|
+
resources = slot.get("resources") or {}
|
|
33
|
+
cdp = (resources.get("browser") or {}).get("cdp_port")
|
|
34
|
+
port = (resources.get("dev-server") or {}).get("port")
|
|
35
|
+
session = slot.get("session") or ""
|
|
36
|
+
slot_id = slot.get("id") or ""
|
|
37
|
+
score = 0
|
|
38
|
+
if cdp is not None:
|
|
39
|
+
score += 100
|
|
40
|
+
if port is not None:
|
|
41
|
+
score += 10
|
|
42
|
+
if slot_suffix:
|
|
43
|
+
if session == f"mme-{slot_suffix}":
|
|
44
|
+
score += 50
|
|
45
|
+
if slot_id.endswith(f"mme-{slot_suffix}") or f"-mme-{slot_suffix}" in slot_id:
|
|
46
|
+
score += 40
|
|
47
|
+
if "demo" not in slot_id.lower():
|
|
48
|
+
score += 5
|
|
49
|
+
return score, cdp, port, slot_id
|
|
50
|
+
|
|
51
|
+
best = None
|
|
52
|
+
for path in sorted(glob.glob(os.path.join(pool_root, "pool", "*.json"))):
|
|
53
|
+
if ".bak." in os.path.basename(path):
|
|
54
|
+
continue
|
|
55
|
+
try:
|
|
56
|
+
data = json.load(open(path, encoding="utf-8"))
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
for slot in data.get("slots") or []:
|
|
60
|
+
slot_repo = slot.get("repo") or ""
|
|
61
|
+
if not slot_repo:
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
if os.path.realpath(slot_repo) != repo:
|
|
65
|
+
continue
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
scored = score_slot(slot)
|
|
69
|
+
if best is None or scored[0] > best[0]:
|
|
70
|
+
best = scored
|
|
71
|
+
|
|
72
|
+
if best is None:
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
_, cdp, port, slot_id = best
|
|
76
|
+
if cdp is not None:
|
|
77
|
+
print(f"CDP_PORT={cdp}")
|
|
78
|
+
if port is not None:
|
|
79
|
+
print(f"WATCHER_PORT={port}")
|
|
80
|
+
if slot_id:
|
|
81
|
+
print(f"SLOT_ID={slot_id}")
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
PY
|
|
84
|
+
)" || continue
|
|
85
|
+
[ -n "$match" ] || continue
|
|
86
|
+
printf '%s\n' "$match"
|
|
87
|
+
return 0
|
|
88
|
+
done
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
infer_extension_slot_suffix() {
|
|
93
|
+
local repo="$1" base
|
|
94
|
+
base="$(basename "$repo")"
|
|
95
|
+
if [[ "$base" =~ -([0-9]+)$ ]]; then
|
|
96
|
+
printf '%s' "${BASH_REMATCH[1]}"
|
|
97
|
+
return 0
|
|
98
|
+
fi
|
|
99
|
+
return 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolve_default_extension_ports() {
|
|
103
|
+
local repo="$1" n
|
|
104
|
+
n="$(infer_extension_slot_suffix "$repo" || true)"
|
|
105
|
+
[ -n "$n" ] || return 1
|
|
106
|
+
printf 'CDP_PORT=%s\n' "$((6660 + n))"
|
|
107
|
+
printf 'WATCHER_PORT=%s\n' "$((9010 + n))"
|
|
108
|
+
printf 'SLOT_ID=local-extension-%s\n' "$n"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolve_extension_runtime_ports() {
|
|
112
|
+
local repo="${1:-.}"
|
|
113
|
+
if resolve_farmslot_ports_by_repo "$repo"; then
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
resolve_default_extension_ports "$repo"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
apply_resolved_extension_ports() {
|
|
120
|
+
local repo="${1:-.}" line key val from_pool=false resolved=""
|
|
121
|
+
if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
122
|
+
from_pool=true
|
|
123
|
+
elif resolved="$(resolve_default_extension_ports "$repo" 2>/dev/null)"; then
|
|
124
|
+
from_pool=false
|
|
125
|
+
else
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
while IFS= read -r line; do
|
|
129
|
+
[ -n "$line" ] || continue
|
|
130
|
+
key="${line%%=*}"
|
|
131
|
+
val="${line#*=}"
|
|
132
|
+
case "$key" in
|
|
133
|
+
CDP_PORT)
|
|
134
|
+
if [ "$from_pool" = true ] || [ -z "${CDP_PORT:-}" ]; then CDP_PORT="$val"; fi
|
|
135
|
+
;;
|
|
136
|
+
WATCHER_PORT)
|
|
137
|
+
if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
|
|
138
|
+
;;
|
|
139
|
+
SLOT_ID)
|
|
140
|
+
if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
done <<< "$resolved"
|
|
144
|
+
}
|