@deeeed/metamask-harness 0.70.0 → 0.71.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 +28 -0
- package/README.md +16 -9
- package/adapters/extension/console-tail.mjs +38 -6
- package/adapters/extension/launch-browser.cjs +16 -4
- package/adapters/manifest.json +128 -0
- package/adapters/shared/browser-cdp.cjs +21 -7
- package/adapters/shared/browser-resolver.cjs +22 -11
- package/adapters/terminal/cleanup.mjs +68 -0
- package/adapters/terminal/inject.mjs +74 -0
- package/adapters/terminal/launch.mjs +628 -0
- package/adapters/terminal/lib/cdp-client.mjs +84 -0
- package/adapters/terminal/lib/display.mjs +115 -0
- package/adapters/terminal/lib/hud.mjs +148 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
- package/adapters/terminal/lib/origin.mjs +30 -0
- package/adapters/terminal/lib/page-script.mjs +246 -0
- package/adapters/terminal/lib/processes.mjs +187 -0
- package/adapters/terminal/lib/readiness.mjs +274 -0
- package/adapters/terminal/lib/strict-wallet.mjs +138 -0
- package/adapters/terminal/stop.mjs +37 -0
- package/adapters/terminal/verify.mjs +48 -0
- package/adapters/terminal/wallet-host.mjs +703 -0
- package/dist/adapters/console-capture.js +122 -0
- package/dist/adapters/extension/console-capture.js +22 -85
- package/dist/adapters/surface.js +3 -1
- package/dist/adapters/terminal/console-capture.js +46 -0
- package/dist/adapters/terminal/stop.js +19 -0
- package/dist/adapters/terminal/surface.js +104 -0
- package/dist/adapters.js +27 -4
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +12 -4
- package/dist/commands/check.js +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/domain.js +1 -1
- package/dist/commands/farmslot-ready.js +2 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/launch/index.js +97 -2
- package/dist/commands/manifest.js +3 -3
- package/dist/commands/recipe-advice.js +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/run-engine.js +1 -1
- package/dist/commands/shared.js +1 -1
- package/dist/doctor.js +4 -1
- package/dist/harness.js +17 -11
- package/dist/manifest.js +5 -0
- package/dist/mm-harness-cli.js +34 -20
- package/dist/paths.js +2 -2
- package/dist/recipe-security.js +12 -3
- package/dist/review/knowledge.js +1 -1
- package/dist/run-diagnostics.js +150 -33
- package/dist/runner.js +1 -1
- package/dist/runtime-context.js +3 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/library/actions/extension/platform/cdp.mjs +35 -10
- package/library/actions/shared/console-allowlist-add.mjs +137 -0
- package/library/actions/shared/console-collector.mjs +230 -0
- package/library/actions/shared/console-findings.mjs +511 -0
- package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
- package/library/actions/terminal/app/launch.mjs +45 -0
- package/library/actions/terminal/perps/_venue.mjs +242 -0
- package/library/actions/terminal/perps/assert_orders.mjs +54 -0
- package/library/actions/terminal/perps/assert_positions.mjs +40 -0
- package/library/actions/terminal/perps/read_orders.mjs +26 -0
- package/library/actions/terminal/perps/read_positions.mjs +25 -0
- package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
- package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
- package/library/actions/terminal/perps/teardown_state.mjs +104 -0
- package/library/actions/terminal/platform/hud.mjs +71 -0
- package/library/actions/terminal/platform/page.mjs +247 -0
- package/library/actions/terminal/platform/runtime.mjs +200 -0
- package/library/actions/terminal/ui/navigate.mjs +31 -0
- package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
- package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
- package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
- package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
- package/library/manifests/terminal.action-manifest.json +1786 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.71.0 - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `terminal` adapter for the MetaMask Web Terminal (Next.js site): `mm-harness launch|doctor|verify|install|cleanup|stop|run --adapter terminal` drive a per-slot Chrome for Testing on `--cdp-port` against the slot dev server on `--watcher-port`. `signer=extension` loads a MetaMask build with the fixture account imported; `signer=injected` is a strict EIP-1193 test wallet that rejects typed data for another chain. A wallet host logs every wallet request (method, EIP-712 primary type, chain, outcome; never params or signatures) and every MetaMask confirmation shown.
|
|
10
|
+
- Terminal actions: `metamask.app.launch`, venue-backed `metamask.perps.read_positions|read_orders|read_snapshot|assert_positions|assert_orders`, testnet `metamask.perps.teardown_state` and `metamask.perps.revoke_agent` (signed in Node by the fixture account; mainnet only with `mainnet_confirmation=REAL_FUNDS`), `metamask.wallet.read_signatures|assert_signatures|list_accounts`, and `ui.*` with `surface: app|wallet` to drive the MetaMask side panel / notification.
|
|
11
|
+
- Terminal browser is visible by default, one window per slot side by side on the main screen (odd CDP port left, even right; `TERMINAL_WINDOW` / `TERMINAL_SCREEN` override) with a profile zoom that keeps the desktop layout. `--headless`, launch node `headless: true` or `TERMINAL_HEADLESS=1` hide it; `--headful` stays accepted. `--slow-mo <ms>` / `slow_mo_ms` / `TERMINAL_SLOW_MO` pause before each UI action. Terminal `app.hud` draws the recipe, step N/M, current intent and last result on the app tab (closed shadow root outside `<body>`, pointer-events none, redrawn after navigation, never on MetaMask pages); `--hud auto` shows it only in a visible browser.
|
|
12
|
+
- Terminal testnet enforcement: every mainnet host the venue SDK names, and the API and base URLs of `@metamask/perps-controller` and the app source, are unresolvable for the whole browser and blocked by the wallet host (`blocked-mainnet`); mainnet Hyperliquid typed data is refused before any signer sees it (`refused-mainnet`). The venues' web hosts (`app.hyperliquid.xyz`, `app.hyperliquid-testnet.xyz`) are neither blocked nor proof of the served network, and a launch fails if any scanned source uses one with an API path. Source URLs are compared in canonical form (parsed: lowercase host without a trailing dot, default port dropped, dot segments resolved), and the host resolver rules cover each host's trailing-dot form. The launch proves both blocks with probes and that the app talks to a testnet API host, and fails if the SDK names a mainnet host missing from the block list. Mainnet needs `network: mainnet` with `mainnet_confirmation: REAL_FUNDS`. A browser is reused only while it enforces the same host list.
|
|
13
|
+
- Terminal slot safety: slot processes carry `--mm-harness-owner` markers and only proven-owned ones are signalled; wallet calls are judged against their own document's commit, and anything that can't be (including calls pending when a tab detaches) is logged as `unattributed`; a failed launch stops what it started and removes temporary key material; a first page that never commits is reported with what the tab and the dev server did. Only `TERMINAL_CHROME_BIN` overrides the browser.
|
|
14
|
+
- Terminal focus: a visible browser starts in the background through LaunchServices with no startup window; one check after the launch (shared `macos-focus.cjs`, `restoreFrontmostIfOurs`) gives the front back once, by pid, only if this slot's browser took it, and the wallet host only observes. Pages come to front only with `MM_HARNESS_FOCUS_BROWSER=1`.
|
|
15
|
+
- Terminal run diagnostics: recipe runs start the shared console collector for the slot (`terminal/app-console.log` for the app page, `terminal/extension-console.log` for MetaMask; `mm-harness logs` lists both and the wallet request log), and the run's non-probe `blocked-mainnet`, `refused-mainnet` and `unattributed` wallet entries are findings. `metamask.app.assert_no_console_errors` fails a recipe on unexplained page, browser or extension errors.
|
|
16
|
+
- Console allowlists (`diagnostics/console-allowlist.json`, `schemaVersion: 3`, earlier versions refused): exact signatures of an event's first line, continuation and optional location, compared by string equality through one matcher, `library/actions/shared/console-findings.mjs`, which recipe-library guards also call. Normalisation only touches parsed fields: URLs (Next.js scope only on a plain path: every segment made of letters, digits, `.`, `_`, `@`, `-` and `%40`, never `.` or `..`) in a loopback port, a `/_next/static/` asset name (its directory and extension kept), `dpl`/`v` on `/_next/` paths and an unpacked extension id; frames and locations lose their position; other text only in times, UUIDs and hex of 9+ digits. `library/actions/shared/console-allowlist-add.mjs <run artifacts dir>` writes entries from a real run.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Console collector capture files are framed: every line after an event's first starts with `│ `, and a browser Log entry's `[browser:<source>]` tag sits in the record header before the level word, where message text can't put it; its location is its URL alone, without a line number. **Logs from older collectors:** each unprefixed line is a finding of its own (at least an error) and their browser entries read as page events, so they report REVIEW.
|
|
21
|
+
- Run diagnostics and `assert_no_console_errors` require capture evidence: this runtime's own collector (a process running this checkout's `console-tail.mjs` as its script, with its exact argument list, so install and runtime paths may contain spaces; owner marker; a marker-less collector for the same log is replaced once, and this runtime's collector started by another harness install is retired before its replacement starts; capture files rotate past 32 MB) and a control line logged in the captured target that reaches the capture file (for the Extension, from MetaMask's own extension id). **Extension consumers:** a run whose capture can't be proven at the end reports UNAVAILABLE instead of CLEAN, so the browser must still be open then; a capture over the 512 KB read limit with no findings is UNAVAILABLE too. Capture control lines are never findings.
|
|
22
|
+
|
|
23
|
+
## 0.70.1 - 2026-10-02
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Detect forced Google Chrome sign-in (`BrowserSignin: 2`, `ForceBrowserSignin`) as managed: `plutil` returns these scalar policies only as raw values, so a Mac with only forced sign-in looked unmanaged and fell back to the managed Chrome.
|
|
28
|
+
- Disable other user extensions in the slot browser before its start page (a dapp or the wallet home) loads; a browser started without a window gets a blank background window first.
|
|
29
|
+
- Fail the launch, and stop the browser, when a slot browser started without a window cannot open its start window, instead of recording a browser with no window.
|
|
30
|
+
- Start the library autolaunch browser on macOS without a startup window, load the wallet home into its window's blank tab (so it is the page a screenshot of the window shows) or a background window, and restore the operator's front app once, by pid, only if its own browser took it. Headful browser probes also start without a startup window.
|
|
31
|
+
- Show doctor's managed-Chrome diagnostics next to any other browser warning.
|
|
32
|
+
|
|
5
33
|
## 0.70.0 - 2026-10-02
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -149,15 +149,22 @@ On a cold cache the probe can take up to about 80 seconds and
|
|
|
149
149
|
shows a small (about 480×360) browser window; `doctor --adapter extension`
|
|
150
150
|
runs the same probe.
|
|
151
151
|
|
|
152
|
-
On a Mac whose Google Chrome is managed by policy
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
On a Mac whose Google Chrome is managed by policy, `auto` uses Chrome for
|
|
153
|
+
Testing, which does not read those policies, so no forced extension can open
|
|
154
|
+
windows in the slot. Only MDM-managed preferences are read
|
|
155
|
+
(`/Library/Managed Preferences/com.google.Chrome.plist` and its per-user
|
|
156
|
+
copy under `/Library/Managed Preferences/<user>/`), not `defaults` a user set
|
|
157
|
+
themselves; force-installed extensions (`ExtensionInstallForcelist`,
|
|
158
|
+
`ExtensionSettings`) or forced sign-in (`BrowserSignin: 2`,
|
|
159
|
+
`ForceBrowserSignin`) count as managed; a policy stored as a Data or Date
|
|
160
|
+
value is treated as absent. If Chrome for Testing does not start
|
|
161
|
+
and paint there, the launch stops with `BROWSER_UNLAUNCHABLE` rather than
|
|
162
|
+
falling back to the managed Chrome; the way out is
|
|
163
|
+
`RECIPE_HARNESS_BROWSER=chrome` (accepting that forced extensions may open
|
|
164
|
+
windows that take the front), or a less loaded machine. Doctor lists the forced
|
|
165
|
+
extension ids, with names where it can read them (best effort, read-only: the
|
|
166
|
+
slot record, or the operator's own Chrome profile folders). A slot profile
|
|
167
|
+
created on the managed Chrome keeps it, with a warning to reset the profile.
|
|
161
168
|
|
|
162
169
|
A slot then keeps the kind of browser its profile was created with, until the
|
|
163
170
|
profile is reset (`mm-harness fixtures set` or `fixtures reset`):
|
|
@@ -39,11 +39,13 @@ function parseArgs(argv) {
|
|
|
39
39
|
case '-h':
|
|
40
40
|
case '--help':
|
|
41
41
|
process.stdout.write(
|
|
42
|
-
'Usage: console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>]\n',
|
|
42
|
+
'Usage: console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>] [--mm-harness-owner=<marker>]\n',
|
|
43
43
|
);
|
|
44
44
|
process.exit(0);
|
|
45
45
|
break;
|
|
46
46
|
default:
|
|
47
|
+
// An ownership marker the caller identifies its collector by.
|
|
48
|
+
if (arg.startsWith('--mm-harness-owner=')) break;
|
|
47
49
|
throw new Error(`Unknown argument: ${arg}`);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -73,7 +75,13 @@ const cooldown = new Map();
|
|
|
73
75
|
// Writes formatted console-event strings (never raw HTTP response bodies) to
|
|
74
76
|
// stdout and optionally to a caller-supplied log file. The source is the local
|
|
75
77
|
// Chrome CDP WebSocket (127.0.0.1:<cdpPort>) — a trusted-local endpoint.
|
|
76
|
-
|
|
78
|
+
// Every line after an event's first is written with this prefix, so text
|
|
79
|
+
// inside a message can never read as the start of another event.
|
|
80
|
+
const CONTINUATION_PREFIX = '│ ';
|
|
81
|
+
|
|
82
|
+
function out(event, destination = null) {
|
|
83
|
+
const [first, ...rest] = String(event).split('\n');
|
|
84
|
+
const line = [first, ...rest.map((continued) => `${CONTINUATION_PREFIX}${continued}`)].join('\n');
|
|
77
85
|
process.stdout.write(`${line}\n`);
|
|
78
86
|
if (destination) {
|
|
79
87
|
// This file is explicitly an untrusted application-console transcript; it is never executed or parsed as configuration.
|
|
@@ -136,10 +144,23 @@ function previewArg(arg) {
|
|
|
136
144
|
return arg.subtype || arg.type || '?';
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
// Where an event came from, appended as " (at <frame>)": the first stack
|
|
148
|
+
// frame of a console call or exception, the URL of a browser log entry. Run
|
|
149
|
+
// diagnostics group findings by message and report this frame.
|
|
150
|
+
function frameText(callFrame) {
|
|
151
|
+
if (!callFrame?.url) return '';
|
|
152
|
+
return `${callFrame.functionName || '<anonymous>'} ${callFrame.url}:${(callFrame.lineNumber ?? 0) + 1}:${(callFrame.columnNumber ?? 0) + 1}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function atSuffix(frame) {
|
|
156
|
+
return frame ? ` (at ${frame})` : '';
|
|
157
|
+
}
|
|
158
|
+
|
|
139
159
|
function emitConsole(label, params, destination) {
|
|
140
160
|
const level = String(params.type || 'log').toUpperCase();
|
|
141
161
|
const text = (params.args || []).map(previewArg).join(' ');
|
|
142
|
-
|
|
162
|
+
const [first, ...rest] = text.split('\n');
|
|
163
|
+
out(`${stamp()} [${label}] ${level} ${first}${atSuffix(frameText(params.stackTrace?.callFrames?.[0]))}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
function emitException(label, params, destination) {
|
|
@@ -149,13 +170,23 @@ function emitException(label, params, destination) {
|
|
|
149
170
|
d.exception?.value ||
|
|
150
171
|
d.text ||
|
|
151
172
|
'uncaught exception';
|
|
152
|
-
|
|
173
|
+
const frame = frameText(d.stackTrace?.callFrames?.[0]) || frameText({ url: d.url, lineNumber: d.lineNumber, columnNumber: d.columnNumber });
|
|
174
|
+
const [first, ...rest] = String(text).split('\n');
|
|
175
|
+
out(`${stamp()} [${label}] EXCEPTION ${first}${atSuffix(frame)}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
// Browser log entries (network failures, interventions, deprecations) are
|
|
179
|
+
// tagged with their Log source in the record header, before the level word,
|
|
180
|
+
// e.g. "[dapp:page:BTC] [browser:network] ERROR …": message text (written
|
|
181
|
+
// after the level word) can never produce the tag.
|
|
155
182
|
function emitLogEntry(label, params, destination) {
|
|
156
183
|
const e = params.entry || {};
|
|
157
184
|
const level = String(e.level || 'log').toUpperCase();
|
|
158
|
-
|
|
185
|
+
const source = String(e.source || 'other').replace(/[^a-z-]/gu, '') || 'other';
|
|
186
|
+
// A Log entry's location is its URL as is: a line number appended to a URL
|
|
187
|
+
// could not be told apart from the URL's own text.
|
|
188
|
+
const where = e.url ? e.url : frameText(e.stackTrace?.callFrames?.[0]);
|
|
189
|
+
out(`${stamp()} [${label}] [browser:${source}] ${level} ${e.text || ''}${atSuffix(where)}`, destination);
|
|
159
190
|
}
|
|
160
191
|
|
|
161
192
|
function handleMessage(label, destination, data) {
|
|
@@ -185,7 +216,8 @@ function handleMessage(label, destination, data) {
|
|
|
185
216
|
function attach(target) {
|
|
186
217
|
const stream = targetStream(target);
|
|
187
218
|
if (!stream) return;
|
|
188
|
-
|
|
219
|
+
// No brackets or spaces: the label is a header field.
|
|
220
|
+
const label = `${stream.kind}:${shortLabel(target)}`.replace(/[[\]\s]/gu, '_');
|
|
189
221
|
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
190
222
|
const entry = { ws, label, destination: stream.log, opened: false };
|
|
191
223
|
attached.set(target.id, entry);
|
|
@@ -223,7 +223,7 @@ try {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
if (loadsOverCdp) cdpLoad = loadExtensionOverCdp(browserPid);
|
|
226
|
-
else if (application) openBackgroundWindow(initialUrl);
|
|
226
|
+
else if (application) openBackgroundWindow(browserPid, initialUrl);
|
|
227
227
|
owner = cdpLoad?.owner ?? ownerIdentity(browserPid);
|
|
228
228
|
for (const problem of pruneExtraHomeTabs(cdpPort, args['extension-dir'])) {
|
|
229
229
|
process.stderr.write(`[launch] home-tab pruning: ${problem}\n`);
|
|
@@ -283,14 +283,26 @@ try {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// The first window of a browser started without one, opened in the background.
|
|
286
|
-
|
|
286
|
+
// Without it the slot has no window at all, so a failure fails the launch:
|
|
287
|
+
// the owned browser is stopped (its markers kept if it cannot be).
|
|
288
|
+
function openBackgroundWindow(pid, url) {
|
|
287
289
|
const result = spawnSync(process.execPath, [browserResolverPath, 'open-window', '--port', String(cdpPort), '--url', url], {
|
|
288
290
|
encoding: 'utf8',
|
|
289
291
|
timeout: 30_000,
|
|
290
292
|
});
|
|
291
|
-
if (result.status
|
|
292
|
-
|
|
293
|
+
if (result.status === 0) return;
|
|
294
|
+
const openError = (result.stderr || result.error?.message || `exit ${result.status}`).trim();
|
|
295
|
+
try {
|
|
296
|
+
stopProfileProcessesSync(args.profile, { extraPids: [pid] });
|
|
297
|
+
} catch (stopError) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Chrome started but its start window could not be opened (${openError}), and stopping it failed: ${stopError.message}. ` +
|
|
300
|
+
`The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
|
|
301
|
+
);
|
|
293
302
|
}
|
|
303
|
+
clearDetachedLaunchUnproven(args.profile);
|
|
304
|
+
if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
|
|
305
|
+
throw new Error(`Chrome started but its start window could not be opened: ${openError}. Next: inspect ${args['chrome-log']}, then rerun.`);
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
// live.sh passes the record its resolver just wrote (--browser-resolution);
|
package/adapters/manifest.json
CHANGED
|
@@ -638,6 +638,134 @@
|
|
|
638
638
|
"purpose": "Bootstrap numbered MetaMask checkouts through the setup-base command.",
|
|
639
639
|
"inputs": "setup-base command arguments",
|
|
640
640
|
"outputs": "numbered checkouts, saved preferences, and run summary"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"id": "terminal/launch",
|
|
644
|
+
"entry": "adapters/terminal/launch.mjs",
|
|
645
|
+
"kind": "node",
|
|
646
|
+
"purpose": "Start or reuse the Web Terminal slot browser (Chrome for Testing unless TERMINAL_CHROME_BIN; MetaMask extension with the fixture imported, or the injected strict wallet) and its wallet host. On macOS the browser starts through LaunchServices with no startup window; in testnet mode the venue SDK's mainnet hosts are unresolvable for the whole browser.",
|
|
647
|
+
"inputs": "--target --cdp-port --app-port [--signer extension|injected] [--account] [--fresh-profile] [--headless|--headful] [--slow-mo] [--network mainnet --mainnet-confirmation REAL_FUNDS] [--json]; env TERMINAL_CHROME_BIN, TERMINAL_EXTENSION_DIR, TERMINAL_EXTENSION_CHECKOUT, RECIPE_WALLET_FIXTURE, TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN",
|
|
648
|
+
"outputs": "<runtime>/terminal/browser.json + pid/log files; JSON on stdout; exit 0/1/2"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"id": "terminal/wallet-host",
|
|
652
|
+
"entry": "adapters/terminal/wallet-host.mjs",
|
|
653
|
+
"kind": "node",
|
|
654
|
+
"purpose": "Long-running CDP observer: opens the app tab blank, hooks it once per target, navigates it to the app and verifies the log binding before ready; logs every wallet request from the app top frame and every MetaMask confirmation shown; answers the injected strict wallet in injected mode (other frames and blank popups get 4100).",
|
|
655
|
+
"inputs": "--cdp-port --runtime-dir --signer --app-origin [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
|
|
656
|
+
"outputs": "<runtime>/terminal/wallet-requests.jsonl, wallet-host.ready"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"id": "terminal/stop",
|
|
660
|
+
"entry": "adapters/terminal/stop.mjs",
|
|
661
|
+
"kind": "node",
|
|
662
|
+
"purpose": "Stop the terminal slot browser, wallet host and console collector and prove the CDP port is free.",
|
|
663
|
+
"inputs": "--target [--cdp-port]",
|
|
664
|
+
"outputs": "JSON on stdout; exit 0/1"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
"id": "terminal/inject",
|
|
668
|
+
"entry": "adapters/terminal/inject.mjs",
|
|
669
|
+
"kind": "node",
|
|
670
|
+
"purpose": "Terminal install: overlay marker, runtime dir and wallet fixture (0600); no product writes.",
|
|
671
|
+
"inputs": "--target [--fixture] [--force]; env RECIPE_WALLET_FIXTURE",
|
|
672
|
+
"outputs": "<harness>/terminal/install.json, <runtime>/wallet-fixture.json; exit 0/1"
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
"id": "terminal/cleanup",
|
|
676
|
+
"entry": "adapters/terminal/cleanup.mjs",
|
|
677
|
+
"kind": "node",
|
|
678
|
+
"purpose": "Terminal cleanup: stop browser/host, remove terminal runtime state, overlay marker and installed fixture; profiles kept unless --reset-profile.",
|
|
679
|
+
"inputs": "--target [--cdp-port] [--reset-profile]",
|
|
680
|
+
"outputs": "JSON on stdout; exit 0/1"
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
"id": "terminal/verify",
|
|
684
|
+
"entry": "adapters/terminal/verify.mjs",
|
|
685
|
+
"kind": "node",
|
|
686
|
+
"purpose": "Read-only terminal slot readiness: checkout, deps, dev server, forced testnet, fixture, browser start probe, extension copy, CDP port.",
|
|
687
|
+
"inputs": "--target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
|
|
688
|
+
"outputs": "readiness report; exit 0 ready / 1 not ready"
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"id": "terminal/lib-cdp-client",
|
|
692
|
+
"entry": "adapters/terminal/lib/cdp-client.mjs",
|
|
693
|
+
"kind": "lib",
|
|
694
|
+
"purpose": "Browser-level CDP client with flattened target sessions for the wallet host.",
|
|
695
|
+
"inputs": "CdpClient.connect(url)",
|
|
696
|
+
"outputs": "send/on"
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
"id": "terminal/lib-page-script",
|
|
700
|
+
"entry": "adapters/terminal/lib/page-script.mjs",
|
|
701
|
+
"kind": "lib",
|
|
702
|
+
"purpose": "Page script source: wallet request logging and the injected strict provider (EIP-6963 io.metamask).",
|
|
703
|
+
"inputs": "pageScriptSource({ signer, appOrigin })",
|
|
704
|
+
"outputs": "script source string"
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
"id": "terminal/lib-strict-wallet",
|
|
708
|
+
"entry": "adapters/terminal/lib/strict-wallet.mjs",
|
|
709
|
+
"kind": "lib",
|
|
710
|
+
"purpose": "EIP-1193 strict test wallet enforcing typed-data chainId == active chain.",
|
|
711
|
+
"inputs": "createStrictWallet({ account, chainId })",
|
|
712
|
+
"outputs": "request()"
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"id": "terminal/lib-processes",
|
|
716
|
+
"entry": "adapters/terminal/lib/processes.mjs",
|
|
717
|
+
"kind": "lib",
|
|
718
|
+
"purpose": "Pid-file process stop with owner-marker checks (--mm-harness-owner=<kind>-<id>, exact token; live foreign pids kept and reported, dead ones dropped), process-group termination, CDP port release check.",
|
|
719
|
+
"inputs": "stopTerminalBrowser(target, { cdpPort }), stopConsoleCollector(target), ownerMarker(kind, runtimeDir), ownsProcess(kind, pid, runtimeDir)",
|
|
720
|
+
"outputs": "stopped processes"
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
"id": "terminal/lib-readiness",
|
|
724
|
+
"entry": "adapters/terminal/lib/readiness.mjs",
|
|
725
|
+
"kind": "lib",
|
|
726
|
+
"purpose": "Terminal slot readiness checks shared by verify, doctor and the surface.",
|
|
727
|
+
"inputs": "terminalReadiness({ target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort }) (advisory)",
|
|
728
|
+
"outputs": "checks[]"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"id": "terminal/lib-display",
|
|
732
|
+
"entry": "adapters/terminal/lib/display.mjs",
|
|
733
|
+
"kind": "lib",
|
|
734
|
+
"purpose": "Headless/headful and slow-mo resolution, per-slot window placement and the profile zoom that keeps the desktop layout.",
|
|
735
|
+
"inputs": "resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom",
|
|
736
|
+
"outputs": "browser display args"
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
"id": "terminal/lib-hud",
|
|
740
|
+
"entry": "adapters/terminal/lib/hud.mjs",
|
|
741
|
+
"kind": "lib",
|
|
742
|
+
"purpose": "Recipe HUD state (<runtime>/hud.json) and the page expression that draws it on the app origin only.",
|
|
743
|
+
"inputs": "nextHudState, hudRenderExpression",
|
|
744
|
+
"outputs": "HUD state, expression source"
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
"id": "terminal/lib-origin",
|
|
748
|
+
"entry": "adapters/terminal/lib/origin.mjs",
|
|
749
|
+
"kind": "lib",
|
|
750
|
+
"purpose": "Exact app-origin and app top-frame checks for targets, wallet requests and log entries.",
|
|
751
|
+
"inputs": "isAppUrl(url, appOrigin), isAppTopFrameContext(context, { targetId, appOrigin })",
|
|
752
|
+
"outputs": "boolean"
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
"id": "terminal/lib-mainnet-guard",
|
|
756
|
+
"entry": "adapters/terminal/lib/mainnet-guard.mjs",
|
|
757
|
+
"kind": "lib",
|
|
758
|
+
"purpose": "Testnet enforcement: the venue endpoint hosts (from the venue SDK, @metamask/perps-controller and the app source in the checkout), the browser's host-resolver block, and the mainnet typed-data classifier used by the page script and the wallet host.",
|
|
759
|
+
"inputs": "venueHosts(checkout), hostResolverRules(hosts), isMainnetVenueUrl(url, hosts), mainnetTypedDataReason(data)",
|
|
760
|
+
"outputs": "host list, launch switch, refusal reason"
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
"id": "terminal/surface",
|
|
764
|
+
"entry": "src/adapters/terminal/surface.ts",
|
|
765
|
+
"kind": "module",
|
|
766
|
+
"purpose": "Terminal adapter surface (runtime status, browser stop, logs, hints).",
|
|
767
|
+
"inputs": "terminalSurface",
|
|
768
|
+
"outputs": "AdapterSurface"
|
|
641
769
|
}
|
|
642
770
|
]
|
|
643
771
|
}
|
|
@@ -428,12 +428,21 @@ async function openBackgroundWindow(send, url, timeoutMs) {
|
|
|
428
428
|
return send('Target.createTarget', { url, newWindow: true, background: true }, undefined, timeoutMs);
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
const isBlankPage = (target) => target.type === 'page' && (target.url === 'about:blank' || target.url.startsWith('chrome://newtab') || target.url.startsWith('chrome://new-tab-page'));
|
|
432
|
+
|
|
433
|
+
// A browser started without a window gets a blank background window, so the
|
|
434
|
+
// first window never takes the operator's focus and nothing is navigated yet.
|
|
435
|
+
async function ensureBackgroundWindow(send, remaining) {
|
|
436
|
+
const { targetInfos } = await send('Target.getTargets', {}, undefined, remaining());
|
|
437
|
+
if ((targetInfos || []).some((target) => target.type === 'page')) return;
|
|
438
|
+
await openBackgroundWindow(send, 'about:blank', remaining());
|
|
439
|
+
}
|
|
440
|
+
|
|
431
441
|
// Point the launch tab at `url` once the extension exists: the tab opened at
|
|
432
|
-
// launch would otherwise show an error page for chrome-extension:// URLs.
|
|
433
|
-
// browser started without a window gets a background window instead.
|
|
442
|
+
// launch would otherwise show an error page for chrome-extension:// URLs.
|
|
434
443
|
async function openUrl(send, url, remaining) {
|
|
435
444
|
const { targetInfos } = await send('Target.getTargets', {}, undefined, remaining());
|
|
436
|
-
const blank = targetInfos
|
|
445
|
+
const blank = (targetInfos || []).find(isBlankPage);
|
|
437
446
|
if (!blank) {
|
|
438
447
|
await openBackgroundWindow(send, url, remaining());
|
|
439
448
|
return;
|
|
@@ -489,12 +498,15 @@ async function loadUnpackedOverPort(port, extensionDir, {
|
|
|
489
498
|
assertSameOwner(owner, { exec, timeoutMs: remaining() });
|
|
490
499
|
step = 'Extensions.loadUnpacked';
|
|
491
500
|
const loaded = await loadUnpackedExtension(client.send, extensionDir, { expectedId, timeoutMs: remaining() });
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
501
|
+
// A browser started without a window gets a blank background window
|
|
502
|
+
// first; other extensions are disabled before the start page (a dapp, or
|
|
503
|
+
// the wallet home) loads in it.
|
|
504
|
+
step = 'opening a background window';
|
|
505
|
+
await ensureBackgroundWindow(client.send, remaining);
|
|
496
506
|
step = 'isolating the wallet from other extensions';
|
|
497
507
|
const otherExtensions = await isolateWalletExtension(client.send, loaded.id, { timeoutMs: remaining() });
|
|
508
|
+
step = 'opening the start page';
|
|
509
|
+
if (url) await openUrl(client.send, url, remaining);
|
|
498
510
|
remaining();
|
|
499
511
|
return { ...loaded, otherExtensions, owner };
|
|
500
512
|
} catch (error) {
|
|
@@ -523,6 +535,8 @@ module.exports = {
|
|
|
523
535
|
expectedExtensionId: (extensionDir) => extensionIdFromExtensionDir(extensionDir) || null,
|
|
524
536
|
loadUnpackedExtension,
|
|
525
537
|
loadUnpackedOverPort,
|
|
538
|
+
ensureBackgroundWindow,
|
|
526
539
|
isolateWalletExtension,
|
|
527
540
|
openBackgroundWindow,
|
|
541
|
+
openUrl,
|
|
528
542
|
};
|
|
@@ -106,14 +106,24 @@ function managedChromePlists(env = process.env) {
|
|
|
106
106
|
];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// One policy key
|
|
109
|
+
// One policy key, or undefined when absent or unreadable. `plutil -extract
|
|
110
|
+
// … json` only extracts arrays and dictionaries; a scalar (BrowserSignin: 2,
|
|
111
|
+
// ForceBrowserSignin: true) comes out with `raw`.
|
|
110
112
|
function readPolicyKey(file, key) {
|
|
113
|
+
const extract = (format) => execFileSync('plutil', ['-extract', key, format, '-o', '-', file], {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
116
|
+
timeout: 5000,
|
|
117
|
+
});
|
|
111
118
|
try {
|
|
112
|
-
return JSON.parse(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
return JSON.parse(extract('json'));
|
|
120
|
+
} catch {
|
|
121
|
+
// A scalar, or the key is absent: try it as a raw value.
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const raw = extract('raw').trim();
|
|
125
|
+
if (raw === 'true' || raw === 'false') return raw === 'true';
|
|
126
|
+
return /^-?\d+$/u.test(raw) ? Number(raw) : raw;
|
|
117
127
|
} catch {
|
|
118
128
|
return undefined;
|
|
119
129
|
}
|
|
@@ -512,11 +522,12 @@ async function probeLaunch(executable, {
|
|
|
512
522
|
let exited = null;
|
|
513
523
|
try {
|
|
514
524
|
const port = await freePort();
|
|
515
|
-
// A headful
|
|
516
|
-
// background with no startup window (Chrome
|
|
517
|
-
// one); renderCheck opens a background
|
|
518
|
-
// a window
|
|
519
|
-
|
|
525
|
+
// A headful probe starts like the slot browsers (the launchers and the
|
|
526
|
+
// library autolaunch): in the background with no startup window (Chrome
|
|
527
|
+
// activates itself when it opens one); renderCheck opens a background
|
|
528
|
+
// window. Like the launchers, it keeps a window behind the operator's apps
|
|
529
|
+
// rendering.
|
|
530
|
+
const backgroundWindow = display === HEADFUL;
|
|
520
531
|
const args = [
|
|
521
532
|
...(display === HEADLESS ? ['--headless=new'] : ['--window-size=480,360']),
|
|
522
533
|
`--user-data-dir=${profile}`,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cleanup.mjs — `mm-harness cleanup --adapter terminal`: stop the slot browser
|
|
3
|
+
// and wallet host, then remove the terminal runtime state and the installed
|
|
4
|
+
// wallet fixture. Browser profiles are kept unless --reset-profile.
|
|
5
|
+
//
|
|
6
|
+
// Inputs: --target <checkout> [--cdp-port <port>] [--reset-profile]
|
|
7
|
+
// Outputs: JSON summary on stdout. Exit 0 cleaned; 1 the CDP port stayed busy.
|
|
8
|
+
// Never touches: product source files, the Next.js dev server.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { recipeHarnessPath, walletFixturePath } from '../../library/actions/harness-exports.mjs';
|
|
15
|
+
import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
|
|
16
|
+
import { stopConsoleCollector, stopTerminalBrowser } from './lib/processes.mjs';
|
|
17
|
+
|
|
18
|
+
const usage = 'Usage: cleanup.mjs --target <checkout> [--cdp-port <port>] [--reset-profile]';
|
|
19
|
+
|
|
20
|
+
export async function cleanupTerminalRuntime({ target, cdpPort, resetProfile = false }) {
|
|
21
|
+
const root = path.resolve(target);
|
|
22
|
+
const stop = await stopTerminalBrowser(root, { cdpPort });
|
|
23
|
+
await stopConsoleCollector(root);
|
|
24
|
+
const runtime = terminalRuntimeDir(root);
|
|
25
|
+
const removed = [];
|
|
26
|
+
if (existsSync(runtime)) {
|
|
27
|
+
for (const entry of readdirSync(runtime)) {
|
|
28
|
+
if (entry.startsWith('profile-') && !resetProfile) continue;
|
|
29
|
+
rmSync(path.join(runtime, entry), { recursive: true, force: true });
|
|
30
|
+
removed.push(entry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const overlay = recipeHarnessPath(root, 'terminal');
|
|
34
|
+
if (existsSync(overlay)) {
|
|
35
|
+
rmSync(overlay, { recursive: true, force: true });
|
|
36
|
+
removed.push(path.relative(root, overlay));
|
|
37
|
+
}
|
|
38
|
+
const fixture = walletFixturePath(root);
|
|
39
|
+
if (existsSync(fixture)) {
|
|
40
|
+
rmSync(fixture, { force: true });
|
|
41
|
+
removed.push(path.relative(root, fixture));
|
|
42
|
+
}
|
|
43
|
+
return { status: 'pass', target: root, stopped: stop.stopped, cdpPortFree: stop.cdpPortFree, removed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
49
|
+
process.stdout.write(`${usage}\n`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
const value = (flag) => {
|
|
53
|
+
const index = argv.indexOf(flag);
|
|
54
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
55
|
+
};
|
|
56
|
+
const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
57
|
+
try {
|
|
58
|
+
const result = await cleanupTerminalRuntime({
|
|
59
|
+
target: value('--target') ?? process.cwd(),
|
|
60
|
+
cdpPort: rawPort ? Number(rawPort) : undefined,
|
|
61
|
+
resetProfile: argv.includes('--reset-profile'),
|
|
62
|
+
});
|
|
63
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
process.stdout.write(`${JSON.stringify({ status: 'fail', error: error.message })}\n`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// inject.mjs — `mm-harness install --adapter terminal`: prepare the slot runtime
|
|
3
|
+
// directory and install the wallet fixture the terminal actions read.
|
|
4
|
+
//
|
|
5
|
+
// Inputs: --target <terminal checkout> [--fixture <wallet-fixture.json>] [--force]
|
|
6
|
+
// env RECIPE_WALLET_FIXTURE (fixture source when --fixture is absent)
|
|
7
|
+
// Outputs: <target>/<harness root>/terminal/install.json (overlay marker),
|
|
8
|
+
// <target>/<runtime>/terminal/ (0700), <runtime>/wallet-fixture.json (0600);
|
|
9
|
+
// JSON summary on stdout (account names only). Exit 0 installed; 1 failure; 2 usage.
|
|
10
|
+
// Never touches: product source files; never prints key material.
|
|
11
|
+
|
|
12
|
+
import { copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
import { recipeHarnessPath, recipeRuntimeDir, walletFixturePath } from '../../library/actions/harness-exports.mjs';
|
|
17
|
+
import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
|
|
18
|
+
import { isTerminalCheckout } from './lib/readiness.mjs';
|
|
19
|
+
|
|
20
|
+
const usage = 'Usage: inject.mjs --target <terminal checkout> [--fixture <wallet-fixture.json>] [--force]';
|
|
21
|
+
|
|
22
|
+
export function installTerminalRuntime({ target, fixture, force = false }) {
|
|
23
|
+
const root = path.resolve(target);
|
|
24
|
+
if (!isTerminalCheckout(root)) throw new Error(`${root} is not a Web Terminal checkout (needs next + src/features/perpetuals).`);
|
|
25
|
+
mkdirSync(path.join(root, recipeRuntimeDir()), { recursive: true, mode: 0o700 });
|
|
26
|
+
mkdirSync(terminalRuntimeDir(root), { recursive: true, mode: 0o700 });
|
|
27
|
+
const destination = walletFixturePath(root);
|
|
28
|
+
let fixtureAction = 'kept';
|
|
29
|
+
if (fixture && (force || !existsSync(destination))) {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(fixture, 'utf8'));
|
|
31
|
+
if (!Array.isArray(parsed.accounts) || parsed.accounts.length === 0) throw new Error(`${fixture} has no accounts.`);
|
|
32
|
+
copyFileSync(fixture, destination);
|
|
33
|
+
chmodSync(destination, 0o600);
|
|
34
|
+
fixtureAction = 'installed';
|
|
35
|
+
} else if (!existsSync(destination)) {
|
|
36
|
+
fixtureAction = 'missing';
|
|
37
|
+
}
|
|
38
|
+
const accounts = existsSync(destination)
|
|
39
|
+
? JSON.parse(readFileSync(destination, 'utf8')).accounts.map((account) => account?.name).filter(Boolean)
|
|
40
|
+
: [];
|
|
41
|
+
const marker = { schemaVersion: 1, adapter: 'terminal', installedAt: new Date().toISOString(), fixture: fixtureAction };
|
|
42
|
+
// The overlay directory is the install marker run/call auto-ensure checks.
|
|
43
|
+
const overlay = recipeHarnessPath(root, 'terminal');
|
|
44
|
+
mkdirSync(overlay, { recursive: true, mode: 0o700 });
|
|
45
|
+
writeFileSync(path.join(overlay, 'install.json'), `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
|
|
46
|
+
return { status: fixtureAction === 'missing' ? 'fail' : 'pass', target: root, fixture: fixtureAction, fixturePath: destination, accounts };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
50
|
+
const argv = process.argv.slice(2);
|
|
51
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
52
|
+
process.stdout.write(`${usage}\n`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const value = (flag) => {
|
|
56
|
+
const index = argv.indexOf(flag);
|
|
57
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const result = installTerminalRuntime({
|
|
61
|
+
target: value('--target') ?? process.cwd(),
|
|
62
|
+
fixture: value('--fixture') ?? process.env.RECIPE_WALLET_FIXTURE,
|
|
63
|
+
force: argv.includes('--force'),
|
|
64
|
+
});
|
|
65
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
66
|
+
if (result.status !== 'pass') {
|
|
67
|
+
process.stderr.write('No wallet fixture installed. Next: set RECIPE_WALLET_FIXTURE or pass --fixture <wallet-fixture.json>.\n');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
process.stderr.write(`${error.message}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|