@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,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// doctor.mjs — orchestration feature-surface doctor
|
|
3
|
+
//
|
|
4
|
+
// Purpose:
|
|
5
|
+
// Verifies orchestration/manifest.json against reality: every entry point
|
|
6
|
+
// exists; every script entry answers --help with exit 0; and every script
|
|
7
|
+
// under orchestration/{mobile,extension,core} is listed (no doc drift).
|
|
8
|
+
//
|
|
9
|
+
// Inputs (flags): --json (machine-readable report)
|
|
10
|
+
// Outputs: per-feature lines (or JSON report) on stdout.
|
|
11
|
+
// Exit 0 — surface healthy; 1 — missing entry, failing --help, or
|
|
12
|
+
// unlisted script; 2 — bad args.
|
|
13
|
+
// Never touches: anything (read-only + --help subprocesses).
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
let json = false;
|
|
20
|
+
for (const arg of process.argv.slice(2)) {
|
|
21
|
+
if (arg === '--json') json = true;
|
|
22
|
+
else if (arg === '-h' || arg === '--help') {
|
|
23
|
+
console.log('Usage: doctor.mjs [--json]');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
} else {
|
|
26
|
+
console.error(`Unknown arg: ${arg}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const orchestrationDir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const runnerRoot = path.resolve(orchestrationDir, '..');
|
|
33
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(orchestrationDir, 'manifest.json'), 'utf8'));
|
|
34
|
+
|
|
35
|
+
const results = [];
|
|
36
|
+
for (const feature of manifest.features) {
|
|
37
|
+
const entryAbs = path.join(runnerRoot, feature.entry);
|
|
38
|
+
const checks = { exists: fs.existsSync(entryAbs), help: null };
|
|
39
|
+
if (checks.exists && (feature.kind === 'bash' || feature.kind === 'node')) {
|
|
40
|
+
const runner = feature.kind === 'bash' ? 'bash' : process.execPath;
|
|
41
|
+
const run = spawnSync(runner, [entryAbs, '--help'], { encoding: 'utf8', timeout: 30000 });
|
|
42
|
+
checks.help = run.status === 0;
|
|
43
|
+
}
|
|
44
|
+
const ok = checks.exists && (checks.help === null || checks.help === true);
|
|
45
|
+
results.push({ id: feature.id, entry: feature.entry, kind: feature.kind, ...checks, ok });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// drift guard: every script in the orchestration trees must be listed
|
|
49
|
+
const listed = new Set(manifest.features.map((feature) => feature.entry));
|
|
50
|
+
const unlisted = [];
|
|
51
|
+
for (const platform of ['mobile', 'extension', 'core', 'lib']) {
|
|
52
|
+
const dir = path.join(orchestrationDir, platform);
|
|
53
|
+
if (!fs.existsSync(dir)) continue;
|
|
54
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
55
|
+
const rel = `orchestration/${platform}/${entry}`;
|
|
56
|
+
if (!/\.(?:sh|mjs|cjs|json)$/u.test(entry)) continue;
|
|
57
|
+
if (!listed.has(rel)) unlisted.push(rel);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const failed = results.filter((result) => !result.ok);
|
|
62
|
+
const status = failed.length === 0 && unlisted.length === 0 ? 'pass' : 'fail';
|
|
63
|
+
if (json) {
|
|
64
|
+
console.log(JSON.stringify({ status, features: results, unlisted }, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
for (const result of results) {
|
|
67
|
+
console.log(`${result.ok ? 'ok ' : 'FAIL'} ${result.id} (${result.entry})${result.help === false ? ' [--help failed]' : ''}${result.exists ? '' : ' [missing]'}`);
|
|
68
|
+
}
|
|
69
|
+
for (const rel of unlisted) console.log(`FAIL unlisted script not in manifest: ${rel}`);
|
|
70
|
+
console.log(`orchestration surface: ${status} (${results.length - failed.length}/${results.length} features, ${unlisted.length} unlisted)`);
|
|
71
|
+
}
|
|
72
|
+
process.exit(status === 'pass' ? 0 : 1);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cleanup.mjs — remove the extension harness overlay
|
|
3
|
+
//
|
|
4
|
+
// Purpose:
|
|
5
|
+
// Removes exactly the .git/info/exclude entries the install recorded,
|
|
6
|
+
// then deletes the target's extension harness dir. Overlay-only: no
|
|
7
|
+
// product files are involved.
|
|
8
|
+
//
|
|
9
|
+
// Inputs (flags / env):
|
|
10
|
+
// --target <metamask-extension> (default $PWD); env RECIPE_HARNESS_ROOT
|
|
11
|
+
//
|
|
12
|
+
// Outputs:
|
|
13
|
+
// Removes <harness>/extension; confirmation line on stdout.
|
|
14
|
+
// Exit 0 — cleaned (idempotent if nothing installed); 2 — bad args.
|
|
15
|
+
//
|
|
16
|
+
// Never touches: product files; .git/info/exclude lines it did not record.
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import process from 'node:process';
|
|
21
|
+
import { recipeHarnessRoot } from '../lib/recipe-paths.mjs';
|
|
22
|
+
|
|
23
|
+
function usage() { console.error('Usage: cleanup-extension-harness.mjs [--target <metamask-extension>]'); }
|
|
24
|
+
let target = process.cwd();
|
|
25
|
+
for (let i = 2; i < process.argv.length; i += 1) {
|
|
26
|
+
const arg = process.argv[i];
|
|
27
|
+
if (arg === '--target') {
|
|
28
|
+
if (i + 1 >= process.argv.length) { usage(); process.exit(2); }
|
|
29
|
+
target = process.argv[++i];
|
|
30
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
31
|
+
usage(); process.exit(0);
|
|
32
|
+
} else {
|
|
33
|
+
console.error(`cleanup-extension-harness: unknown arg: ${arg}`);
|
|
34
|
+
usage(); process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
target = path.resolve(target);
|
|
39
|
+
const harnessRoot = harnessRootValue();
|
|
40
|
+
const harnessDir = path.join(target, harnessRoot, 'extension');
|
|
41
|
+
removeRecordedGitExcludeEntries(target, path.join(harnessDir, 'added-git-exclude'));
|
|
42
|
+
fs.rmSync(harnessDir, { recursive: true, force: true });
|
|
43
|
+
console.log(`Cleaned extension recipe harness from ${target}`);
|
|
44
|
+
|
|
45
|
+
function harnessRootValue() {
|
|
46
|
+
return recipeHarnessRoot();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeRecordedGitExcludeEntries(repo, recordFile) {
|
|
50
|
+
if (!fs.existsSync(recordFile)) return;
|
|
51
|
+
const gitDirResult = spawnSync('git', ['-C', repo, 'rev-parse', '--git-dir'], { encoding: 'utf8' });
|
|
52
|
+
if (gitDirResult.status !== 0) return;
|
|
53
|
+
let gitDir = gitDirResult.stdout.trim();
|
|
54
|
+
if (!path.isAbsolute(gitDir)) gitDir = path.join(repo, gitDir);
|
|
55
|
+
const excludeFile = path.join(gitDir, 'info/exclude');
|
|
56
|
+
if (!fs.existsSync(excludeFile)) return;
|
|
57
|
+
const remove = new Set(fs.readFileSync(recordFile, 'utf8').split('\n').filter(Boolean));
|
|
58
|
+
const kept = fs.readFileSync(excludeFile, 'utf8').split('\n').filter((line) => !remove.has(line));
|
|
59
|
+
fs.writeFileSync(excludeFile, `${kept.join('\n').replace(/\n+$/u, '')}\n`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// console-tail.mjs — live stream of the running extension's console.
|
|
3
|
+
//
|
|
4
|
+
// Connects to a Chrome CDP endpoint and tails the console output of every
|
|
5
|
+
// extension target — the MV3 service worker AND extension UI pages
|
|
6
|
+
// (chrome-extension:// pages) — printing each console call / runtime
|
|
7
|
+
// exception / log entry as a single line. Intended to host a tmux window so
|
|
8
|
+
// the tab shows live extension logs (the equivalent of the Metro/webpack tabs).
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// console-tail.mjs --cdp-port <port> [--log <file>]
|
|
12
|
+
//
|
|
13
|
+
// Resilience: MV3 service workers cycle on idle and UI pages open/close, so we
|
|
14
|
+
// re-poll /json/list every ~2s and (re)attach to any extension target that is
|
|
15
|
+
// not currently connected. A dropped websocket simply frees the target for the
|
|
16
|
+
// next poll to re-attach (printed as a [reconnect] note).
|
|
17
|
+
//
|
|
18
|
+
// Requires Node 22+ (global WebSocket).
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
import { appendFileSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = { cdpPort: null, log: null };
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
switch (arg) {
|
|
28
|
+
case '--cdp-port':
|
|
29
|
+
args.cdpPort = argv[++i];
|
|
30
|
+
break;
|
|
31
|
+
case '--log':
|
|
32
|
+
args.log = argv[++i];
|
|
33
|
+
break;
|
|
34
|
+
case '-h':
|
|
35
|
+
case '--help':
|
|
36
|
+
process.stdout.write(
|
|
37
|
+
'Usage: console-tail.mjs --cdp-port <port> [--log <file>]\n',
|
|
38
|
+
);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const args = parseArgs(process.argv.slice(2));
|
|
49
|
+
const cdpPort = Number(args.cdpPort);
|
|
50
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
51
|
+
throw new Error('console-tail requires a numeric --cdp-port.');
|
|
52
|
+
}
|
|
53
|
+
const HOST = `127.0.0.1:${cdpPort}`;
|
|
54
|
+
|
|
55
|
+
if (typeof WebSocket === 'undefined') {
|
|
56
|
+
throw new Error('console-tail requires Node 22+ with a global WebSocket.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// targetId -> { ws, label, opened }
|
|
60
|
+
const attached = new Map();
|
|
61
|
+
// targetId -> epoch ms until which re-attach is skipped (targets that errored
|
|
62
|
+
// before ever connecting, so a dead/non-debuggable target is not retried every poll).
|
|
63
|
+
const cooldown = new Map();
|
|
64
|
+
|
|
65
|
+
function out(line) {
|
|
66
|
+
process.stdout.write(`${line}\n`);
|
|
67
|
+
if (args.log) appendFileSync(args.log, `${line}\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stamp() {
|
|
71
|
+
const d = new Date();
|
|
72
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
73
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shortLabel(target) {
|
|
77
|
+
if (target.type === 'service_worker') return 'sw';
|
|
78
|
+
let tail = target.url || '';
|
|
79
|
+
try {
|
|
80
|
+
const u = new URL(target.url);
|
|
81
|
+
tail = u.pathname.split('/').filter(Boolean).pop() || u.host;
|
|
82
|
+
} catch {
|
|
83
|
+
// Non-URL target url (rare); fall back to the raw string below.
|
|
84
|
+
tail = (target.url || target.type || 'page').slice(-24);
|
|
85
|
+
}
|
|
86
|
+
return `page:${tail}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isExtensionTarget(target) {
|
|
90
|
+
if (!target || !target.webSocketDebuggerUrl) return false;
|
|
91
|
+
if (target.type !== 'service_worker' && target.type !== 'page') return false;
|
|
92
|
+
return String(target.url || '').startsWith('chrome-extension://');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function previewArg(arg) {
|
|
96
|
+
if (!arg || typeof arg !== 'object') return String(arg);
|
|
97
|
+
if ('value' in arg && arg.value !== undefined) {
|
|
98
|
+
return typeof arg.value === 'object'
|
|
99
|
+
? JSON.stringify(arg.value)
|
|
100
|
+
: String(arg.value);
|
|
101
|
+
}
|
|
102
|
+
if (arg.unserializableValue) return String(arg.unserializableValue);
|
|
103
|
+
if (arg.description) return arg.description;
|
|
104
|
+
if (arg.preview?.description) return arg.preview.description;
|
|
105
|
+
return arg.subtype || arg.type || '?';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function emitConsole(label, params) {
|
|
109
|
+
const level = String(params.type || 'log').toUpperCase();
|
|
110
|
+
const text = (params.args || []).map(previewArg).join(' ');
|
|
111
|
+
out(`${stamp()} [${label}] ${level} ${text}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emitException(label, params) {
|
|
115
|
+
const d = params.exceptionDetails || {};
|
|
116
|
+
const text =
|
|
117
|
+
d.exception?.description ||
|
|
118
|
+
d.exception?.value ||
|
|
119
|
+
d.text ||
|
|
120
|
+
'uncaught exception';
|
|
121
|
+
out(`${stamp()} [${label}] EXCEPTION ${text}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function emitLogEntry(label, params) {
|
|
125
|
+
const e = params.entry || {};
|
|
126
|
+
const level = String(e.level || 'log').toUpperCase();
|
|
127
|
+
out(`${stamp()} [${label}] ${level} ${e.text || ''}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function handleMessage(label, data) {
|
|
131
|
+
let message;
|
|
132
|
+
try {
|
|
133
|
+
message = JSON.parse(data);
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-JSON frame; nothing actionable to stream.
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (message.id != null) return; // command ack, not an event
|
|
139
|
+
switch (message.method) {
|
|
140
|
+
case 'Runtime.consoleAPICalled':
|
|
141
|
+
emitConsole(label, message.params || {});
|
|
142
|
+
break;
|
|
143
|
+
case 'Runtime.exceptionThrown':
|
|
144
|
+
emitException(label, message.params || {});
|
|
145
|
+
break;
|
|
146
|
+
case 'Log.entryAdded':
|
|
147
|
+
emitLogEntry(label, message.params || {});
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function attach(target) {
|
|
155
|
+
const label = shortLabel(target);
|
|
156
|
+
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
157
|
+
const entry = { ws, label, opened: false };
|
|
158
|
+
attached.set(target.id, entry);
|
|
159
|
+
|
|
160
|
+
ws.addEventListener('open', () => {
|
|
161
|
+
entry.opened = true;
|
|
162
|
+
let id = 0;
|
|
163
|
+
for (const method of ['Runtime.enable', 'Log.enable']) {
|
|
164
|
+
ws.send(JSON.stringify({ id: ++id, method, params: {} }));
|
|
165
|
+
}
|
|
166
|
+
out(`${stamp()} [${label}] [attached]`);
|
|
167
|
+
});
|
|
168
|
+
ws.addEventListener('message', (event) => {
|
|
169
|
+
handleMessage(label, typeof event.data === 'string' ? event.data : '');
|
|
170
|
+
});
|
|
171
|
+
const drop = () => {
|
|
172
|
+
if (attached.get(target.id) !== entry) return;
|
|
173
|
+
attached.delete(target.id);
|
|
174
|
+
if (entry.opened) {
|
|
175
|
+
out(`${stamp()} [${label}] [reconnect] target detached; will re-attach`);
|
|
176
|
+
} else {
|
|
177
|
+
// Never connected (target gone / not debuggable yet): back off so a dead
|
|
178
|
+
// target is not retried — and logged — on every 2s poll.
|
|
179
|
+
cooldown.set(target.id, Date.now() + 10000);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
ws.addEventListener('close', drop);
|
|
183
|
+
ws.addEventListener('error', drop);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function poll() {
|
|
187
|
+
let targets;
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetch(`http://${HOST}/json/list`);
|
|
190
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
191
|
+
targets = await response.json();
|
|
192
|
+
} catch {
|
|
193
|
+
// CDP not reachable this tick (browser starting/restarting); retry next poll.
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const target of Array.isArray(targets) ? targets : []) {
|
|
197
|
+
if (!isExtensionTarget(target)) continue;
|
|
198
|
+
if (attached.has(target.id)) continue;
|
|
199
|
+
const until = cooldown.get(target.id);
|
|
200
|
+
if (until !== undefined) {
|
|
201
|
+
if (Date.now() < until) continue;
|
|
202
|
+
cooldown.delete(target.id);
|
|
203
|
+
}
|
|
204
|
+
attach(target);
|
|
205
|
+
}
|
|
206
|
+
// Drop expired cooldown entries so the map cannot grow unbounded across a
|
|
207
|
+
// long session of service-worker / page churn.
|
|
208
|
+
for (const [id, until] of cooldown) {
|
|
209
|
+
if (Date.now() >= until) cooldown.delete(id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function shutdown() {
|
|
214
|
+
for (const { ws } of attached.values()) {
|
|
215
|
+
try {
|
|
216
|
+
ws.close();
|
|
217
|
+
} catch {
|
|
218
|
+
// Best-effort close on exit.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
process.on('SIGINT', shutdown);
|
|
224
|
+
process.on('SIGTERM', shutdown);
|
|
225
|
+
|
|
226
|
+
out(`${stamp()} [console-tail] streaming extension console via cdp ${cdpPort}`);
|
|
227
|
+
await poll();
|
|
228
|
+
setInterval(poll, 2000);
|