@crouton-kit/capture 1.3.2 → 1.3.4

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/bin/capture CHANGED
@@ -32,50 +32,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  mod
33
33
  ));
34
34
 
35
- // src/har-manager.ts
36
- function harFilePath(id) {
37
- return path.join(HAR_DIR, `${id}.json`);
38
- }
39
- function createHarRecording() {
40
- const id = Math.random().toString(36).slice(2, 8);
41
- const harPath = harFilePath(id);
42
- fs.mkdirSync(HAR_DIR, { recursive: true });
43
- fs.writeFileSync(harPath, JSON.stringify({ log: { entries: [] } }, null, 2));
44
- return { id, path: harPath };
45
- }
46
- function readHarRecording(id) {
47
- try {
48
- return JSON.parse(fs.readFileSync(harFilePath(id), "utf-8"));
49
- } catch {
50
- return null;
51
- }
52
- }
53
- function appendToHarRecording(id, entries) {
54
- const harPath = harFilePath(id);
55
- let har;
56
- try {
57
- har = JSON.parse(fs.readFileSync(harPath, "utf-8"));
58
- } catch {
59
- har = { log: { entries: [] } };
60
- }
61
- har.log.entries.push(...entries);
62
- fs.writeFileSync(harPath, JSON.stringify(har, null, 2));
63
- }
64
- function deleteHarRecording(id) {
65
- try {
66
- fs.unlinkSync(harFilePath(id));
67
- } catch {
68
- }
69
- }
70
- var fs, path, HAR_DIR;
71
- var init_har_manager = __esm({
72
- "src/har-manager.ts"() {
73
- fs = __toESM(require("fs"));
74
- path = __toESM(require("path"));
75
- HAR_DIR = "/tmp/capture-har";
76
- }
77
- });
78
-
79
35
  // node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js
80
36
  var require_constants = __commonJS({
81
37
  "node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js"(exports2, module2) {
@@ -3708,377 +3664,418 @@ var init_wrapper = __esm({
3708
3664
  }
3709
3665
  });
3710
3666
 
3711
- // src/session-context.ts
3712
- function getActiveSession() {
3667
+ // src/cdp/client.ts
3668
+ var CDPClient;
3669
+ var init_client = __esm({
3670
+ "src/cdp/client.ts"() {
3671
+ init_wrapper();
3672
+ CDPClient = class {
3673
+ ws;
3674
+ messageId = 0;
3675
+ pendingRequests = /* @__PURE__ */ new Map();
3676
+ pendingTimeouts = /* @__PURE__ */ new Map();
3677
+ eventHandlers = /* @__PURE__ */ new Map();
3678
+ ready;
3679
+ constructor(wsUrl, connectTimeout = 5e3) {
3680
+ this.ws = new wrapper_default(wsUrl);
3681
+ this.ready = new Promise((resolve3, reject) => {
3682
+ const timeout = setTimeout(() => {
3683
+ reject(new Error(`WebSocket connection timeout (${connectTimeout}ms)`));
3684
+ }, connectTimeout);
3685
+ this.ws.on("open", () => {
3686
+ clearTimeout(timeout);
3687
+ resolve3();
3688
+ });
3689
+ this.ws.on("error", (err) => {
3690
+ clearTimeout(timeout);
3691
+ reject(err);
3692
+ });
3693
+ });
3694
+ this.ws.on("message", (data) => {
3695
+ const msg = JSON.parse(data.toString());
3696
+ if (msg.id !== void 0) {
3697
+ const pending = this.pendingRequests.get(msg.id);
3698
+ if (pending) {
3699
+ this.pendingRequests.delete(msg.id);
3700
+ const timer = this.pendingTimeouts.get(msg.id);
3701
+ if (timer) {
3702
+ clearTimeout(timer);
3703
+ this.pendingTimeouts.delete(msg.id);
3704
+ }
3705
+ if (msg.error) {
3706
+ pending.reject(new Error(msg.error.message));
3707
+ } else {
3708
+ pending.resolve(msg.result);
3709
+ }
3710
+ }
3711
+ } else if (msg.method) {
3712
+ const handlers = this.eventHandlers.get(msg.method);
3713
+ handlers?.forEach((h) => h(msg.params));
3714
+ }
3715
+ });
3716
+ }
3717
+ async waitReady() {
3718
+ await this.ready;
3719
+ }
3720
+ async send(method, params = {}, timeout = 6e4) {
3721
+ await this.ready;
3722
+ const id = ++this.messageId;
3723
+ return new Promise((resolve3, reject) => {
3724
+ this.pendingRequests.set(id, { resolve: resolve3, reject });
3725
+ this.ws.send(JSON.stringify({ id, method, params }));
3726
+ const timer = setTimeout(() => {
3727
+ if (this.pendingRequests.has(id)) {
3728
+ this.pendingRequests.delete(id);
3729
+ this.pendingTimeouts.delete(id);
3730
+ reject(new Error(`CDP request timeout (${timeout}ms): ${method}`));
3731
+ }
3732
+ }, timeout);
3733
+ this.pendingTimeouts.set(id, timer);
3734
+ });
3735
+ }
3736
+ on(event, handler) {
3737
+ if (!this.eventHandlers.has(event)) {
3738
+ this.eventHandlers.set(event, []);
3739
+ }
3740
+ this.eventHandlers.get(event).push(handler);
3741
+ }
3742
+ close() {
3743
+ for (const timer of this.pendingTimeouts.values()) {
3744
+ clearTimeout(timer);
3745
+ }
3746
+ this.pendingTimeouts.clear();
3747
+ this.ws.close();
3748
+ }
3749
+ };
3750
+ }
3751
+ });
3752
+
3753
+ // src/cdp/detect.ts
3754
+ function getDefaultBrowserId() {
3713
3755
  try {
3714
- if (!fs2.existsSync(ACTIVE_PATH)) return null;
3715
- const state = JSON.parse(fs2.readFileSync(ACTIVE_PATH, "utf-8"));
3716
- if (!fs2.existsSync(state.dir)) {
3717
- clearActiveSession();
3718
- return null;
3719
- }
3720
- return state;
3756
+ const output = (0, import_child_process.execSync)(
3757
+ "defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers 2>/dev/null",
3758
+ { encoding: "utf-8" }
3759
+ );
3760
+ const match = output.match(
3761
+ /LSHandlerRoleAll = "([^"]+)";\s*LSHandlerURLScheme = https;/
3762
+ );
3763
+ return match?.[1] ?? null;
3721
3764
  } catch {
3722
3765
  return null;
3723
3766
  }
3724
3767
  }
3725
- function setActiveSession(state) {
3726
- fs2.mkdirSync(CAPTURE_ROOT, { recursive: true });
3727
- fs2.writeFileSync(ACTIVE_PATH, JSON.stringify(state, null, 2));
3728
- }
3729
- function clearActiveSession() {
3768
+ async function probeCdpPort(port) {
3730
3769
  try {
3731
- fs2.unlinkSync(ACTIVE_PATH);
3770
+ const controller = new AbortController();
3771
+ const timeout = setTimeout(() => controller.abort(), 500);
3772
+ const resp = await fetch(`http://localhost:${port}/json/version`, {
3773
+ signal: controller.signal
3774
+ });
3775
+ clearTimeout(timeout);
3776
+ if (resp.ok) {
3777
+ const version = await resp.json();
3778
+ const browser = version.Browser;
3779
+ const userAgent = version["User-Agent"] ?? "";
3780
+ if (!browser) return null;
3781
+ const isElectron = userAgent.includes("Electron");
3782
+ let app = browser;
3783
+ if (isElectron) {
3784
+ const appMatch = userAgent.match(
3785
+ /(\S+?)\/[\d.]+ Chrome\/[\d.]+ Electron/
3786
+ );
3787
+ if (appMatch) app = appMatch[1];
3788
+ }
3789
+ return { browser, app, isElectron };
3790
+ }
3732
3791
  } catch {
3733
3792
  }
3793
+ return null;
3734
3794
  }
3735
- function updateActiveSession(patch) {
3736
- const current = getActiveSession();
3737
- if (!current) return null;
3738
- const updated = { ...current, ...patch };
3739
- setActiveSession(updated);
3740
- return updated;
3795
+ function getLocalhostListeningPorts() {
3796
+ try {
3797
+ const output = (0, import_child_process.execSync)(
3798
+ 'netstat -an 2>/dev/null | grep "127.0.0.1.*LISTEN"',
3799
+ { encoding: "utf-8" }
3800
+ );
3801
+ const ports = [];
3802
+ for (const line of output.split("\n")) {
3803
+ const match = line.match(/127\.0\.0\.1\.(\d+)/);
3804
+ if (match) {
3805
+ ports.push(parseInt(match[1], 10));
3806
+ }
3807
+ }
3808
+ return ports;
3809
+ } catch {
3810
+ return [];
3811
+ }
3741
3812
  }
3742
- function sanitizeLabel(label) {
3743
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3813
+ async function detectCdpPortsAsync() {
3814
+ const ports = getLocalhostListeningPorts();
3815
+ const results = [];
3816
+ const probes = ports.map(async (port) => {
3817
+ const probe = await probeCdpPort(port);
3818
+ if (probe) {
3819
+ const lower = probe.browser.toLowerCase();
3820
+ let bundleId = "unknown";
3821
+ for (const [bid, patterns] of Object.entries(BROWSER_PATTERNS)) {
3822
+ if (patterns.some((p) => lower.includes(p))) {
3823
+ bundleId = bid;
3824
+ break;
3825
+ }
3826
+ }
3827
+ results.push({
3828
+ port,
3829
+ app: probe.app,
3830
+ bundleId,
3831
+ isElectron: probe.isElectron
3832
+ });
3833
+ }
3834
+ });
3835
+ await Promise.all(probes);
3836
+ return results;
3744
3837
  }
3745
- function nextStepPath(action, label) {
3746
- const session = getActiveSession();
3747
- if (!session) return null;
3748
- const step = session.stepCount + 1;
3749
- const nn = String(step).padStart(2, "0");
3750
- const sanitized = sanitizeLabel(label);
3751
- const filename = `${nn}-${action}-${sanitized}.png`;
3752
- const shotPath = path2.join(session.dir, "shots", filename);
3753
- updateActiveSession({ stepCount: step });
3754
- return shotPath;
3838
+ async function detectCdpPort() {
3839
+ const endpoints = await detectCdpPortsAsync();
3840
+ if (endpoints.length === 0) {
3841
+ throw new Error(
3842
+ "No browser with CDP found. Start your browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
3843
+ );
3844
+ }
3845
+ const defaultBrowser = getDefaultBrowserId();
3846
+ if (defaultBrowser) {
3847
+ const match = endpoints.find(
3848
+ (p) => p.bundleId === defaultBrowser && !p.isElectron
3849
+ );
3850
+ if (match) return match.port;
3851
+ }
3852
+ const nonElectron = endpoints.find((p) => !p.isElectron);
3853
+ return (nonElectron ?? endpoints[0]).port;
3755
3854
  }
3756
- var fs2, path2, os, CAPTURE_ROOT, ACTIVE_PATH;
3757
- var init_session_context = __esm({
3758
- "src/session-context.ts"() {
3759
- fs2 = __toESM(require("fs"));
3760
- path2 = __toESM(require("path"));
3761
- os = __toESM(require("os"));
3762
- CAPTURE_ROOT = path2.join(os.tmpdir(), "capture-sessions");
3763
- ACTIVE_PATH = path2.join(CAPTURE_ROOT, ".active");
3855
+ var import_child_process, BROWSER_PATTERNS;
3856
+ var init_detect = __esm({
3857
+ "src/cdp/detect.ts"() {
3858
+ import_child_process = require("child_process");
3859
+ BROWSER_PATTERNS = {
3860
+ "company.thebrowser.browser": ["arc"],
3861
+ "com.google.chrome": ["chrome", "google chrome"],
3862
+ "com.brave.browser": ["brave"],
3863
+ "com.microsoft.edgemac": ["edge", "microsoft edge"],
3864
+ "org.chromium.chromium": ["chromium"]
3865
+ };
3764
3866
  }
3765
3867
  });
3766
3868
 
3767
- // src/interact.ts
3768
- async function clickByName(client, name, role) {
3769
- await client.send("Accessibility.enable");
3770
- await client.send("DOM.enable");
3771
- const { nodes } = await client.send("Accessibility.getFullAXTree");
3772
- await client.send("Accessibility.disable");
3773
- const nameLower = name.toLowerCase();
3774
- const matches = nodes.filter((n) => {
3775
- if (!n.backendDOMNodeId) return false;
3776
- if (!n.name?.value) return false;
3777
- if (n.name.value.toLowerCase() !== nameLower) return false;
3778
- if (role && n.role?.value !== role) return false;
3779
- return true;
3780
- });
3781
- if (matches.length === 0) {
3869
+ // src/cdp/targets.ts
3870
+ async function getBrowserClient(port) {
3871
+ const versionResp = await fetch(`http://localhost:${port}/json/version`);
3872
+ if (!versionResp.ok) {
3782
3873
  throw new Error(
3783
- `No element found with accessible name "${name}"${role ? ` and role "${role}"` : ""}. Run \`capture a11y --interactive\` to see available elements.`
3874
+ `Failed to connect to CDP on port ${port}: ${versionResp.status}`
3784
3875
  );
3785
3876
  }
3786
- if (matches.length > 1 && !role) {
3787
- const INPUT_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "textarea", "spinbutton"]);
3788
- const inputMatches = matches.filter((n) => n.role?.value && INPUT_ROLES.has(n.role.value));
3789
- if (inputMatches.length === 1) {
3790
- matches.length = 0;
3791
- matches.push(inputMatches[0]);
3792
- } else {
3793
- const summary = matches.slice(0, 5).map((n) => ` ${n.role?.value} "${n.name?.value}"`).join("\n");
3877
+ const version = await versionResp.json();
3878
+ const client = new CDPClient(version.webSocketDebuggerUrl);
3879
+ await client.waitReady();
3880
+ return { client, browserWsUrl: version.webSocketDebuggerUrl };
3881
+ }
3882
+ async function listTargets(port) {
3883
+ const { client } = await getBrowserClient(port);
3884
+ try {
3885
+ await client.send("Target.setDiscoverTargets", { discover: true });
3886
+ const result = await client.send("Target.getTargets");
3887
+ return result.targetInfos.map((t) => ({
3888
+ id: t.targetId,
3889
+ title: t.title,
3890
+ url: t.url,
3891
+ type: t.type,
3892
+ webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${t.targetId}`
3893
+ }));
3894
+ } finally {
3895
+ client.close();
3896
+ }
3897
+ }
3898
+ async function findTab(port, urlPattern) {
3899
+ const targets = await listTargets(port);
3900
+ const pages = targets.filter((t) => t.type === "page");
3901
+ if (!urlPattern) {
3902
+ return pages[0] ?? null;
3903
+ }
3904
+ const pattern = urlPattern.toLowerCase();
3905
+ return pages.find((t) => t.url.toLowerCase().includes(pattern)) ?? null;
3906
+ }
3907
+ async function findTabById(port, targetId) {
3908
+ const targets = await listTargets(port);
3909
+ const exact = targets.find((t) => t.id === targetId);
3910
+ if (exact) return exact;
3911
+ if (targetId.length >= 4) {
3912
+ const prefix = targetId.toUpperCase();
3913
+ const matches = targets.filter((t) => t.id.toUpperCase().startsWith(prefix));
3914
+ if (matches.length === 1) return matches[0];
3915
+ if (matches.length > 1) {
3794
3916
  throw new Error(
3795
- `Multiple elements match "${name}" \u2014 disambiguate with --role:
3796
- ${summary}`
3917
+ `Ambiguous target prefix "${targetId}" matches ${matches.length} tabs:
3918
+ ` + matches.map((t) => ` ${t.id.slice(0, 8)} ${t.url}`).join("\n")
3797
3919
  );
3798
3920
  }
3799
3921
  }
3800
- const match = matches[0];
3801
- const backendNodeId = match.backendDOMNodeId;
3802
- await client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
3803
- const { model } = await client.send("DOM.getBoxModel", { backendNodeId });
3804
- const quad = model.content;
3805
- const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
3806
- const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
3807
- await client.send("Input.dispatchMouseEvent", {
3808
- type: "mousePressed",
3809
- x,
3810
- y,
3811
- button: "left",
3812
- clickCount: 1
3813
- });
3814
- await client.send("Input.dispatchMouseEvent", {
3815
- type: "mouseReleased",
3816
- x,
3817
- y,
3818
- button: "left",
3819
- clickCount: 1
3820
- });
3821
- return {
3822
- x: Math.round(x),
3823
- y: Math.round(y),
3824
- role: match.role?.value ?? "unknown",
3825
- name: match.name?.value ?? name
3826
- };
3827
- }
3828
- async function typeText(client, text) {
3829
- await client.send("Input.insertText", { text });
3922
+ return null;
3830
3923
  }
3831
- async function focusAndType(client, fieldName, text, role) {
3832
- const result = await clickByName(client, fieldName, role);
3833
- await new Promise((r) => setTimeout(r, 100));
3834
- await typeText(client, text);
3835
- return result;
3924
+ async function openTab(port, url) {
3925
+ const { client } = await getBrowserClient(port);
3926
+ try {
3927
+ const result = await client.send("Target.createTarget", {
3928
+ url,
3929
+ background: true
3930
+ });
3931
+ return {
3932
+ id: result.targetId,
3933
+ title: "",
3934
+ url,
3935
+ type: "page",
3936
+ webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${result.targetId}`
3937
+ };
3938
+ } finally {
3939
+ client.close();
3940
+ }
3836
3941
  }
3837
- var init_interact = __esm({
3838
- "src/interact.ts"() {
3942
+ var init_targets = __esm({
3943
+ "src/cdp/targets.ts"() {
3944
+ init_client();
3839
3945
  }
3840
3946
  });
3841
3947
 
3842
- // src/cdp.ts
3843
- var cdp_exports = {};
3844
- __export(cdp_exports, {
3845
- CDPClient: () => CDPClient,
3846
- ConsoleRecorder: () => ConsoleRecorder,
3847
- HARRecorder: () => HARRecorder,
3848
- acquireTabLock: () => acquireTabLock,
3849
- captureScreenshot: () => captureScreenshot,
3850
- cdpMain: () => main,
3851
- detectCdpPort: () => detectCdpPort,
3852
- executeInBrowser: () => executeInBrowser,
3853
- findTab: () => findTab,
3854
- findTabById: () => findTabById,
3855
- getAccessibilityTree: () => getAccessibilityTree,
3856
- isTabLocked: () => isTabLocked,
3857
- listTargets: () => listTargets,
3858
- navigateAndRecord: () => navigateAndRecord,
3859
- navigateAndWait: () => navigateAndWait,
3860
- openTab: () => openTab,
3861
- recordTraffic: () => recordTraffic,
3862
- releaseTabLock: () => releaseTabLock,
3863
- withTabLock: () => withTabLock
3864
- });
3865
- function shouldRecordRequest(url) {
3866
- if (SKIP_EXTENSIONS.test(url)) return false;
3867
- if (SKIP_DOMAINS.test(url)) return false;
3868
- return true;
3869
- }
3870
- function printConsoleSummary(entries) {
3871
- const errors = entries.filter((e) => e.level === "error");
3872
- const warnings = entries.filter((e) => e.level === "warning");
3873
- if (errors.length > 0 || warnings.length > 0) {
3874
- console.error(
3875
- `
3876
- Console: ${errors.length} errors, ${warnings.length} warnings`
3877
- );
3878
- for (const entry of [...errors, ...warnings]) {
3879
- const prefix = entry.level === "error" ? "ERR" : "WARN";
3880
- const src = entry.source ? ` (${entry.source})` : "";
3881
- const text = entry.text.length > 200 ? entry.text.slice(0, 197) + "..." : entry.text;
3882
- console.error(` [${prefix}] ${text}${src}`);
3948
+ // src/session-context.ts
3949
+ function getActiveSession() {
3950
+ try {
3951
+ if (!fs.existsSync(ACTIVE_PATH)) return null;
3952
+ const state = JSON.parse(fs.readFileSync(ACTIVE_PATH, "utf-8"));
3953
+ if (!fs.existsSync(state.dir)) {
3954
+ clearActiveSession();
3955
+ return null;
3883
3956
  }
3957
+ return state;
3958
+ } catch {
3959
+ return null;
3884
3960
  }
3885
3961
  }
3886
- function writeHarAndPrintSummary(har, harOutPath) {
3887
- if (har.log.entries.length === 0 && !harOutPath) return void 0;
3888
- const harPath = harOutPath ?? `/tmp/capture-har-${Date.now()}.json`;
3962
+ function setActiveSession(state) {
3963
+ fs.mkdirSync(CAPTURE_ROOT, { recursive: true });
3964
+ fs.writeFileSync(ACTIVE_PATH, JSON.stringify(state, null, 2));
3965
+ }
3966
+ function clearActiveSession() {
3889
3967
  try {
3890
- fs3.writeFileSync(harPath, JSON.stringify(har, null, 2));
3891
- } catch (err) {
3892
- const stripped = {
3893
- log: {
3894
- ...har.log,
3895
- entries: har.log.entries.map((e) => ({
3896
- ...e,
3897
- response: { ...e.response, content: { text: "[body stripped \u2014 too large to serialize]" } }
3898
- }))
3899
- }
3900
- };
3901
- fs3.writeFileSync(harPath, JSON.stringify(stripped, null, 2));
3902
- console.error(" WARNING: Response bodies stripped from HAR (too large to serialize)");
3903
- }
3904
- const errors = har.log.entries.filter((e) => e.response.status >= 400);
3905
- console.error(
3906
- `
3907
- HAR: ${har.log.entries.length} requests captured \u2192 ${harPath}`
3908
- );
3909
- if (errors.length > 0) {
3910
- console.error(` ${errors.length} failed:`);
3911
- for (const entry of errors) {
3912
- const method = entry.request.method.padEnd(6);
3913
- const status = entry.response.status;
3914
- const url = entry.request.url.length > 100 ? entry.request.url.slice(0, 97) + "..." : entry.request.url;
3915
- console.error(` ${method} ${status} ${url}`);
3916
- }
3968
+ fs.unlinkSync(ACTIVE_PATH);
3969
+ } catch {
3917
3970
  }
3918
- return harPath;
3919
3971
  }
3920
- function parseCliArgs(argv) {
3921
- const command = argv[0] ?? "";
3922
- const positional = [];
3923
- const parsed = { command, positional };
3924
- for (let i = 1; i < argv.length; i++) {
3925
- const arg = argv[i];
3926
- const next = argv[i + 1];
3927
- if (arg === "--port" && next) {
3928
- parsed.port = parseInt(next, 10);
3929
- i++;
3930
- } else if (arg === "--out" && next) {
3931
- parsed.out = next;
3932
- i++;
3933
- } else if (arg === "--json") {
3934
- parsed.json = true;
3935
- } else if (arg === "--interactive") {
3936
- parsed.interactive = true;
3937
- } else if (arg === "--har-out" && next) {
3938
- parsed.harOut = next;
3939
- i++;
3940
- } else if (arg === "--record") {
3941
- parsed.record = true;
3942
- } else if (arg === "--duration" && next) {
3943
- parsed.duration = parseInt(next, 10);
3944
- i++;
3945
- } else if (arg === "--settle" && next) {
3946
- parsed.settle = parseInt(next, 10);
3947
- i++;
3948
- } else if (arg === "--file" && next) {
3949
- parsed.file = next;
3950
- i++;
3951
- } else if (arg === "--nested") {
3952
- parsed.nested = true;
3953
- } else if (arg === "--har" && next) {
3954
- parsed.har = next;
3955
- i++;
3956
- } else if (arg === "--new") {
3957
- parsed.new = true;
3958
- } else if (arg === "--target" && next) {
3959
- parsed.target = next;
3960
- i++;
3961
- } else if (arg === "--role" && next) {
3962
- parsed.role = next;
3963
- i++;
3964
- } else if (arg === "--into" && next) {
3965
- parsed.into = next;
3966
- i++;
3967
- } else if (arg === "--no-screenshot") {
3968
- parsed.noScreenshot = true;
3969
- } else if (arg === "--viewport" && next) {
3970
- parsed.viewport = next;
3971
- i++;
3972
- } else if (arg === "--full-page") {
3973
- parsed.fullPage = true;
3974
- } else if (arg === "--height" && next) {
3975
- parsed.height = parseInt(next, 10);
3976
- i++;
3977
- } else if (arg === "--filter-url" && next) {
3978
- parsed.filterUrl = next;
3979
- i++;
3980
- } else if (arg === "--filter-status" && next) {
3981
- parsed.filterStatus = next;
3982
- i++;
3983
- } else if (arg === "--filter-method" && next) {
3984
- parsed.filterMethod = next.toUpperCase();
3985
- i++;
3986
- } else if (arg === "--limit" && next) {
3987
- parsed.limit = parseInt(next, 10);
3988
- i++;
3989
- } else if (arg === "--help" || arg === "-h") {
3990
- parsed.help = true;
3991
- } else if (arg.startsWith("--")) {
3992
- console.error(`Unknown flag: ${arg}`);
3993
- process.exit(1);
3994
- } else {
3995
- positional.push(arg);
3996
- }
3997
- }
3998
- if (!parsed.target && process.env.CDP_TARGET) {
3999
- parsed.target = process.env.CDP_TARGET;
4000
- }
4001
- if (!parsed.har && process.env.CDP_HAR_ID) {
4002
- parsed.har = process.env.CDP_HAR_ID;
4003
- }
3972
+ function updateActiveSession(patch) {
3973
+ const current = getActiveSession();
3974
+ if (!current) return null;
3975
+ const updated = { ...current, ...patch };
3976
+ setActiveSession(updated);
3977
+ return updated;
3978
+ }
3979
+ function sanitizeLabel(label) {
3980
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3981
+ }
3982
+ function nextStepPath(action, label) {
4004
3983
  const session = getActiveSession();
4005
- if (session) {
4006
- if (!parsed.har && session.harId) parsed.har = session.harId;
4007
- if (!parsed.target && session.targetId) parsed.target = session.targetId;
4008
- }
4009
- return parsed;
3984
+ if (!session) return null;
3985
+ const step = session.stepCount + 1;
3986
+ const nn = String(step).padStart(2, "0");
3987
+ const sanitized = sanitizeLabel(label);
3988
+ const filename = `${nn}-${action}-${sanitized}.png`;
3989
+ const shotPath = path.join(session.dir, "shots", filename);
3990
+ updateActiveSession({ stepCount: step });
3991
+ return shotPath;
4010
3992
  }
4011
- async function connectForCommand(parsed) {
4012
- if (!parsed.target) {
4013
- throw new Error('Use --target <tabId> to target a tab. Run "capture list" to see available tabs.');
3993
+ var fs, path, os, CAPTURE_ROOT, ACTIVE_PATH;
3994
+ var init_session_context = __esm({
3995
+ "src/session-context.ts"() {
3996
+ fs = __toESM(require("fs"));
3997
+ path = __toESM(require("path"));
3998
+ os = __toESM(require("os"));
3999
+ CAPTURE_ROOT = path.join(os.tmpdir(), "capture-sessions");
4000
+ ACTIVE_PATH = path.join(CAPTURE_ROOT, ".active");
4014
4001
  }
4015
- let tab = null;
4016
- if (parsed.port) {
4017
- tab = await findTabById(parsed.port, parsed.target);
4018
- } else {
4019
- const endpoints = await detectCdpPortsAsync();
4020
- for (const ep of endpoints) {
4021
- try {
4022
- tab = await findTabById(ep.port, parsed.target);
4023
- if (tab) break;
4024
- } catch {
4025
- }
4026
- }
4002
+ });
4003
+
4004
+ // src/cdp/screenshot.ts
4005
+ async function captureScreenshot(client, viewport, options) {
4006
+ const MAX_DIM = 1600;
4007
+ if (viewport) {
4008
+ await client.send("Emulation.setDeviceMetricsOverride", {
4009
+ width: viewport.width,
4010
+ height: viewport.height,
4011
+ deviceScaleFactor: 1,
4012
+ mobile: false
4013
+ });
4014
+ await new Promise((r) => setTimeout(r, 150));
4027
4015
  }
4028
- if (!tab) {
4029
- const query = parsed.target;
4030
- throw new Error(
4031
- `No tab found for target "${query}". Run "capture list" to see available tabs.`
4032
- );
4016
+ if (options?.fullPage) {
4017
+ const layoutMetrics = await client.send("Page.getLayoutMetrics");
4018
+ const contentWidth = layoutMetrics.cssVisualViewport?.clientWidth ?? viewport?.width ?? 1280;
4019
+ const contentHeight = layoutMetrics.contentSize?.height ?? viewport?.height ?? 800;
4020
+ await client.send("Emulation.setDeviceMetricsOverride", {
4021
+ width: contentWidth,
4022
+ height: Math.ceil(contentHeight),
4023
+ deviceScaleFactor: 1,
4024
+ mobile: false
4025
+ });
4026
+ await new Promise((r) => setTimeout(r, 150));
4033
4027
  }
4034
- if (!tab.webSocketDebuggerUrl) {
4035
- throw new Error("Tab has no WebSocket debugger URL");
4028
+ let screenshotOpts = {
4029
+ format: "png",
4030
+ captureBeyondViewport: false
4031
+ };
4032
+ try {
4033
+ const metrics = await client.send("Page.getLayoutMetrics");
4034
+ const vw = metrics.cssVisualViewport?.clientWidth ?? 0;
4035
+ const vh = metrics.cssVisualViewport?.clientHeight ?? 0;
4036
+ const sx = metrics.cssVisualViewport?.pageX ?? 0;
4037
+ const sy = metrics.cssVisualViewport?.pageY ?? 0;
4038
+ const dprResult = await client.send("Runtime.evaluate", {
4039
+ expression: "window.devicePixelRatio",
4040
+ returnByValue: true
4041
+ });
4042
+ const dpr = dprResult.result.value ?? 1;
4043
+ const actualMaxSide = Math.max(vw, vh) * dpr;
4044
+ const scale = actualMaxSide > MAX_DIM ? MAX_DIM / actualMaxSide : 1 / dpr;
4045
+ screenshotOpts = {
4046
+ ...screenshotOpts,
4047
+ clip: { x: sx, y: sy, width: vw, height: vh, scale }
4048
+ };
4049
+ } catch {
4036
4050
  }
4037
- const activeSession = getActiveSession();
4038
- if (activeSession && !activeSession.targetId) {
4039
- updateActiveSession({ targetId: tab.id });
4051
+ const result = await client.send(
4052
+ "Page.captureScreenshot",
4053
+ screenshotOpts
4054
+ );
4055
+ if (viewport || options?.fullPage) {
4056
+ await client.send("Emulation.clearDeviceMetricsOverride");
4040
4057
  }
4041
- const client = new CDPClient(tab.webSocketDebuggerUrl);
4042
- await client.waitReady();
4043
- return { client, tab };
4058
+ return Buffer.from(result.data, "base64");
4044
4059
  }
4045
- async function withConnection(parsed, fn, opts = {}) {
4046
- const { client, tab } = await connectForCommand(parsed);
4047
- const consoleRecorder = new ConsoleRecorder(client);
4048
- await consoleRecorder.start();
4049
- let harRecorder;
4050
- if (parsed.har) {
4051
- const harPath = harFilePath(parsed.har);
4052
- if (!fs3.existsSync(harPath)) {
4053
- console.error(
4054
- `ERROR: No HAR recording found for --har "${parsed.har}". Run 'har create' first.`
4055
- );
4056
- process.exit(1);
4057
- }
4058
- harRecorder = new HARRecorder(client);
4059
- await harRecorder.start();
4060
- }
4061
- try {
4062
- const result = await fn(client, tab);
4063
- if (harRecorder && parsed.har) {
4064
- const settle = opts.settle !== void 0 ? opts.settle : 3e3;
4065
- if (settle > 0) {
4066
- await new Promise((r) => setTimeout(r, settle));
4067
- }
4068
- const har = await harRecorder.finish();
4069
- if (har.log.entries.length > 0) {
4070
- appendToHarRecording(parsed.har, har.log.entries);
4071
- console.error(
4072
- ` [har:${parsed.har}] +${har.log.entries.length} entries`
4073
- );
4074
- }
4075
- }
4076
- printConsoleSummary(consoleRecorder.finish());
4077
- return result;
4078
- } finally {
4079
- client.close();
4080
- }
4060
+ async function autoScreenshot(client, action, label, noScreenshot) {
4061
+ if (noScreenshot) return null;
4062
+ const shotPath = nextStepPath(action, label);
4063
+ if (!shotPath) return null;
4064
+ await new Promise((r) => setTimeout(r, 300));
4065
+ const png = await captureScreenshot(client);
4066
+ fs2.writeFileSync(shotPath, png);
4067
+ console.error(` [screenshot] ${shotPath}`);
4068
+ return shotPath;
4081
4069
  }
4070
+ var fs2;
4071
+ var init_screenshot = __esm({
4072
+ "src/cdp/screenshot.ts"() {
4073
+ fs2 = __toESM(require("fs"));
4074
+ init_session_context();
4075
+ }
4076
+ });
4077
+
4078
+ // src/cdp/a11y.ts
4082
4079
  function flattenA11yTree(nodes, opts = {}, depth = 0) {
4083
4080
  const lines = [];
4084
4081
  for (const node of nodes) {
@@ -4097,263 +4094,251 @@ function flattenA11yTree(nodes, opts = {}, depth = 0) {
4097
4094
  }
4098
4095
  return lines;
4099
4096
  }
4100
- function getDefaultBrowserId() {
4101
- try {
4102
- const output = (0, import_child_process.execSync)(
4103
- "defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers 2>/dev/null",
4104
- { encoding: "utf-8" }
4105
- );
4106
- const match = output.match(
4107
- /LSHandlerRoleAll = "([^"]+)";\s*LSHandlerURLScheme = https;/
4108
- );
4109
- return match?.[1] ?? null;
4110
- } catch {
4111
- return null;
4097
+ async function getAccessibilityTree(client) {
4098
+ await client.send("Accessibility.enable");
4099
+ const result = await client.send("Accessibility.getFullAXTree");
4100
+ await client.send("Accessibility.disable");
4101
+ const nodeMap = /* @__PURE__ */ new Map();
4102
+ for (const node of result.nodes) {
4103
+ nodeMap.set(node.nodeId, node);
4112
4104
  }
4113
- }
4114
- async function probeCdpPort(port) {
4115
- try {
4116
- const controller = new AbortController();
4117
- const timeout = setTimeout(() => controller.abort(), 500);
4118
- const resp = await fetch(`http://localhost:${port}/json/version`, {
4119
- signal: controller.signal
4105
+ const SKIP_ROLES = /* @__PURE__ */ new Set(["none", "generic", "InlineTextBox"]);
4106
+ function buildNode(node) {
4107
+ const role = node.role?.value ?? "";
4108
+ const childNodes = (node.childIds ?? []).flatMap((id) => {
4109
+ const child = nodeMap.get(id);
4110
+ return child ? buildNode(child) : [];
4120
4111
  });
4121
- clearTimeout(timeout);
4122
- if (resp.ok) {
4123
- const version = await resp.json();
4124
- const browser = version.Browser;
4125
- const userAgent = version["User-Agent"] ?? "";
4126
- if (!browser) return null;
4127
- const isElectron = userAgent.includes("Electron");
4128
- let app = browser;
4129
- if (isElectron) {
4130
- const appMatch = userAgent.match(
4131
- /(\S+?)\/[\d.]+ Chrome\/[\d.]+ Electron/
4132
- );
4133
- if (appMatch) app = appMatch[1];
4134
- }
4135
- return { browser, app, isElectron };
4112
+ if (SKIP_ROLES.has(role)) {
4113
+ return childNodes;
4136
4114
  }
4137
- } catch {
4138
- }
4139
- return null;
4140
- }
4141
- function getLocalhostListeningPorts() {
4142
- try {
4143
- const output = (0, import_child_process.execSync)(
4144
- 'netstat -an 2>/dev/null | grep "127.0.0.1.*LISTEN"',
4145
- { encoding: "utf-8" }
4146
- );
4147
- const ports = [];
4148
- for (const line of output.split("\n")) {
4149
- const match = line.match(/127\.0\.0\.1\.(\d+)/);
4150
- if (match) {
4151
- ports.push(parseInt(match[1], 10));
4115
+ return [
4116
+ {
4117
+ role,
4118
+ name: node.name?.value ?? "",
4119
+ children: childNodes
4152
4120
  }
4153
- }
4154
- return ports;
4155
- } catch {
4156
- return [];
4121
+ ];
4157
4122
  }
4123
+ const root = result.nodes[0];
4124
+ return root ? buildNode(root) : [];
4158
4125
  }
4159
- async function detectCdpPortsAsync() {
4160
- const ports = getLocalhostListeningPorts();
4161
- const results = [];
4162
- const probes = ports.map(async (port) => {
4163
- const probe = await probeCdpPort(port);
4164
- if (probe) {
4165
- const lower = probe.browser.toLowerCase();
4166
- let bundleId = "unknown";
4167
- for (const [bid, patterns] of Object.entries(BROWSER_PATTERNS)) {
4168
- if (patterns.some((p) => lower.includes(p))) {
4169
- bundleId = bid;
4170
- break;
4171
- }
4172
- }
4173
- results.push({
4174
- port,
4175
- app: probe.app,
4176
- bundleId,
4177
- isElectron: probe.isElectron
4178
- });
4179
- }
4180
- });
4181
- await Promise.all(probes);
4182
- return results;
4183
- }
4184
- async function detectCdpPort() {
4185
- const endpoints = await detectCdpPortsAsync();
4186
- if (endpoints.length === 0) {
4187
- throw new Error(
4188
- "No browser with CDP found. Start your browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
4189
- );
4126
+ var INTERACTIVE_ROLES;
4127
+ var init_a11y = __esm({
4128
+ "src/cdp/a11y.ts"() {
4129
+ INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
4130
+ "button",
4131
+ "link",
4132
+ "textbox",
4133
+ "searchbox",
4134
+ "combobox",
4135
+ "checkbox",
4136
+ "radio",
4137
+ "menuitem",
4138
+ "tab",
4139
+ "switch",
4140
+ "slider",
4141
+ "spinbutton",
4142
+ "option",
4143
+ "menuitemcheckbox",
4144
+ "menuitemradio"
4145
+ ]);
4190
4146
  }
4191
- const defaultBrowser = getDefaultBrowserId();
4192
- if (defaultBrowser) {
4193
- const match = endpoints.find(
4194
- (p) => p.bundleId === defaultBrowser && !p.isElectron
4147
+ });
4148
+
4149
+ // src/cdp/console-recorder.ts
4150
+ function printConsoleSummary(entries) {
4151
+ const errors = entries.filter((e) => e.level === "error");
4152
+ const warnings = entries.filter((e) => e.level === "warning");
4153
+ if (errors.length > 0 || warnings.length > 0) {
4154
+ console.error(
4155
+ `
4156
+ Console: ${errors.length} errors, ${warnings.length} warnings`
4195
4157
  );
4196
- if (match) return match.port;
4158
+ for (const entry of [...errors, ...warnings]) {
4159
+ const prefix = entry.level === "error" ? "ERR" : "WARN";
4160
+ const src = entry.source ? ` (${entry.source})` : "";
4161
+ const text = entry.text.length > 200 ? entry.text.slice(0, 197) + "..." : entry.text;
4162
+ console.error(` [${prefix}] ${text}${src}`);
4163
+ }
4197
4164
  }
4198
- const nonElectron = endpoints.find((p) => !p.isElectron);
4199
- return (nonElectron ?? endpoints[0]).port;
4200
4165
  }
4201
- async function getBrowserClient(port) {
4202
- const versionResp = await fetch(`http://localhost:${port}/json/version`);
4203
- if (!versionResp.ok) {
4204
- throw new Error(
4205
- `Failed to connect to CDP on port ${port}: ${versionResp.status}`
4206
- );
4166
+ var ConsoleRecorder;
4167
+ var init_console_recorder = __esm({
4168
+ "src/cdp/console-recorder.ts"() {
4169
+ ConsoleRecorder = class {
4170
+ constructor(client) {
4171
+ this.client = client;
4172
+ }
4173
+ entries = [];
4174
+ async start() {
4175
+ await this.client.send("Runtime.enable");
4176
+ this.client.on("Runtime.consoleAPICalled", (params) => {
4177
+ const p = params;
4178
+ const text = p.args.map(
4179
+ (a) => a.value !== void 0 ? String(a.value) : a.description ?? ""
4180
+ ).join(" ");
4181
+ this.entries.push({
4182
+ level: p.type,
4183
+ text,
4184
+ timestamp: p.timestamp
4185
+ });
4186
+ });
4187
+ this.client.on("Runtime.exceptionThrown", (params) => {
4188
+ const p = params;
4189
+ this.entries.push({
4190
+ level: "error",
4191
+ text: p.exceptionDetails.exception?.description ?? p.exceptionDetails.text ?? "Unknown exception",
4192
+ timestamp: p.timestamp,
4193
+ source: p.exceptionDetails.url ? `${p.exceptionDetails.url}:${p.exceptionDetails.lineNumber}` : void 0
4194
+ });
4195
+ });
4196
+ }
4197
+ finish() {
4198
+ return [...this.entries];
4199
+ }
4200
+ };
4207
4201
  }
4208
- const version = await versionResp.json();
4209
- const client = new CDPClient(version.webSocketDebuggerUrl);
4210
- await client.waitReady();
4211
- return { client, browserWsUrl: version.webSocketDebuggerUrl };
4202
+ });
4203
+
4204
+ // src/cdp/har-recorder.ts
4205
+ function shouldRecordRequest(url) {
4206
+ if (SKIP_EXTENSIONS.test(url)) return false;
4207
+ if (SKIP_DOMAINS.test(url)) return false;
4208
+ return true;
4212
4209
  }
4213
- async function listTargets(port) {
4214
- const { client } = await getBrowserClient(port);
4210
+ var SKIP_EXTENSIONS, SKIP_DOMAINS, MAX_BODY_SIZE, HARRecorder;
4211
+ var init_har_recorder = __esm({
4212
+ "src/cdp/har-recorder.ts"() {
4213
+ SKIP_EXTENSIONS = /\.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot|css|mp4|webm|mp3)(\?|$)/i;
4214
+ SKIP_DOMAINS = /(google-analytics|googletagmanager|doubleclick|facebook\.com\/tr|px\.ads|analytics|tracking|beacon|telemetry)/i;
4215
+ MAX_BODY_SIZE = 256 * 1024;
4216
+ HARRecorder = class {
4217
+ constructor(client) {
4218
+ this.client = client;
4219
+ }
4220
+ requests = /* @__PURE__ */ new Map();
4221
+ responses = /* @__PURE__ */ new Map();
4222
+ get responseCount() {
4223
+ return this.responses.size;
4224
+ }
4225
+ async start() {
4226
+ await this.client.send("Network.enable");
4227
+ this.client.on("Network.requestWillBeSent", (params) => {
4228
+ const p = params;
4229
+ if (!shouldRecordRequest(p.request.url)) return;
4230
+ this.requests.set(p.requestId, {
4231
+ requestId: p.requestId,
4232
+ url: p.request.url,
4233
+ method: p.request.method,
4234
+ headers: p.request.headers,
4235
+ postData: p.request.postData,
4236
+ timestamp: p.timestamp
4237
+ });
4238
+ });
4239
+ this.client.on("Network.responseReceived", (params) => {
4240
+ const p = params;
4241
+ if (!this.requests.has(p.requestId)) return;
4242
+ this.responses.set(p.requestId, {
4243
+ requestId: p.requestId,
4244
+ url: p.response.url,
4245
+ status: p.response.status,
4246
+ headers: p.response.headers,
4247
+ timestamp: p.timestamp
4248
+ });
4249
+ });
4250
+ }
4251
+ buildHar() {
4252
+ return {
4253
+ log: {
4254
+ entries: Array.from(this.responses.values()).map((resp) => {
4255
+ const req = this.requests.get(resp.requestId);
4256
+ return {
4257
+ startedDateTime: new Date(resp.timestamp * 1e3).toISOString(),
4258
+ request: {
4259
+ method: req?.method ?? "GET",
4260
+ url: resp.url,
4261
+ headers: Object.entries(req?.headers ?? {}).map(
4262
+ ([name, value]) => ({ name, value })
4263
+ ),
4264
+ postData: req?.postData ? { text: req.postData } : void 0
4265
+ },
4266
+ response: {
4267
+ status: resp.status,
4268
+ headers: Object.entries(resp.headers).map(([name, value]) => ({
4269
+ name,
4270
+ value
4271
+ })),
4272
+ content: { text: resp.body }
4273
+ }
4274
+ };
4275
+ })
4276
+ }
4277
+ };
4278
+ }
4279
+ async finish() {
4280
+ await Promise.allSettled(
4281
+ Array.from(this.responses.entries()).map(async ([reqId, resp]) => {
4282
+ const bodyResult = await this.client.send("Network.getResponseBody", {
4283
+ requestId: reqId
4284
+ }, 5e3);
4285
+ const decoded = bodyResult.base64Encoded ? Buffer.from(bodyResult.body, "base64").toString() : bodyResult.body;
4286
+ resp.body = decoded.length > MAX_BODY_SIZE ? decoded.slice(0, MAX_BODY_SIZE) + `
4287
+ [truncated: ${decoded.length} bytes]` : decoded;
4288
+ })
4289
+ );
4290
+ return this.buildHar();
4291
+ }
4292
+ finishPartial() {
4293
+ return this.buildHar();
4294
+ }
4295
+ };
4296
+ }
4297
+ });
4298
+
4299
+ // src/cdp/har-output.ts
4300
+ function writeHarAndPrintSummary(har, harOutPath) {
4301
+ if (har.log.entries.length === 0 && !harOutPath) return void 0;
4302
+ const harPath = harOutPath ?? `/tmp/capture-har-${Date.now()}.json`;
4215
4303
  try {
4216
- await client.send("Target.setDiscoverTargets", { discover: true });
4217
- const result = await client.send("Target.getTargets");
4218
- return result.targetInfos.map((t) => ({
4219
- id: t.targetId,
4220
- title: t.title,
4221
- url: t.url,
4222
- type: t.type,
4223
- webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${t.targetId}`
4224
- }));
4225
- } finally {
4226
- client.close();
4304
+ fs3.writeFileSync(harPath, JSON.stringify(har, null, 2));
4305
+ } catch (err) {
4306
+ const stripped = {
4307
+ log: {
4308
+ ...har.log,
4309
+ entries: har.log.entries.map((e) => ({
4310
+ ...e,
4311
+ response: { ...e.response, content: { text: "[body stripped \u2014 too large to serialize]" } }
4312
+ }))
4313
+ }
4314
+ };
4315
+ fs3.writeFileSync(harPath, JSON.stringify(stripped, null, 2));
4316
+ console.error(" WARNING: Response bodies stripped from HAR (too large to serialize)");
4227
4317
  }
4228
- }
4229
- async function findTab(port, urlPattern) {
4230
- const targets = await listTargets(port);
4231
- const pages = targets.filter((t) => t.type === "page");
4232
- if (!urlPattern) {
4233
- return pages[0] ?? null;
4318
+ const errors = har.log.entries.filter((e) => e.response.status >= 400);
4319
+ console.error(
4320
+ `
4321
+ HAR: ${har.log.entries.length} requests captured \u2192 ${harPath}`
4322
+ );
4323
+ if (errors.length > 0) {
4324
+ console.error(` ${errors.length} failed:`);
4325
+ for (const entry of errors) {
4326
+ const method = entry.request.method.padEnd(6);
4327
+ const status = entry.response.status;
4328
+ const url = entry.request.url.length > 100 ? entry.request.url.slice(0, 97) + "..." : entry.request.url;
4329
+ console.error(` ${method} ${status} ${url}`);
4330
+ }
4234
4331
  }
4235
- const pattern = urlPattern.toLowerCase();
4236
- return pages.find((t) => t.url.toLowerCase().includes(pattern)) ?? null;
4332
+ return harPath;
4237
4333
  }
4238
- async function findTabById(port, targetId) {
4239
- const targets = await listTargets(port);
4240
- const exact = targets.find((t) => t.id === targetId);
4241
- if (exact) return exact;
4242
- if (targetId.length >= 4) {
4243
- const prefix = targetId.toUpperCase();
4244
- const matches = targets.filter((t) => t.id.toUpperCase().startsWith(prefix));
4245
- if (matches.length === 1) return matches[0];
4246
- if (matches.length > 1) {
4247
- throw new Error(
4248
- `Ambiguous target prefix "${targetId}" matches ${matches.length} tabs:
4249
- ` + matches.map((t) => ` ${t.id.slice(0, 8)} ${t.url}`).join("\n")
4250
- );
4251
- }
4252
- }
4253
- return null;
4254
- }
4255
- async function openTab(port, url) {
4256
- const { client } = await getBrowserClient(port);
4257
- try {
4258
- const result = await client.send("Target.createTarget", {
4259
- url,
4260
- background: true
4261
- });
4262
- return {
4263
- id: result.targetId,
4264
- title: "",
4265
- url,
4266
- type: "page",
4267
- webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${result.targetId}`
4268
- };
4269
- } finally {
4270
- client.close();
4271
- }
4272
- }
4273
- async function captureScreenshot(client, viewport, options) {
4274
- const MAX_DIM = 1600;
4275
- if (viewport) {
4276
- await client.send("Emulation.setDeviceMetricsOverride", {
4277
- width: viewport.width,
4278
- height: viewport.height,
4279
- deviceScaleFactor: 1,
4280
- mobile: false
4281
- });
4282
- await new Promise((r) => setTimeout(r, 150));
4283
- }
4284
- if (options?.fullPage) {
4285
- const layoutMetrics = await client.send("Page.getLayoutMetrics");
4286
- const contentWidth = layoutMetrics.cssVisualViewport?.clientWidth ?? viewport?.width ?? 1280;
4287
- const contentHeight = layoutMetrics.contentSize?.height ?? viewport?.height ?? 800;
4288
- await client.send("Emulation.setDeviceMetricsOverride", {
4289
- width: contentWidth,
4290
- height: Math.ceil(contentHeight),
4291
- deviceScaleFactor: 1,
4292
- mobile: false
4293
- });
4294
- await new Promise((r) => setTimeout(r, 150));
4295
- }
4296
- let screenshotOpts = {
4297
- format: "png",
4298
- captureBeyondViewport: false
4299
- };
4300
- try {
4301
- const metrics = await client.send("Page.getLayoutMetrics");
4302
- const vw = metrics.cssVisualViewport?.clientWidth ?? 0;
4303
- const vh = metrics.cssVisualViewport?.clientHeight ?? 0;
4304
- const sx = metrics.cssVisualViewport?.pageX ?? 0;
4305
- const sy = metrics.cssVisualViewport?.pageY ?? 0;
4306
- const dprResult = await client.send("Runtime.evaluate", {
4307
- expression: "window.devicePixelRatio",
4308
- returnByValue: true
4309
- });
4310
- const dpr = dprResult.result.value ?? 1;
4311
- const actualMaxSide = Math.max(vw, vh) * dpr;
4312
- const scale = actualMaxSide > MAX_DIM ? MAX_DIM / actualMaxSide : 1 / dpr;
4313
- screenshotOpts = {
4314
- ...screenshotOpts,
4315
- clip: { x: sx, y: sy, width: vw, height: vh, scale }
4316
- };
4317
- } catch {
4318
- }
4319
- const result = await client.send(
4320
- "Page.captureScreenshot",
4321
- screenshotOpts
4322
- );
4323
- if (viewport || options?.fullPage) {
4324
- await client.send("Emulation.clearDeviceMetricsOverride");
4325
- }
4326
- return Buffer.from(result.data, "base64");
4327
- }
4328
- async function getAccessibilityTree(client) {
4329
- await client.send("Accessibility.enable");
4330
- const result = await client.send("Accessibility.getFullAXTree");
4331
- await client.send("Accessibility.disable");
4332
- const nodeMap = /* @__PURE__ */ new Map();
4333
- for (const node of result.nodes) {
4334
- nodeMap.set(node.nodeId, node);
4335
- }
4336
- const SKIP_ROLES = /* @__PURE__ */ new Set(["none", "generic", "InlineTextBox"]);
4337
- function buildNode(node) {
4338
- const role = node.role?.value ?? "";
4339
- const childNodes = (node.childIds ?? []).flatMap((id) => {
4340
- const child = nodeMap.get(id);
4341
- return child ? buildNode(child) : [];
4342
- });
4343
- if (SKIP_ROLES.has(role)) {
4344
- return childNodes;
4345
- }
4346
- return [
4347
- {
4348
- role,
4349
- name: node.name?.value ?? "",
4350
- children: childNodes
4351
- }
4352
- ];
4334
+ var fs3;
4335
+ var init_har_output = __esm({
4336
+ "src/cdp/har-output.ts"() {
4337
+ fs3 = __toESM(require("fs"));
4353
4338
  }
4354
- const root = result.nodes[0];
4355
- return root ? buildNode(root) : [];
4356
- }
4339
+ });
4340
+
4341
+ // src/cdp/execute.ts
4357
4342
  async function executeInBrowser(code, options = {}) {
4358
4343
  const port = options.port ?? await detectCdpPort();
4359
4344
  if (!options.targetId) {
@@ -4421,6 +4406,19 @@ async function executeInBrowser(code, options = {}) {
4421
4406
  };
4422
4407
  }
4423
4408
  }
4409
+ var init_execute = __esm({
4410
+ "src/cdp/execute.ts"() {
4411
+ init_client();
4412
+ init_detect();
4413
+ init_targets();
4414
+ init_console_recorder();
4415
+ init_har_recorder();
4416
+ init_console_recorder();
4417
+ init_har_output();
4418
+ }
4419
+ });
4420
+
4421
+ // src/cdp/record.ts
4424
4422
  async function recordTraffic(options) {
4425
4423
  const port = options.port ?? await detectCdpPort();
4426
4424
  const duration = options.duration ?? 10;
@@ -4493,14 +4491,14 @@ async function navigateAndRecord(options) {
4493
4491
  try {
4494
4492
  await Promise.race([
4495
4493
  (async () => {
4496
- await new Promise((resolve2) => {
4494
+ await new Promise((resolve3) => {
4497
4495
  const timer = setTimeout(() => {
4498
4496
  console.error("Page load timeout (10s), continuing with settle...");
4499
- resolve2();
4497
+ resolve3();
4500
4498
  }, 1e4);
4501
4499
  client.on("Page.loadEventFired", () => {
4502
4500
  clearTimeout(timer);
4503
- resolve2();
4501
+ resolve3();
4504
4502
  });
4505
4503
  });
4506
4504
  console.error(`Settling for ${settle}ms...`);
@@ -4541,14 +4539,14 @@ async function navigateAndWait(port, url, options = {}) {
4541
4539
  const client = new CDPClient(tab.webSocketDebuggerUrl);
4542
4540
  await client.waitReady();
4543
4541
  await client.send("Page.enable");
4544
- await new Promise((resolve2, reject) => {
4542
+ await new Promise((resolve3, reject) => {
4545
4543
  const timer = setTimeout(
4546
4544
  () => reject(new Error(`Page load timeout: ${url}`)),
4547
4545
  timeout
4548
4546
  );
4549
4547
  client.on("Page.loadEventFired", () => {
4550
4548
  clearTimeout(timer);
4551
- resolve2();
4549
+ resolve3();
4552
4550
  });
4553
4551
  });
4554
4552
  client.close();
@@ -4556,8 +4554,19 @@ async function navigateAndWait(port, url, options = {}) {
4556
4554
  const refreshed = targets.find((t) => t.id === tab.id);
4557
4555
  return refreshed ?? tab;
4558
4556
  }
4557
+ var init_record = __esm({
4558
+ "src/cdp/record.ts"() {
4559
+ init_client();
4560
+ init_detect();
4561
+ init_targets();
4562
+ init_har_recorder();
4563
+ init_har_output();
4564
+ }
4565
+ });
4566
+
4567
+ // src/cdp/tab-lock.ts
4559
4568
  function lockPath(tabId) {
4560
- return path3.join(LOCK_DIR, `capture-cdp-tab-${tabId}.lock`);
4569
+ return path2.join(LOCK_DIR, `capture-cdp-tab-${tabId}.lock`);
4561
4570
  }
4562
4571
  function isPidAlive(pid) {
4563
4572
  try {
@@ -4569,7 +4578,7 @@ function isPidAlive(pid) {
4569
4578
  }
4570
4579
  function isLockStale(filePath) {
4571
4580
  try {
4572
- const content = fs3.readFileSync(filePath, "utf-8");
4581
+ const content = fs4.readFileSync(filePath, "utf-8");
4573
4582
  const { pid, ts } = JSON.parse(content);
4574
4583
  if (!isPidAlive(pid)) return true;
4575
4584
  return Date.now() - ts > LOCK_STALE_MS;
@@ -4583,7 +4592,7 @@ function registerCleanup() {
4583
4592
  const cleanup = () => {
4584
4593
  for (const file of heldLocks) {
4585
4594
  try {
4586
- fs3.unlinkSync(file);
4595
+ fs4.unlinkSync(file);
4587
4596
  } catch {
4588
4597
  }
4589
4598
  }
@@ -4606,9 +4615,9 @@ async function acquireTabLock(tabId) {
4606
4615
  let delay = LOCK_POLL_BASE_MS;
4607
4616
  while (true) {
4608
4617
  try {
4609
- const fd = fs3.openSync(file, "wx");
4610
- fs3.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
4611
- fs3.closeSync(fd);
4618
+ const fd = fs4.openSync(file, "wx");
4619
+ fs4.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
4620
+ fs4.closeSync(fd);
4612
4621
  heldLocks.add(file);
4613
4622
  return;
4614
4623
  } catch (err) {
@@ -4616,7 +4625,7 @@ async function acquireTabLock(tabId) {
4616
4625
  }
4617
4626
  if (isLockStale(file)) {
4618
4627
  try {
4619
- fs3.unlinkSync(file);
4628
+ fs4.unlinkSync(file);
4620
4629
  } catch {
4621
4630
  }
4622
4631
  continue;
@@ -4633,7 +4642,7 @@ async function acquireTabLock(tabId) {
4633
4642
  function isTabLocked(tabId) {
4634
4643
  const file = lockPath(tabId);
4635
4644
  try {
4636
- fs3.accessSync(file);
4645
+ fs4.accessSync(file);
4637
4646
  return !isLockStale(file);
4638
4647
  } catch {
4639
4648
  return false;
@@ -4643,7 +4652,7 @@ function releaseTabLock(tabId) {
4643
4652
  const file = lockPath(tabId);
4644
4653
  heldLocks.delete(file);
4645
4654
  try {
4646
- fs3.unlinkSync(file);
4655
+ fs4.unlinkSync(file);
4647
4656
  } catch {
4648
4657
  }
4649
4658
  }
@@ -4655,583 +4664,124 @@ async function withTabLock(tabId, fn) {
4655
4664
  releaseTabLock(tabId);
4656
4665
  }
4657
4666
  }
4658
- async function autoScreenshot(client, action, label, noScreenshot) {
4659
- if (noScreenshot) return null;
4660
- const shotPath = nextStepPath(action, label);
4661
- if (!shotPath) return null;
4662
- await new Promise((r) => setTimeout(r, 300));
4663
- const png = await captureScreenshot(client);
4664
- fs3.writeFileSync(shotPath, png);
4665
- console.error(` [screenshot] ${shotPath}`);
4666
- return shotPath;
4667
- }
4668
- async function main() {
4669
- const args = process.argv.slice(2);
4670
- const parsed = parseCliArgs(args);
4671
- switch (parsed.command) {
4672
- case "detect": {
4673
- if (parsed.help) {
4674
- console.log(
4675
- "Usage: capture detect\n\nDetect CDP port (prioritizes default browser).\nReturns JSON with port, app name, and all discovered endpoints."
4676
- );
4677
- process.exit(0);
4678
- }
4679
- const endpoints = await detectCdpPortsAsync();
4680
- if (endpoints.length === 0) {
4681
- console.error(
4682
- "ERROR: No browser with CDP found.\n\nFix: Start a browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
4683
- );
4684
- process.exit(1);
4685
- }
4686
- const defaultBrowser = getDefaultBrowserId();
4687
- const preferred = endpoints.find(
4688
- (p) => p.bundleId === defaultBrowser && !p.isElectron
4689
- ) ?? endpoints.find((p) => !p.isElectron) ?? endpoints[0];
4690
- console.log(
4691
- JSON.stringify(
4692
- {
4693
- port: preferred.port,
4694
- app: preferred.app,
4695
- isElectron: preferred.isElectron,
4696
- all: endpoints.map((e) => ({
4697
- port: e.port,
4698
- app: e.app,
4699
- isElectron: e.isElectron
4700
- }))
4701
- },
4702
- null,
4703
- 2
4704
- )
4705
- );
4706
- console.error(
4707
- `
4708
- Found ${endpoints.length} CDP endpoint${endpoints.length > 1 ? "s" : ""}. Default: port ${preferred.port} (${preferred.app}).
4709
-
4710
- Next: capture list --port ${preferred.port}`
4711
- );
4712
- break;
4713
- }
4714
- case "list": {
4715
- if (parsed.help) {
4716
- console.log(
4717
- "Usage: capture list [--port <port>]\n\nList all browser tabs across all CDP endpoints (or a specific port).\nReturns JSON array of {id, title, url, port, app}."
4718
- );
4719
- process.exit(0);
4720
- }
4721
- if (parsed.port) {
4722
- const targets = await listTargets(parsed.port);
4723
- const pages = targets.filter((t) => t.type === "page");
4724
- console.log(JSON.stringify(pages.map((t) => ({
4725
- id: t.id,
4726
- title: t.title,
4727
- url: t.url
4728
- })), null, 2));
4729
- console.error(
4730
- `
4731
- ${pages.length} tab${pages.length !== 1 ? "s" : ""} on port ${parsed.port}.
4732
-
4733
- Target a tab with: --target <id prefix> (first 8 chars sufficient)`
4734
- );
4735
- } else {
4736
- const endpoints = await detectCdpPortsAsync();
4737
- if (endpoints.length === 0) {
4738
- console.error("No CDP endpoints found.");
4739
- process.exit(1);
4740
- }
4741
- const allPages = [];
4742
- await Promise.all(endpoints.map(async (ep) => {
4743
- try {
4744
- const targets = await listTargets(ep.port);
4745
- const pages = targets.filter((t) => t.type === "page");
4746
- for (const t of pages) {
4747
- allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
4748
- }
4749
- } catch {
4750
- }
4751
- }));
4752
- console.log(JSON.stringify(allPages, null, 2));
4753
- console.error(
4754
- `
4755
- ${allPages.length} tab${allPages.length !== 1 ? "s" : ""} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? "s" : ""}.
4667
+ var fs4, os2, path2, LOCK_DIR, LOCK_STALE_MS, LOCK_TIMEOUT_MS, LOCK_POLL_BASE_MS, heldLocks, cleanupRegistered;
4668
+ var init_tab_lock = __esm({
4669
+ "src/cdp/tab-lock.ts"() {
4670
+ fs4 = __toESM(require("fs"));
4671
+ os2 = __toESM(require("os"));
4672
+ path2 = __toESM(require("path"));
4673
+ LOCK_DIR = os2.tmpdir();
4674
+ LOCK_STALE_MS = 12e4;
4675
+ LOCK_TIMEOUT_MS = 6e4;
4676
+ LOCK_POLL_BASE_MS = 200;
4677
+ heldLocks = /* @__PURE__ */ new Set();
4678
+ cleanupRegistered = false;
4679
+ }
4680
+ });
4756
4681
 
4757
- Target a tab with: --target <id prefix> (first 8 chars sufficient)`
4758
- );
4759
- }
4760
- break;
4682
+ // src/cdp/args.ts
4683
+ function parseCliArgs(argv) {
4684
+ const command = argv[0] ?? "";
4685
+ const positional = [];
4686
+ const parsed = { command, positional };
4687
+ for (let i = 1; i < argv.length; i++) {
4688
+ const arg = argv[i];
4689
+ const next = argv[i + 1];
4690
+ if (arg === "--port" && next) {
4691
+ parsed.port = parseInt(next, 10);
4692
+ i++;
4693
+ } else if (arg === "--out" && next) {
4694
+ parsed.out = next;
4695
+ i++;
4696
+ } else if (arg === "--json") {
4697
+ parsed.json = true;
4698
+ } else if (arg === "--interactive") {
4699
+ parsed.interactive = true;
4700
+ } else if (arg === "--har-out" && next) {
4701
+ parsed.harOut = next;
4702
+ i++;
4703
+ } else if (arg === "--record") {
4704
+ parsed.record = true;
4705
+ } else if (arg === "--duration" && next) {
4706
+ parsed.duration = parseInt(next, 10);
4707
+ i++;
4708
+ } else if (arg === "--settle" && next) {
4709
+ parsed.settle = parseInt(next, 10);
4710
+ i++;
4711
+ } else if (arg === "--file" && next) {
4712
+ parsed.file = next;
4713
+ i++;
4714
+ } else if (arg === "--nested") {
4715
+ parsed.nested = true;
4716
+ } else if (arg === "--har" && next) {
4717
+ parsed.har = next;
4718
+ i++;
4719
+ } else if (arg === "--new") {
4720
+ parsed.new = true;
4721
+ } else if (arg === "--target" && next) {
4722
+ parsed.target = next;
4723
+ i++;
4724
+ } else if (arg === "--role" && next) {
4725
+ parsed.role = next;
4726
+ i++;
4727
+ } else if (arg === "--into" && next) {
4728
+ parsed.into = next;
4729
+ i++;
4730
+ } else if (arg === "--no-screenshot") {
4731
+ parsed.noScreenshot = true;
4732
+ } else if (arg === "--viewport" && next) {
4733
+ parsed.viewport = next;
4734
+ i++;
4735
+ } else if (arg === "--full-page") {
4736
+ parsed.fullPage = true;
4737
+ } else if (arg === "--height" && next) {
4738
+ parsed.height = parseInt(next, 10);
4739
+ i++;
4740
+ } else if (arg === "--filter-url" && next) {
4741
+ parsed.filterUrl = next;
4742
+ i++;
4743
+ } else if (arg === "--filter-status" && next) {
4744
+ parsed.filterStatus = next;
4745
+ i++;
4746
+ } else if (arg === "--filter-method" && next) {
4747
+ parsed.filterMethod = next.toUpperCase();
4748
+ i++;
4749
+ } else if (arg === "--limit" && next) {
4750
+ parsed.limit = parseInt(next, 10);
4751
+ i++;
4752
+ } else if (arg === "--help" || arg === "-h") {
4753
+ parsed.help = true;
4754
+ } else if (arg.startsWith("--")) {
4755
+ console.error(`Unknown flag: ${arg}`);
4756
+ process.exit(1);
4757
+ } else {
4758
+ positional.push(arg);
4761
4759
  }
4762
- case "open": {
4763
- if (parsed.help) {
4764
- console.log(
4765
- 'Usage: capture open <url> [--new] [--port <port>]\n\nOpen URL in browser (returns tab ID).\n\nExample: capture open "https://app.example.com" --new'
4766
- );
4767
- process.exit(0);
4768
- }
4769
- const url = parsed.positional[0];
4770
- if (!url) {
4771
- console.error(
4772
- 'ERROR: Missing URL.\n\nUsage: capture open <url> [--new] [--port <port>]\n\nExample: capture open "https://app.example.com" --new'
4773
- );
4774
- process.exit(1);
4775
- }
4776
- const p = parsed.port ?? await detectCdpPort();
4777
- const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
4778
- console.log(JSON.stringify({
4779
- id: tab.id,
4780
- title: tab.title,
4781
- url: tab.url,
4782
- port: p
4783
- }, null, 2));
4784
- console.error(
4785
- `
4786
- Opened: ${tab.title || url}
4787
-
4788
- Next: capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}
4789
- capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}
4790
- capture a11y --port ${p} --target ${tab.id.slice(0, 8)}`
4791
- );
4792
- break;
4793
- }
4794
- case "exec": {
4795
- if (parsed.help) {
4796
- console.log(
4797
- `Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]
4798
-
4799
- Execute JavaScript in a browser tab. Supports await (wrapped in async IIFE).
4800
-
4801
- Examples:
4802
- capture exec "document.title" --target <id>
4803
- capture exec "await fetch('/api/data').then(r=>r.json())" --target <id>
4804
- capture exec "document.querySelector('.btn').click()" --target <id>
4805
- capture exec --file /tmp/scrape.js --target <id> --record`
4806
- );
4807
- process.exit(0);
4808
- }
4809
- let code;
4810
- if (parsed.file) {
4811
- code = fs3.readFileSync(parsed.file, "utf-8");
4812
- } else {
4813
- code = parsed.positional[0];
4814
- if (!code) {
4815
- console.error(
4816
- `ERROR: Missing code to execute.
4817
-
4818
- Usage: capture exec <code> [--target <id>] [--record] [--file <path>]
4760
+ }
4761
+ if (!parsed.target && process.env.CDP_TARGET) {
4762
+ parsed.target = process.env.CDP_TARGET;
4763
+ }
4764
+ if (!parsed.har && process.env.CDP_HAR_ID) {
4765
+ parsed.har = process.env.CDP_HAR_ID;
4766
+ }
4767
+ const session = getActiveSession();
4768
+ if (session) {
4769
+ if (!parsed.har && session.harId) parsed.har = session.harId;
4770
+ if (!parsed.target && session.targetId) parsed.target = session.targetId;
4771
+ }
4772
+ return parsed;
4773
+ }
4774
+ var init_args = __esm({
4775
+ "src/cdp/args.ts"() {
4776
+ init_session_context();
4777
+ }
4778
+ });
4819
4779
 
4820
- Examples:
4821
- capture exec "document.title" --target <id>
4822
- capture exec "document.querySelector('.btn').click()" --target <id>
4823
- capture exec --file /tmp/scrape.js --target <id> --record`
4824
- );
4825
- process.exit(1);
4826
- }
4827
- }
4828
- await withConnection(
4829
- parsed,
4830
- async (client) => {
4831
- await client.send("Emulation.setFocusEmulationEnabled", { enabled: true });
4832
- let standaloneRecorder;
4833
- if (parsed.record) {
4834
- standaloneRecorder = new HARRecorder(client);
4835
- await standaloneRecorder.start();
4836
- }
4837
- const needsAsyncWrap = /\bawait\b/.test(code);
4838
- const expression = needsAsyncWrap ? `(async () => { ${code} })()` : code;
4839
- const evalResult = await client.send("Runtime.evaluate", {
4840
- expression,
4841
- awaitPromise: true,
4842
- returnByValue: true
4843
- });
4844
- if (standaloneRecorder) {
4845
- const har = await standaloneRecorder.finish();
4846
- writeHarAndPrintSummary(har, parsed.harOut);
4847
- }
4848
- if (evalResult.exceptionDetails) {
4849
- console.error(
4850
- `ERROR: ${evalResult.exceptionDetails.exception?.description ?? "Unknown error"}`
4851
- );
4852
- process.exit(1);
4853
- }
4854
- console.log(JSON.stringify(evalResult.result?.value));
4855
- },
4856
- { settle: 3e3 }
4857
- );
4858
- break;
4859
- }
4860
- case "record": {
4861
- if (parsed.help) {
4862
- console.log(
4863
- "Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]\n\nPassive HAR recording for the specified duration (default: 10s)."
4864
- );
4865
- process.exit(0);
4866
- }
4867
- if (!parsed.target) {
4868
- console.error(
4869
- "Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]"
4870
- );
4871
- process.exit(1);
4872
- }
4873
- const result = await recordTraffic({
4874
- port: parsed.port,
4875
- targetId: parsed.target,
4876
- duration: parsed.duration,
4877
- harOutPath: parsed.harOut
4878
- });
4879
- console.log(
4880
- JSON.stringify(
4881
- { entryCount: result.entryCount, harPath: result.harPath },
4882
- null,
4883
- 2
4884
- )
4885
- );
4886
- break;
4887
- }
4888
- case "reset-tab": {
4889
- if (parsed.help) {
4890
- console.log(
4891
- 'Usage: capture reset-tab <url> [--port <port>]\n\nAbandon a stuck/unresponsive tab and open a fresh one.\nUpdates session context so subsequent commands auto-target the new tab.\n\nExample: capture reset-tab "https://www.instagram.com/"'
4892
- );
4893
- process.exit(0);
4894
- }
4895
- const url = parsed.positional[0];
4896
- if (!url) {
4897
- console.error(
4898
- "Usage: capture reset-tab <url> [--port <port>]"
4899
- );
4900
- process.exit(1);
4901
- }
4902
- const p = parsed.port ?? await detectCdpPort();
4903
- const tab = await openTab(p, url);
4904
- if (tab.webSocketDebuggerUrl) {
4905
- const client = new CDPClient(tab.webSocketDebuggerUrl);
4906
- await client.waitReady();
4907
- await client.send("Page.enable");
4908
- await new Promise((resolve2) => {
4909
- const timer = setTimeout(() => resolve2(), 1e4);
4910
- client.on("Page.loadEventFired", () => {
4911
- clearTimeout(timer);
4912
- resolve2();
4913
- });
4914
- });
4915
- client.close();
4916
- }
4917
- updateActiveSession({ targetId: tab.id });
4918
- console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
4919
- console.error(`
4920
- New tab opened. Session context updated.`);
4921
- console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
4922
- break;
4923
- }
4924
- case "navigate": {
4925
- if (parsed.help) {
4926
- console.log(
4927
- 'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>] [--har <id>]\n\nNavigate to URL and record HAR. Appends to session HAR if --har is set.\n\nExample: capture navigate "https://app.example.com/dashboard" --settle 3000'
4928
- );
4929
- process.exit(0);
4930
- }
4931
- const url = parsed.positional[0];
4932
- if (!url) {
4933
- console.error(
4934
- "Usage: capture navigate <url> [--har-out <path>] [--settle <ms>]"
4935
- );
4936
- process.exit(1);
4937
- }
4938
- const result = await navigateAndRecord({
4939
- port: parsed.port,
4940
- url,
4941
- targetId: parsed.target,
4942
- harOutPath: parsed.har ? void 0 : parsed.harOut,
4943
- settle: parsed.settle
4944
- });
4945
- if (parsed.har && result.har.log.entries.length > 0) {
4946
- appendToHarRecording(parsed.har, result.har.log.entries);
4947
- console.error(` [har:${parsed.har}] +${result.har.log.entries.length} entries`);
4948
- }
4949
- console.log(
4950
- JSON.stringify(
4951
- {
4952
- entryCount: result.entryCount,
4953
- harPath: result.harPath,
4954
- tabUrl: result.tab.url,
4955
- timedOut: result.timedOut
4956
- },
4957
- null,
4958
- 2
4959
- )
4960
- );
4961
- break;
4962
- }
4963
- case "screenshot": {
4964
- if (parsed.help) {
4965
- console.log(
4966
- "Usage: capture screenshot [--target <id>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\nCapture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\nOptions:\n --viewport <preset> Viewport preset (default: desktop)\n desktop-wide 1920x1080\n desktop 1280x800\n tablet 768x1024\n mobile 390x844\n --height <px> Override viewport height (e.g. 1600)\n --full-page Capture the entire scrollable page"
4967
- );
4968
- process.exit(0);
4969
- }
4970
- const VIEWPORTS = {
4971
- "desktop-wide": { width: 1920, height: 1080 },
4972
- "desktop": { width: 1280, height: 800 },
4973
- "tablet": { width: 768, height: 1024 },
4974
- "mobile": { width: 390, height: 844 }
4975
- };
4976
- const viewportName = parsed.viewport ?? "desktop";
4977
- const viewport = VIEWPORTS[viewportName];
4978
- if (!viewport) {
4979
- console.error(`Unknown viewport "${viewportName}". Options: ${Object.keys(VIEWPORTS).join(", ")}`);
4980
- process.exit(1);
4981
- }
4982
- if (parsed.height) {
4983
- viewport.height = parsed.height;
4984
- }
4985
- const result = await withConnection(
4986
- parsed,
4987
- async (client) => {
4988
- const png = await captureScreenshot(client, viewport, { fullPage: parsed.fullPage });
4989
- const outPath = parsed.out ? parsed.out : nextStepPath("screenshot", "manual") ?? `/tmp/capture-screenshot-${Date.now()}.png`;
4990
- fs3.writeFileSync(outPath, png);
4991
- return { path: outPath, bytes: png.length };
4992
- },
4993
- { settle: 0 }
4994
- );
4995
- console.log(JSON.stringify(result, null, 2));
4996
- break;
4997
- }
4998
- case "click": {
4999
- if (parsed.help) {
5000
- console.log(
5001
- 'Usage: capture click "name" [--role button|link|...] [--no-screenshot]\n\nClick an element by its accessible name. Auto-captures a screenshot after clicking.'
5002
- );
5003
- process.exit(0);
5004
- }
5005
- const name = parsed.positional[0];
5006
- if (!name) {
5007
- console.error('Usage: capture click "name" [--role button|link|...] [--no-screenshot]');
5008
- process.exit(1);
5009
- }
5010
- const clickSettle = parsed.settle !== void 0 ? parsed.settle : parsed.har ? 2500 : 1e3;
5011
- const result = await withConnection(
5012
- parsed,
5013
- async (client) => {
5014
- const clickResult = await clickByName(client, name, parsed.role);
5015
- const screenshot = await autoScreenshot(client, "click", name, parsed.noScreenshot);
5016
- return { ...clickResult, screenshot };
5017
- },
5018
- { settle: clickSettle }
5019
- );
5020
- console.log(JSON.stringify(result, null, 2));
5021
- break;
5022
- }
5023
- case "type": {
5024
- if (parsed.help) {
5025
- console.log(
5026
- 'Usage: capture type "text" [--into "field name"] [--no-screenshot]\n\nType text into the focused element, or into a named field with --into.'
5027
- );
5028
- process.exit(0);
5029
- }
5030
- const text = parsed.positional[0];
5031
- if (!text) {
5032
- console.error('Usage: capture type "text" [--into "field name"] [--no-screenshot]');
5033
- process.exit(1);
5034
- }
5035
- const result = await withConnection(
5036
- parsed,
5037
- async (client) => {
5038
- let field = null;
5039
- if (parsed.into) {
5040
- await focusAndType(client, parsed.into, text, parsed.role);
5041
- field = parsed.into;
5042
- } else {
5043
- await typeText(client, text);
5044
- }
5045
- const label = field ?? text;
5046
- const screenshot = await autoScreenshot(client, "type", label, parsed.noScreenshot);
5047
- return { typed: text, field, screenshot };
5048
- },
5049
- { settle: parsed.settle !== void 0 ? parsed.settle : parsed.har ? 1500 : 500 }
5050
- );
5051
- console.log(JSON.stringify(result, null, 2));
5052
- break;
5053
- }
5054
- case "a11y": {
5055
- if (parsed.help) {
5056
- console.log(
5057
- "Usage: capture a11y [--target <id>] [--json] [--interactive]\n\nGet the accessibility tree. Use --interactive for interactive elements only.\nUse --json for structured JSON output."
5058
- );
5059
- process.exit(0);
5060
- }
5061
- const result = await withConnection(
5062
- parsed,
5063
- async (client) => {
5064
- const tree = await getAccessibilityTree(client);
5065
- if (parsed.json) {
5066
- return tree;
5067
- }
5068
- const lines = flattenA11yTree(tree, {
5069
- interactive: parsed.interactive
5070
- });
5071
- return lines.join("\n");
5072
- },
5073
- { settle: 0 }
5074
- );
5075
- if (parsed.json) {
5076
- console.log(JSON.stringify(result, null, 2));
5077
- } else {
5078
- console.log(result);
5079
- }
5080
- if (!parsed.json) {
5081
- const lineCount = typeof result === "string" ? result.split("\n").filter(Boolean).length : 0;
5082
- if (lineCount === 0) {
5083
- console.error(
5084
- "\nNo elements found." + (parsed.interactive ? " Try without --interactive to see all nodes." : "") + '\n\nIf this page is part of your project, consider adding ARIA attributes:\n - role="button|link|textbox" on interactive elements\n - aria-label="..." on elements without visible text\n - Semantic HTML (<button>, <input>, <nav>) provides roles automatically'
5085
- );
5086
- } else if (parsed.interactive && lineCount < 5) {
5087
- console.error(
5088
- `
5089
- Only ${lineCount} interactive element${lineCount === 1 ? "" : "s"} found. If this seems low, the page may lack ARIA markup.
5090
- Consider adding role and aria-label attributes to interactive elements in your frontend code.`
5091
- );
5092
- }
5093
- }
5094
- break;
5095
- }
5096
- case "har": {
5097
- if (parsed.help) {
5098
- console.log(
5099
- "Usage: capture har <create|read|delete>\n\n create Create a new HAR recording (returns id)\n read [id] Read accumulated HAR entries (id optional in active session)\n delete <id> Delete a HAR recording\n\nFilters (read):\n --filter-url <pattern> Substring or regex match on URL\n --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n --filter-method <method> HTTP method (GET, POST, ...)\n --limit <N> Return only the first N matching entries\n\nPass --har <id> to exec/navigate commands to append traffic to a recording."
5100
- );
5101
- process.exit(0);
5102
- }
5103
- const subcommand = args[1];
5104
- if (subcommand === "create") {
5105
- const { id, path: harPath } = createHarRecording();
5106
- console.log(JSON.stringify({ id, path: harPath }, null, 2));
5107
- console.error(
5108
- `
5109
- Pass --har ${id} to subsequent commands to record traffic.`
5110
- );
5111
- break;
5112
- }
5113
- if (subcommand === "read") {
5114
- let id = args[2];
5115
- if (id && id.startsWith("--")) id = void 0;
5116
- if (!id) {
5117
- const session = getActiveSession();
5118
- if (session?.harId) {
5119
- id = session.harId;
5120
- } else {
5121
- console.error("Usage: capture har read [id]\n ID is optional when a capture session is active.");
5122
- process.exit(1);
5123
- }
5124
- }
5125
- const har = readHarRecording(id);
5126
- if (!har) {
5127
- console.error(`No HAR recording found for id: ${id}`);
5128
- process.exit(1);
5129
- }
5130
- let entries = har.log.entries;
5131
- const total = entries.length;
5132
- const filters = [];
5133
- if (parsed.filterUrl) {
5134
- const pattern = parsed.filterUrl;
5135
- let re = null;
5136
- try {
5137
- re = new RegExp(pattern, "i");
5138
- } catch {
5139
- re = null;
5140
- }
5141
- entries = entries.filter(
5142
- (e) => re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase())
5143
- );
5144
- filters.push(`url~${pattern}`);
5145
- }
5146
- if (parsed.filterStatus) {
5147
- const s = parsed.filterStatus;
5148
- let matcher;
5149
- if (/^\d+-\d+$/.test(s)) {
5150
- const [lo, hi] = s.split("-").map((n) => parseInt(n, 10));
5151
- matcher = (c) => c >= lo && c <= hi;
5152
- } else if (/^\d+$/.test(s)) {
5153
- if (s.length < 3) {
5154
- matcher = (c) => String(c).startsWith(s);
5155
- } else {
5156
- const n = parseInt(s, 10);
5157
- matcher = (c) => c === n;
5158
- }
5159
- } else {
5160
- matcher = () => true;
5161
- }
5162
- entries = entries.filter((e) => matcher(e.response.status));
5163
- filters.push(`status=${s}`);
5164
- }
5165
- if (parsed.filterMethod) {
5166
- const m = parsed.filterMethod;
5167
- entries = entries.filter((e) => e.request.method.toUpperCase() === m);
5168
- filters.push(`method=${m}`);
5169
- }
5170
- if (typeof parsed.limit === "number" && parsed.limit > 0) {
5171
- entries = entries.slice(0, parsed.limit);
5172
- filters.push(`limit=${parsed.limit}`);
5173
- }
5174
- const filtersLabel = filters.length > 0 ? ` [${filters.join(", ")}]` : "";
5175
- console.error(
5176
- `HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`
5177
- );
5178
- console.log(JSON.stringify({ log: { entries } }, null, 2));
5179
- break;
5180
- }
5181
- if (subcommand === "delete") {
5182
- const id = args[2];
5183
- if (!id) {
5184
- console.error("Usage: capture har delete <id>");
5185
- process.exit(1);
5186
- }
5187
- deleteHarRecording(id);
5188
- console.log(JSON.stringify({ deleted: true, id }, null, 2));
5189
- break;
5190
- }
5191
- console.error(
5192
- "Usage: capture har <create|read|delete>\n create Create a new HAR recording (returns id)\n read <id> Read accumulated HAR entries\n delete <id> Delete a HAR recording"
5193
- );
5194
- process.exit(1);
5195
- break;
5196
- }
5197
- case "network": {
5198
- if (parsed.help) {
5199
- console.log(
5200
- 'Usage: capture network <offline|online> [--target <id>]\n\nToggle network connectivity for a tab. Use "offline" to simulate\nnetwork failure (kills WebSocket connections, blocks HTTP requests).\nUse "online" to restore connectivity.'
5201
- );
5202
- process.exit(0);
5203
- }
5204
- const mode = parsed.positional[0];
5205
- if (!mode || !["offline", "online"].includes(mode)) {
5206
- console.error("Usage: capture network <offline|online> [--target <id>]");
5207
- process.exit(1);
5208
- }
5209
- const offline = mode === "offline";
5210
- const result = await withConnection(
5211
- parsed,
5212
- async (client) => {
5213
- await client.send("Network.enable");
5214
- await client.send("Network.emulateNetworkConditions", {
5215
- offline,
5216
- latency: offline ? -1 : 0,
5217
- downloadThroughput: offline ? 0 : -1,
5218
- uploadThroughput: offline ? 0 : -1
5219
- });
5220
- return { network: mode, offline };
5221
- },
5222
- { settle: 0 }
5223
- );
5224
- console.log(JSON.stringify(result, null, 2));
5225
- if (offline) {
5226
- console.error("\nNetwork disabled. WebSocket connections will drop.");
5227
- console.error("Restore with: capture network online");
5228
- } else {
5229
- console.error("\nNetwork restored.");
5230
- }
5231
- break;
5232
- }
5233
- default:
5234
- console.log(`CDP \u2014 browser automation via exec, a11y interactions, and screenshots.
4780
+ // src/cdp/usage.ts
4781
+ var CDP_USAGE;
4782
+ var init_usage = __esm({
4783
+ "src/cdp/usage.ts"() {
4784
+ CDP_USAGE = `CDP \u2014 browser automation via exec, a11y interactions, and screenshots.
5235
4785
 
5236
4786
  Commands:
5237
4787
  exec <code> Execute JavaScript in a browser tab
@@ -5292,246 +4842,919 @@ Examples:
5292
4842
  capture har create
5293
4843
  capture exec "document.querySelector('form').submit()" --target <id> --har <id>
5294
4844
  capture har read <id>
5295
- `);
4845
+ `;
4846
+ }
4847
+ });
4848
+
4849
+ // src/cdp/commands/tabs.ts
4850
+ async function cmdDetect(parsed, _args) {
4851
+ if (parsed.help) {
4852
+ console.log(
4853
+ "Usage: capture detect\n\nDetect CDP port (prioritizes default browser).\nReturns JSON with port, app name, and all discovered endpoints."
4854
+ );
4855
+ process.exit(0);
4856
+ }
4857
+ const endpoints = await detectCdpPortsAsync();
4858
+ if (endpoints.length === 0) {
4859
+ console.error(
4860
+ "ERROR: No browser with CDP found.\n\nFix: Start a browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
4861
+ );
4862
+ process.exit(1);
5296
4863
  }
4864
+ const defaultBrowser = getDefaultBrowserId();
4865
+ const preferred = endpoints.find(
4866
+ (p) => p.bundleId === defaultBrowser && !p.isElectron
4867
+ ) ?? endpoints.find((p) => !p.isElectron) ?? endpoints[0];
4868
+ console.log(
4869
+ JSON.stringify(
4870
+ {
4871
+ port: preferred.port,
4872
+ app: preferred.app,
4873
+ isElectron: preferred.isElectron,
4874
+ all: endpoints.map((e) => ({
4875
+ port: e.port,
4876
+ app: e.app,
4877
+ isElectron: e.isElectron
4878
+ }))
4879
+ },
4880
+ null,
4881
+ 2
4882
+ )
4883
+ );
4884
+ console.error(
4885
+ `
4886
+ Found ${endpoints.length} CDP endpoint${endpoints.length > 1 ? "s" : ""}. Default: port ${preferred.port} (${preferred.app}).
4887
+
4888
+ Next (most tasks): capture session start --url <your app url>
4889
+ (one-off): capture list --port ${preferred.port}`
4890
+ );
4891
+ return;
5297
4892
  }
5298
- var import_child_process, fs3, os2, path3, SKIP_EXTENSIONS, SKIP_DOMAINS, MAX_BODY_SIZE, HARRecorder, ConsoleRecorder, INTERACTIVE_ROLES, BROWSER_PATTERNS, CDPClient, LOCK_DIR, LOCK_STALE_MS, LOCK_TIMEOUT_MS, LOCK_POLL_BASE_MS, heldLocks, cleanupRegistered, isMainModule;
5299
- var init_cdp = __esm({
5300
- "src/cdp.ts"() {
5301
- import_child_process = require("child_process");
5302
- fs3 = __toESM(require("fs"));
5303
- os2 = __toESM(require("os"));
5304
- path3 = __toESM(require("path"));
5305
- init_wrapper();
5306
- init_har_manager();
5307
- init_session_context();
5308
- init_interact();
5309
- SKIP_EXTENSIONS = /\.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot|css|mp4|webm|mp3)(\?|$)/i;
5310
- SKIP_DOMAINS = /(google-analytics|googletagmanager|doubleclick|facebook\.com\/tr|px\.ads|analytics|tracking|beacon|telemetry)/i;
5311
- MAX_BODY_SIZE = 256 * 1024;
5312
- HARRecorder = class {
5313
- constructor(client) {
5314
- this.client = client;
5315
- }
5316
- requests = /* @__PURE__ */ new Map();
5317
- responses = /* @__PURE__ */ new Map();
5318
- get responseCount() {
5319
- return this.responses.size;
5320
- }
5321
- async start() {
5322
- await this.client.send("Network.enable");
5323
- this.client.on("Network.requestWillBeSent", (params) => {
5324
- const p = params;
5325
- if (!shouldRecordRequest(p.request.url)) return;
5326
- this.requests.set(p.requestId, {
5327
- requestId: p.requestId,
5328
- url: p.request.url,
5329
- method: p.request.method,
5330
- headers: p.request.headers,
5331
- postData: p.request.postData,
5332
- timestamp: p.timestamp
5333
- });
5334
- });
5335
- this.client.on("Network.responseReceived", (params) => {
5336
- const p = params;
5337
- if (!this.requests.has(p.requestId)) return;
5338
- this.responses.set(p.requestId, {
5339
- requestId: p.requestId,
5340
- url: p.response.url,
5341
- status: p.response.status,
5342
- headers: p.response.headers,
5343
- timestamp: p.timestamp
5344
- });
5345
- });
4893
+ async function cmdList(parsed, _args) {
4894
+ if (parsed.help) {
4895
+ console.log(
4896
+ "Usage: capture list [--port <port>]\n\nList all browser tabs across all CDP endpoints (or a specific port).\nReturns JSON array of {id, title, url, port, app}."
4897
+ );
4898
+ process.exit(0);
4899
+ }
4900
+ if (parsed.port) {
4901
+ const targets = await listTargets(parsed.port);
4902
+ const pages = targets.filter((t) => t.type === "page");
4903
+ console.log(JSON.stringify(pages.map((t) => ({
4904
+ id: t.id,
4905
+ title: t.title,
4906
+ url: t.url
4907
+ })), null, 2));
4908
+ console.error(
4909
+ `
4910
+ ${pages.length} tab${pages.length !== 1 ? "s" : ""} on port ${parsed.port}.
4911
+
4912
+ Target a tab with: --target <id prefix> (first 8 chars sufficient)`
4913
+ );
4914
+ } else {
4915
+ const endpoints = await detectCdpPortsAsync();
4916
+ if (endpoints.length === 0) {
4917
+ console.error("No CDP endpoints found.");
4918
+ process.exit(1);
4919
+ }
4920
+ const allPages = [];
4921
+ await Promise.all(endpoints.map(async (ep) => {
4922
+ try {
4923
+ const targets = await listTargets(ep.port);
4924
+ const pages = targets.filter((t) => t.type === "page");
4925
+ for (const t of pages) {
4926
+ allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
4927
+ }
4928
+ } catch {
5346
4929
  }
5347
- buildHar() {
5348
- return {
5349
- log: {
5350
- entries: Array.from(this.responses.values()).map((resp) => {
5351
- const req = this.requests.get(resp.requestId);
5352
- return {
5353
- startedDateTime: new Date(resp.timestamp * 1e3).toISOString(),
5354
- request: {
5355
- method: req?.method ?? "GET",
5356
- url: resp.url,
5357
- headers: Object.entries(req?.headers ?? {}).map(
5358
- ([name, value]) => ({ name, value })
5359
- ),
5360
- postData: req?.postData ? { text: req.postData } : void 0
5361
- },
5362
- response: {
5363
- status: resp.status,
5364
- headers: Object.entries(resp.headers).map(([name, value]) => ({
5365
- name,
5366
- value
5367
- })),
5368
- content: { text: resp.body }
5369
- }
5370
- };
5371
- })
5372
- }
5373
- };
5374
- }
5375
- async finish() {
5376
- await Promise.allSettled(
5377
- Array.from(this.responses.entries()).map(async ([reqId, resp]) => {
5378
- const bodyResult = await this.client.send("Network.getResponseBody", {
5379
- requestId: reqId
5380
- }, 5e3);
5381
- const decoded = bodyResult.base64Encoded ? Buffer.from(bodyResult.body, "base64").toString() : bodyResult.body;
5382
- resp.body = decoded.length > MAX_BODY_SIZE ? decoded.slice(0, MAX_BODY_SIZE) + `
5383
- [truncated: ${decoded.length} bytes]` : decoded;
5384
- })
5385
- );
5386
- return this.buildHar();
4930
+ }));
4931
+ console.log(JSON.stringify(allPages, null, 2));
4932
+ console.error(
4933
+ `
4934
+ ${allPages.length} tab${allPages.length !== 1 ? "s" : ""} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? "s" : ""}.
4935
+
4936
+ Target a tab with: --target <id prefix> (first 8 chars sufficient)`
4937
+ );
4938
+ }
4939
+ return;
4940
+ }
4941
+ async function cmdOpen(parsed, _args) {
4942
+ if (parsed.help) {
4943
+ console.log(
4944
+ 'Usage: capture open <url> [--new] [--port <port>]\n\nOpen URL in browser (returns tab ID).\n\nExample: capture open "https://app.example.com" --new'
4945
+ );
4946
+ process.exit(0);
4947
+ }
4948
+ const url = parsed.positional[0];
4949
+ if (!url) {
4950
+ console.error(
4951
+ 'ERROR: Missing URL.\n\nUsage: capture open <url> [--new] [--port <port>]\n\nExample: capture open "https://app.example.com" --new'
4952
+ );
4953
+ process.exit(1);
4954
+ }
4955
+ const p = parsed.port ?? await detectCdpPort();
4956
+ const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
4957
+ console.log(JSON.stringify({
4958
+ id: tab.id,
4959
+ title: tab.title,
4960
+ url: tab.url,
4961
+ port: p
4962
+ }, null, 2));
4963
+ console.error(
4964
+ `
4965
+ Opened: ${tab.title || url}
4966
+
4967
+ For a multi-step validation, prefer a session (auto-targets this kind of tab):
4968
+ capture session start --url "${url}"
4969
+
4970
+ Or interact one-off against this tab:
4971
+ capture a11y --port ${p} --target ${tab.id.slice(0, 8)} --interactive
4972
+ capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}
4973
+ capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}`
4974
+ );
4975
+ return;
4976
+ }
4977
+ async function cmdResetTab(parsed, _args) {
4978
+ if (parsed.help) {
4979
+ console.log(
4980
+ 'Usage: capture reset-tab <url> [--port <port>]\n\nAbandon a stuck/unresponsive tab and open a fresh one.\nUpdates session context so subsequent commands auto-target the new tab.\n\nExample: capture reset-tab "https://www.instagram.com/"'
4981
+ );
4982
+ process.exit(0);
4983
+ }
4984
+ const url = parsed.positional[0];
4985
+ if (!url) {
4986
+ console.error(
4987
+ "Usage: capture reset-tab <url> [--port <port>]"
4988
+ );
4989
+ process.exit(1);
4990
+ }
4991
+ const p = parsed.port ?? await detectCdpPort();
4992
+ const tab = await openTab(p, url);
4993
+ if (tab.webSocketDebuggerUrl) {
4994
+ const client = new CDPClient(tab.webSocketDebuggerUrl);
4995
+ await client.waitReady();
4996
+ await client.send("Page.enable");
4997
+ await new Promise((resolve3) => {
4998
+ const timer = setTimeout(() => resolve3(), 1e4);
4999
+ client.on("Page.loadEventFired", () => {
5000
+ clearTimeout(timer);
5001
+ resolve3();
5002
+ });
5003
+ });
5004
+ client.close();
5005
+ }
5006
+ updateActiveSession({ targetId: tab.id });
5007
+ console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
5008
+ console.error(`
5009
+ New tab opened. Session context updated.`);
5010
+ console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
5011
+ return;
5012
+ }
5013
+ var init_tabs = __esm({
5014
+ "src/cdp/commands/tabs.ts"() {
5015
+ init_detect();
5016
+ init_targets();
5017
+ init_record();
5018
+ init_client();
5019
+ init_session_context();
5020
+ }
5021
+ });
5022
+
5023
+ // src/har-manager.ts
5024
+ function harFilePath(id) {
5025
+ return path3.join(HAR_DIR, `${id}.json`);
5026
+ }
5027
+ function createHarRecording() {
5028
+ const id = Math.random().toString(36).slice(2, 8);
5029
+ const harPath = harFilePath(id);
5030
+ fs5.mkdirSync(HAR_DIR, { recursive: true });
5031
+ fs5.writeFileSync(harPath, JSON.stringify({ log: { entries: [] } }, null, 2));
5032
+ return { id, path: harPath };
5033
+ }
5034
+ function readHarRecording(id) {
5035
+ try {
5036
+ return JSON.parse(fs5.readFileSync(harFilePath(id), "utf-8"));
5037
+ } catch {
5038
+ return null;
5039
+ }
5040
+ }
5041
+ function appendToHarRecording(id, entries) {
5042
+ const harPath = harFilePath(id);
5043
+ let har;
5044
+ try {
5045
+ har = JSON.parse(fs5.readFileSync(harPath, "utf-8"));
5046
+ } catch {
5047
+ har = { log: { entries: [] } };
5048
+ }
5049
+ har.log.entries.push(...entries);
5050
+ fs5.writeFileSync(harPath, JSON.stringify(har, null, 2));
5051
+ }
5052
+ function deleteHarRecording(id) {
5053
+ try {
5054
+ fs5.unlinkSync(harFilePath(id));
5055
+ } catch {
5056
+ }
5057
+ }
5058
+ var fs5, path3, HAR_DIR;
5059
+ var init_har_manager = __esm({
5060
+ "src/har-manager.ts"() {
5061
+ fs5 = __toESM(require("fs"));
5062
+ path3 = __toESM(require("path"));
5063
+ HAR_DIR = "/tmp/capture-har";
5064
+ }
5065
+ });
5066
+
5067
+ // src/cdp/connection.ts
5068
+ async function connectForCommand(parsed) {
5069
+ if (!parsed.target) {
5070
+ throw new Error('Use --target <tabId> to target a tab. Run "capture list" to see available tabs.');
5071
+ }
5072
+ let tab = null;
5073
+ if (parsed.port) {
5074
+ tab = await findTabById(parsed.port, parsed.target);
5075
+ } else {
5076
+ const endpoints = await detectCdpPortsAsync();
5077
+ for (const ep of endpoints) {
5078
+ try {
5079
+ tab = await findTabById(ep.port, parsed.target);
5080
+ if (tab) break;
5081
+ } catch {
5387
5082
  }
5388
- finishPartial() {
5389
- return this.buildHar();
5083
+ }
5084
+ }
5085
+ if (!tab) {
5086
+ const query = parsed.target;
5087
+ throw new Error(
5088
+ `No tab found for target "${query}". Run "capture list" to see available tabs.`
5089
+ );
5090
+ }
5091
+ if (!tab.webSocketDebuggerUrl) {
5092
+ throw new Error("Tab has no WebSocket debugger URL");
5093
+ }
5094
+ const activeSession = getActiveSession();
5095
+ if (activeSession && !activeSession.targetId) {
5096
+ updateActiveSession({ targetId: tab.id });
5097
+ }
5098
+ const client = new CDPClient(tab.webSocketDebuggerUrl);
5099
+ await client.waitReady();
5100
+ return { client, tab };
5101
+ }
5102
+ async function withConnection(parsed, fn, opts = {}) {
5103
+ const { client, tab } = await connectForCommand(parsed);
5104
+ const consoleRecorder = new ConsoleRecorder(client);
5105
+ await consoleRecorder.start();
5106
+ let harRecorder;
5107
+ if (parsed.har) {
5108
+ const harPath = harFilePath(parsed.har);
5109
+ if (!fs6.existsSync(harPath)) {
5110
+ console.error(
5111
+ `ERROR: No HAR recording found for --har "${parsed.har}". Run 'har create' first.`
5112
+ );
5113
+ process.exit(1);
5114
+ }
5115
+ harRecorder = new HARRecorder(client);
5116
+ await harRecorder.start();
5117
+ }
5118
+ try {
5119
+ const result = await fn(client, tab);
5120
+ if (harRecorder && parsed.har) {
5121
+ const settle = opts.settle !== void 0 ? opts.settle : 3e3;
5122
+ if (settle > 0) {
5123
+ await new Promise((r) => setTimeout(r, settle));
5390
5124
  }
5391
- };
5392
- ConsoleRecorder = class {
5393
- constructor(client) {
5394
- this.client = client;
5125
+ const har = await harRecorder.finish();
5126
+ if (har.log.entries.length > 0) {
5127
+ appendToHarRecording(parsed.har, har.log.entries);
5128
+ console.error(
5129
+ ` [har:${parsed.har}] +${har.log.entries.length} entries`
5130
+ );
5395
5131
  }
5396
- entries = [];
5397
- async start() {
5398
- await this.client.send("Runtime.enable");
5399
- this.client.on("Runtime.consoleAPICalled", (params) => {
5400
- const p = params;
5401
- const text = p.args.map(
5402
- (a) => a.value !== void 0 ? String(a.value) : a.description ?? ""
5403
- ).join(" ");
5404
- this.entries.push({
5405
- level: p.type,
5406
- text,
5407
- timestamp: p.timestamp
5408
- });
5409
- });
5410
- this.client.on("Runtime.exceptionThrown", (params) => {
5411
- const p = params;
5412
- this.entries.push({
5413
- level: "error",
5414
- text: p.exceptionDetails.exception?.description ?? p.exceptionDetails.text ?? "Unknown exception",
5415
- timestamp: p.timestamp,
5416
- source: p.exceptionDetails.url ? `${p.exceptionDetails.url}:${p.exceptionDetails.lineNumber}` : void 0
5417
- });
5418
- });
5132
+ }
5133
+ printConsoleSummary(consoleRecorder.finish());
5134
+ return result;
5135
+ } finally {
5136
+ client.close();
5137
+ }
5138
+ }
5139
+ var fs6;
5140
+ var init_connection = __esm({
5141
+ "src/cdp/connection.ts"() {
5142
+ fs6 = __toESM(require("fs"));
5143
+ init_client();
5144
+ init_targets();
5145
+ init_detect();
5146
+ init_console_recorder();
5147
+ init_har_recorder();
5148
+ init_har_manager();
5149
+ init_session_context();
5150
+ }
5151
+ });
5152
+
5153
+ // src/cdp/commands/exec.ts
5154
+ async function cmdExec(parsed, _args) {
5155
+ if (parsed.help) {
5156
+ console.log(
5157
+ `Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]
5158
+
5159
+ Execute JavaScript in a browser tab. Supports await (wrapped in async IIFE).
5160
+
5161
+ Examples:
5162
+ capture exec "document.title" --target <id>
5163
+ capture exec "await fetch('/api/data').then(r=>r.json())" --target <id>
5164
+ capture exec "document.querySelector('.btn').click()" --target <id>
5165
+ capture exec --file /tmp/scrape.js --target <id> --record`
5166
+ );
5167
+ process.exit(0);
5168
+ }
5169
+ let code;
5170
+ if (parsed.file) {
5171
+ code = fs7.readFileSync(parsed.file, "utf-8");
5172
+ } else {
5173
+ code = parsed.positional[0];
5174
+ if (!code) {
5175
+ console.error(
5176
+ `ERROR: Missing code to execute.
5177
+
5178
+ Usage: capture exec <code> [--target <id>] [--record] [--file <path>]
5179
+
5180
+ Examples:
5181
+ capture exec "document.title" --target <id>
5182
+ capture exec "document.querySelector('.btn').click()" --target <id>
5183
+ capture exec --file /tmp/scrape.js --target <id> --record`
5184
+ );
5185
+ process.exit(1);
5186
+ }
5187
+ }
5188
+ await withConnection(
5189
+ parsed,
5190
+ async (client) => {
5191
+ await client.send("Emulation.setFocusEmulationEnabled", { enabled: true });
5192
+ let standaloneRecorder;
5193
+ if (parsed.record) {
5194
+ standaloneRecorder = new HARRecorder(client);
5195
+ await standaloneRecorder.start();
5196
+ }
5197
+ const needsAsyncWrap = /\bawait\b/.test(code);
5198
+ const expression = needsAsyncWrap ? `(async () => { ${code} })()` : code;
5199
+ const evalResult = await client.send("Runtime.evaluate", {
5200
+ expression,
5201
+ awaitPromise: true,
5202
+ returnByValue: true
5203
+ });
5204
+ if (standaloneRecorder) {
5205
+ const har = await standaloneRecorder.finish();
5206
+ writeHarAndPrintSummary(har, parsed.harOut);
5419
5207
  }
5420
- finish() {
5421
- return [...this.entries];
5208
+ if (evalResult.exceptionDetails) {
5209
+ console.error(
5210
+ `ERROR: ${evalResult.exceptionDetails.exception?.description ?? "Unknown error"}`
5211
+ );
5212
+ process.exit(1);
5422
5213
  }
5423
- };
5424
- INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
5425
- "button",
5426
- "link",
5427
- "textbox",
5428
- "searchbox",
5429
- "combobox",
5430
- "checkbox",
5431
- "radio",
5432
- "menuitem",
5433
- "tab",
5434
- "switch",
5435
- "slider",
5436
- "spinbutton",
5437
- "option",
5438
- "menuitemcheckbox",
5439
- "menuitemradio"
5440
- ]);
5441
- BROWSER_PATTERNS = {
5442
- "company.thebrowser.browser": ["arc"],
5443
- "com.google.chrome": ["chrome", "google chrome"],
5444
- "com.brave.browser": ["brave"],
5445
- "com.microsoft.edgemac": ["edge", "microsoft edge"],
5446
- "org.chromium.chromium": ["chromium"]
5447
- };
5448
- CDPClient = class {
5449
- ws;
5450
- messageId = 0;
5451
- pendingRequests = /* @__PURE__ */ new Map();
5452
- pendingTimeouts = /* @__PURE__ */ new Map();
5453
- eventHandlers = /* @__PURE__ */ new Map();
5454
- ready;
5455
- constructor(wsUrl, connectTimeout = 5e3) {
5456
- this.ws = new wrapper_default(wsUrl);
5457
- this.ready = new Promise((resolve2, reject) => {
5458
- const timeout = setTimeout(() => {
5459
- reject(new Error(`WebSocket connection timeout (${connectTimeout}ms)`));
5460
- }, connectTimeout);
5461
- this.ws.on("open", () => {
5462
- clearTimeout(timeout);
5463
- resolve2();
5464
- });
5465
- this.ws.on("error", (err) => {
5466
- clearTimeout(timeout);
5467
- reject(err);
5468
- });
5469
- });
5470
- this.ws.on("message", (data) => {
5471
- const msg = JSON.parse(data.toString());
5472
- if (msg.id !== void 0) {
5473
- const pending = this.pendingRequests.get(msg.id);
5474
- if (pending) {
5475
- this.pendingRequests.delete(msg.id);
5476
- const timer = this.pendingTimeouts.get(msg.id);
5477
- if (timer) {
5478
- clearTimeout(timer);
5479
- this.pendingTimeouts.delete(msg.id);
5480
- }
5481
- if (msg.error) {
5482
- pending.reject(new Error(msg.error.message));
5483
- } else {
5484
- pending.resolve(msg.result);
5485
- }
5486
- }
5487
- } else if (msg.method) {
5488
- const handlers = this.eventHandlers.get(msg.method);
5489
- handlers?.forEach((h) => h(msg.params));
5490
- }
5491
- });
5214
+ console.log(JSON.stringify(evalResult.result?.value));
5215
+ },
5216
+ { settle: 3e3 }
5217
+ );
5218
+ return;
5219
+ }
5220
+ var fs7;
5221
+ var init_exec = __esm({
5222
+ "src/cdp/commands/exec.ts"() {
5223
+ fs7 = __toESM(require("fs"));
5224
+ init_connection();
5225
+ init_har_recorder();
5226
+ init_har_output();
5227
+ }
5228
+ });
5229
+
5230
+ // src/interact.ts
5231
+ async function clickByName(client, name, role) {
5232
+ await client.send("Accessibility.enable");
5233
+ await client.send("DOM.enable");
5234
+ const { nodes } = await client.send("Accessibility.getFullAXTree");
5235
+ await client.send("Accessibility.disable");
5236
+ const nameLower = name.toLowerCase();
5237
+ const matches = nodes.filter((n) => {
5238
+ if (!n.backendDOMNodeId) return false;
5239
+ if (!n.name?.value) return false;
5240
+ if (n.name.value.toLowerCase() !== nameLower) return false;
5241
+ if (role && n.role?.value !== role) return false;
5242
+ return true;
5243
+ });
5244
+ if (matches.length === 0) {
5245
+ throw new Error(
5246
+ `No element found with accessible name "${name}"${role ? ` and role "${role}"` : ""}. Run \`capture a11y --interactive\` to see available elements.`
5247
+ );
5248
+ }
5249
+ if (matches.length > 1 && !role) {
5250
+ const INPUT_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "textarea", "spinbutton"]);
5251
+ const inputMatches = matches.filter((n) => n.role?.value && INPUT_ROLES.has(n.role.value));
5252
+ if (inputMatches.length === 1) {
5253
+ matches.length = 0;
5254
+ matches.push(inputMatches[0]);
5255
+ } else {
5256
+ const summary = matches.slice(0, 5).map((n) => ` ${n.role?.value} "${n.name?.value}"`).join("\n");
5257
+ throw new Error(
5258
+ `Multiple elements match "${name}" \u2014 disambiguate with --role:
5259
+ ${summary}`
5260
+ );
5261
+ }
5262
+ }
5263
+ const match = matches[0];
5264
+ const backendNodeId = match.backendDOMNodeId;
5265
+ await client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
5266
+ const { model } = await client.send("DOM.getBoxModel", { backendNodeId });
5267
+ const quad = model.content;
5268
+ const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
5269
+ const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
5270
+ await client.send("Input.dispatchMouseEvent", {
5271
+ type: "mousePressed",
5272
+ x,
5273
+ y,
5274
+ button: "left",
5275
+ clickCount: 1
5276
+ });
5277
+ await client.send("Input.dispatchMouseEvent", {
5278
+ type: "mouseReleased",
5279
+ x,
5280
+ y,
5281
+ button: "left",
5282
+ clickCount: 1
5283
+ });
5284
+ return {
5285
+ x: Math.round(x),
5286
+ y: Math.round(y),
5287
+ role: match.role?.value ?? "unknown",
5288
+ name: match.name?.value ?? name
5289
+ };
5290
+ }
5291
+ async function typeText(client, text) {
5292
+ await client.send("Input.insertText", { text });
5293
+ }
5294
+ async function focusAndType(client, fieldName, text, role) {
5295
+ const result = await clickByName(client, fieldName, role);
5296
+ await new Promise((r) => setTimeout(r, 100));
5297
+ await typeText(client, text);
5298
+ return result;
5299
+ }
5300
+ var init_interact = __esm({
5301
+ "src/interact.ts"() {
5302
+ }
5303
+ });
5304
+
5305
+ // src/cdp/commands/ui.ts
5306
+ async function cmdScreenshot(parsed, _args) {
5307
+ if (parsed.help) {
5308
+ console.log(
5309
+ "Usage: capture screenshot [--target <id>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\nCapture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\nOptions:\n --viewport <preset> Viewport preset (default: desktop)\n desktop-wide 1920x1080\n desktop 1280x800\n tablet 768x1024\n mobile 390x844\n --height <px> Override viewport height (e.g. 1600)\n --full-page Capture the entire scrollable page"
5310
+ );
5311
+ process.exit(0);
5312
+ }
5313
+ const VIEWPORTS = {
5314
+ "desktop-wide": { width: 1920, height: 1080 },
5315
+ "desktop": { width: 1280, height: 800 },
5316
+ "tablet": { width: 768, height: 1024 },
5317
+ "mobile": { width: 390, height: 844 }
5318
+ };
5319
+ const viewportName = parsed.viewport ?? "desktop";
5320
+ const viewport = VIEWPORTS[viewportName];
5321
+ if (!viewport) {
5322
+ console.error(`Unknown viewport "${viewportName}". Options: ${Object.keys(VIEWPORTS).join(", ")}`);
5323
+ process.exit(1);
5324
+ }
5325
+ if (parsed.height) {
5326
+ viewport.height = parsed.height;
5327
+ }
5328
+ const result = await withConnection(
5329
+ parsed,
5330
+ async (client) => {
5331
+ const png = await captureScreenshot(client, viewport, { fullPage: parsed.fullPage });
5332
+ const outPath = parsed.out ? parsed.out : nextStepPath("screenshot", "manual") ?? `/tmp/capture-screenshot-${Date.now()}.png`;
5333
+ fs8.writeFileSync(outPath, png);
5334
+ return { path: outPath, bytes: png.length };
5335
+ },
5336
+ { settle: 0 }
5337
+ );
5338
+ console.log(JSON.stringify(result, null, 2));
5339
+ return;
5340
+ }
5341
+ async function cmdClick(parsed, _args) {
5342
+ if (parsed.help) {
5343
+ console.log(
5344
+ 'Usage: capture click "name" [--role button|link|...] [--no-screenshot]\n\nClick an element by its accessible name. Auto-captures a screenshot after clicking.'
5345
+ );
5346
+ process.exit(0);
5347
+ }
5348
+ const name = parsed.positional[0];
5349
+ if (!name) {
5350
+ console.error('Usage: capture click "name" [--role button|link|...] [--no-screenshot]');
5351
+ process.exit(1);
5352
+ }
5353
+ const clickSettle = parsed.settle !== void 0 ? parsed.settle : parsed.har ? 2500 : 1e3;
5354
+ const result = await withConnection(
5355
+ parsed,
5356
+ async (client) => {
5357
+ const clickResult = await clickByName(client, name, parsed.role);
5358
+ const screenshot = await autoScreenshot(client, "click", name, parsed.noScreenshot);
5359
+ return { ...clickResult, screenshot };
5360
+ },
5361
+ { settle: clickSettle }
5362
+ );
5363
+ console.log(JSON.stringify(result, null, 2));
5364
+ return;
5365
+ }
5366
+ async function cmdType(parsed, _args) {
5367
+ if (parsed.help) {
5368
+ console.log(
5369
+ 'Usage: capture type "text" [--into "field name"] [--no-screenshot]\n\nType text into the focused element, or into a named field with --into.'
5370
+ );
5371
+ process.exit(0);
5372
+ }
5373
+ const text = parsed.positional[0];
5374
+ if (!text) {
5375
+ console.error('Usage: capture type "text" [--into "field name"] [--no-screenshot]');
5376
+ process.exit(1);
5377
+ }
5378
+ const result = await withConnection(
5379
+ parsed,
5380
+ async (client) => {
5381
+ let field = null;
5382
+ if (parsed.into) {
5383
+ await focusAndType(client, parsed.into, text, parsed.role);
5384
+ field = parsed.into;
5385
+ } else {
5386
+ await typeText(client, text);
5492
5387
  }
5493
- async waitReady() {
5494
- await this.ready;
5388
+ const label = field ?? text;
5389
+ const screenshot = await autoScreenshot(client, "type", label, parsed.noScreenshot);
5390
+ return { typed: text, field, screenshot };
5391
+ },
5392
+ { settle: parsed.settle !== void 0 ? parsed.settle : parsed.har ? 1500 : 500 }
5393
+ );
5394
+ console.log(JSON.stringify(result, null, 2));
5395
+ return;
5396
+ }
5397
+ async function cmdA11y(parsed, _args) {
5398
+ if (parsed.help) {
5399
+ console.log(
5400
+ "Usage: capture a11y [--target <id>] [--json] [--interactive]\n\nGet the accessibility tree. Use --interactive for interactive elements only.\nUse --json for structured JSON output."
5401
+ );
5402
+ process.exit(0);
5403
+ }
5404
+ const result = await withConnection(
5405
+ parsed,
5406
+ async (client) => {
5407
+ const tree = await getAccessibilityTree(client);
5408
+ if (parsed.json) {
5409
+ return tree;
5495
5410
  }
5496
- async send(method, params = {}, timeout = 6e4) {
5497
- await this.ready;
5498
- const id = ++this.messageId;
5499
- return new Promise((resolve2, reject) => {
5500
- this.pendingRequests.set(id, { resolve: resolve2, reject });
5501
- this.ws.send(JSON.stringify({ id, method, params }));
5502
- const timer = setTimeout(() => {
5503
- if (this.pendingRequests.has(id)) {
5504
- this.pendingRequests.delete(id);
5505
- this.pendingTimeouts.delete(id);
5506
- reject(new Error(`CDP request timeout (${timeout}ms): ${method}`));
5507
- }
5508
- }, timeout);
5509
- this.pendingTimeouts.set(id, timer);
5510
- });
5411
+ const lines = flattenA11yTree(tree, {
5412
+ interactive: parsed.interactive
5413
+ });
5414
+ return lines.join("\n");
5415
+ },
5416
+ { settle: 0 }
5417
+ );
5418
+ if (parsed.json) {
5419
+ console.log(JSON.stringify(result, null, 2));
5420
+ } else {
5421
+ console.log(result);
5422
+ }
5423
+ if (!parsed.json) {
5424
+ const lineCount = typeof result === "string" ? result.split("\n").filter(Boolean).length : 0;
5425
+ if (lineCount === 0) {
5426
+ console.error(
5427
+ "\nNo elements found." + (parsed.interactive ? " Try without --interactive to see all nodes." : "") + '\n\nIf this page is part of your project, consider adding ARIA attributes:\n - role="button|link|textbox" on interactive elements\n - aria-label="..." on elements without visible text\n - Semantic HTML (<button>, <input>, <nav>) provides roles automatically'
5428
+ );
5429
+ } else if (parsed.interactive && lineCount < 5) {
5430
+ console.error(
5431
+ `
5432
+ Only ${lineCount} interactive element${lineCount === 1 ? "" : "s"} found. If this seems low, the page may lack ARIA markup.
5433
+ Consider adding role and aria-label attributes to interactive elements in your frontend code.`
5434
+ );
5435
+ }
5436
+ }
5437
+ return;
5438
+ }
5439
+ var fs8;
5440
+ var init_ui = __esm({
5441
+ "src/cdp/commands/ui.ts"() {
5442
+ fs8 = __toESM(require("fs"));
5443
+ init_connection();
5444
+ init_screenshot();
5445
+ init_a11y();
5446
+ init_interact();
5447
+ init_session_context();
5448
+ }
5449
+ });
5450
+
5451
+ // src/cdp/commands/traffic.ts
5452
+ async function cmdRecord(parsed, _args) {
5453
+ if (parsed.help) {
5454
+ console.log(
5455
+ "Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]\n\nPassive HAR recording for the specified duration (default: 10s)."
5456
+ );
5457
+ process.exit(0);
5458
+ }
5459
+ if (!parsed.target) {
5460
+ console.error(
5461
+ "Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]"
5462
+ );
5463
+ process.exit(1);
5464
+ }
5465
+ const result = await recordTraffic({
5466
+ port: parsed.port,
5467
+ targetId: parsed.target,
5468
+ duration: parsed.duration,
5469
+ harOutPath: parsed.harOut
5470
+ });
5471
+ console.log(
5472
+ JSON.stringify(
5473
+ { entryCount: result.entryCount, harPath: result.harPath },
5474
+ null,
5475
+ 2
5476
+ )
5477
+ );
5478
+ return;
5479
+ }
5480
+ async function cmdNavigate(parsed, _args) {
5481
+ if (parsed.help) {
5482
+ console.log(
5483
+ 'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>] [--har <id>]\n\nNavigate to URL and record HAR. Appends to session HAR if --har is set.\n\nExample: capture navigate "https://app.example.com/dashboard" --settle 3000'
5484
+ );
5485
+ process.exit(0);
5486
+ }
5487
+ const url = parsed.positional[0];
5488
+ if (!url) {
5489
+ console.error(
5490
+ "Usage: capture navigate <url> [--har-out <path>] [--settle <ms>]"
5491
+ );
5492
+ process.exit(1);
5493
+ }
5494
+ const result = await navigateAndRecord({
5495
+ port: parsed.port,
5496
+ url,
5497
+ targetId: parsed.target,
5498
+ harOutPath: parsed.har ? void 0 : parsed.harOut,
5499
+ settle: parsed.settle
5500
+ });
5501
+ if (parsed.har && result.har.log.entries.length > 0) {
5502
+ appendToHarRecording(parsed.har, result.har.log.entries);
5503
+ console.error(` [har:${parsed.har}] +${result.har.log.entries.length} entries`);
5504
+ }
5505
+ console.log(
5506
+ JSON.stringify(
5507
+ {
5508
+ entryCount: result.entryCount,
5509
+ harPath: result.harPath,
5510
+ tabUrl: result.tab.url,
5511
+ timedOut: result.timedOut
5512
+ },
5513
+ null,
5514
+ 2
5515
+ )
5516
+ );
5517
+ return;
5518
+ }
5519
+ async function cmdNetwork(parsed, _args) {
5520
+ if (parsed.help) {
5521
+ console.log(
5522
+ 'Usage: capture network <offline|online> [--target <id>]\n\nToggle network connectivity for a tab. Use "offline" to simulate\nnetwork failure (kills WebSocket connections, blocks HTTP requests).\nUse "online" to restore connectivity.'
5523
+ );
5524
+ process.exit(0);
5525
+ }
5526
+ const mode = parsed.positional[0];
5527
+ if (!mode || !["offline", "online"].includes(mode)) {
5528
+ console.error("Usage: capture network <offline|online> [--target <id>]");
5529
+ process.exit(1);
5530
+ }
5531
+ const offline = mode === "offline";
5532
+ const result = await withConnection(
5533
+ parsed,
5534
+ async (client) => {
5535
+ await client.send("Network.enable");
5536
+ await client.send("Network.emulateNetworkConditions", {
5537
+ offline,
5538
+ latency: offline ? -1 : 0,
5539
+ downloadThroughput: offline ? 0 : -1,
5540
+ uploadThroughput: offline ? 0 : -1
5541
+ });
5542
+ return { network: mode, offline };
5543
+ },
5544
+ { settle: 0 }
5545
+ );
5546
+ console.log(JSON.stringify(result, null, 2));
5547
+ if (offline) {
5548
+ console.error("\nNetwork disabled. WebSocket connections will drop.");
5549
+ console.error("Restore with: capture network online");
5550
+ } else {
5551
+ console.error("\nNetwork restored.");
5552
+ }
5553
+ return;
5554
+ }
5555
+ var init_traffic = __esm({
5556
+ "src/cdp/commands/traffic.ts"() {
5557
+ init_record();
5558
+ init_connection();
5559
+ init_har_manager();
5560
+ }
5561
+ });
5562
+
5563
+ // src/cdp/commands/har.ts
5564
+ async function cmdHar(parsed, args) {
5565
+ if (parsed.help) {
5566
+ console.log(
5567
+ "Usage: capture har <create|read|delete>\n\n create Create a new HAR recording (returns id)\n read [id] Read accumulated HAR entries (id optional in active session)\n delete <id> Delete a HAR recording\n\nFilters (read):\n --filter-url <pattern> Substring or regex match on URL\n --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n --filter-method <method> HTTP method (GET, POST, ...)\n --limit <N> Return only the first N matching entries\n\nPass --har <id> to exec/navigate commands to append traffic to a recording."
5568
+ );
5569
+ process.exit(0);
5570
+ }
5571
+ const subcommand = args[1];
5572
+ if (subcommand === "create") {
5573
+ const { id, path: harPath } = createHarRecording();
5574
+ console.log(JSON.stringify({ id, path: harPath }, null, 2));
5575
+ console.error(
5576
+ `
5577
+ Pass --har ${id} to subsequent commands to record traffic.`
5578
+ );
5579
+ return;
5580
+ }
5581
+ if (subcommand === "read") {
5582
+ let id = args[2];
5583
+ if (id && id.startsWith("--")) id = void 0;
5584
+ if (!id) {
5585
+ const session = getActiveSession();
5586
+ if (session?.harId) {
5587
+ id = session.harId;
5588
+ } else {
5589
+ console.error("Usage: capture har read [id]\n ID is optional when a capture session is active.");
5590
+ process.exit(1);
5511
5591
  }
5512
- on(event, handler) {
5513
- if (!this.eventHandlers.has(event)) {
5514
- this.eventHandlers.set(event, []);
5515
- }
5516
- this.eventHandlers.get(event).push(handler);
5592
+ }
5593
+ const har = readHarRecording(id);
5594
+ if (!har) {
5595
+ console.error(`No HAR recording found for id: ${id}`);
5596
+ process.exit(1);
5597
+ }
5598
+ let entries = har.log.entries;
5599
+ const total = entries.length;
5600
+ const filters = [];
5601
+ if (parsed.filterUrl) {
5602
+ const pattern = parsed.filterUrl;
5603
+ let re = null;
5604
+ try {
5605
+ re = new RegExp(pattern, "i");
5606
+ } catch {
5607
+ re = null;
5517
5608
  }
5518
- close() {
5519
- for (const timer of this.pendingTimeouts.values()) {
5520
- clearTimeout(timer);
5609
+ entries = entries.filter(
5610
+ (e) => re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase())
5611
+ );
5612
+ filters.push(`url~${pattern}`);
5613
+ }
5614
+ if (parsed.filterStatus) {
5615
+ const s = parsed.filterStatus;
5616
+ let matcher;
5617
+ if (/^\d+-\d+$/.test(s)) {
5618
+ const [lo, hi] = s.split("-").map((n) => parseInt(n, 10));
5619
+ matcher = (c) => c >= lo && c <= hi;
5620
+ } else if (/^\d+$/.test(s)) {
5621
+ if (s.length < 3) {
5622
+ matcher = (c) => String(c).startsWith(s);
5623
+ } else {
5624
+ const n = parseInt(s, 10);
5625
+ matcher = (c) => c === n;
5521
5626
  }
5522
- this.pendingTimeouts.clear();
5523
- this.ws.close();
5627
+ } else {
5628
+ matcher = () => true;
5524
5629
  }
5525
- };
5526
- LOCK_DIR = os2.tmpdir();
5527
- LOCK_STALE_MS = 12e4;
5528
- LOCK_TIMEOUT_MS = 6e4;
5529
- LOCK_POLL_BASE_MS = 200;
5530
- heldLocks = /* @__PURE__ */ new Set();
5531
- cleanupRegistered = false;
5630
+ entries = entries.filter((e) => matcher(e.response.status));
5631
+ filters.push(`status=${s}`);
5632
+ }
5633
+ if (parsed.filterMethod) {
5634
+ const m = parsed.filterMethod;
5635
+ entries = entries.filter((e) => e.request.method.toUpperCase() === m);
5636
+ filters.push(`method=${m}`);
5637
+ }
5638
+ if (typeof parsed.limit === "number" && parsed.limit > 0) {
5639
+ entries = entries.slice(0, parsed.limit);
5640
+ filters.push(`limit=${parsed.limit}`);
5641
+ }
5642
+ const filtersLabel = filters.length > 0 ? ` [${filters.join(", ")}]` : "";
5643
+ console.error(
5644
+ `HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`
5645
+ );
5646
+ console.log(JSON.stringify({ log: { entries } }, null, 2));
5647
+ return;
5648
+ }
5649
+ if (subcommand === "delete") {
5650
+ const id = args[2];
5651
+ if (!id) {
5652
+ console.error("Usage: capture har delete <id>");
5653
+ process.exit(1);
5654
+ }
5655
+ deleteHarRecording(id);
5656
+ console.log(JSON.stringify({ deleted: true, id }, null, 2));
5657
+ return;
5658
+ }
5659
+ console.error(
5660
+ "Usage: capture har <create|read|delete>\n create Create a new HAR recording (returns id)\n read <id> Read accumulated HAR entries\n delete <id> Delete a HAR recording"
5661
+ );
5662
+ process.exit(1);
5663
+ }
5664
+ var init_har = __esm({
5665
+ "src/cdp/commands/har.ts"() {
5666
+ init_har_manager();
5667
+ init_session_context();
5668
+ }
5669
+ });
5670
+
5671
+ // src/cdp/dispatch.ts
5672
+ async function cdpMain2() {
5673
+ const args = process.argv.slice(2);
5674
+ const parsed = parseCliArgs(args);
5675
+ switch (parsed.command) {
5676
+ case "detect":
5677
+ return cmdDetect(parsed, args);
5678
+ case "list":
5679
+ return cmdList(parsed, args);
5680
+ case "open":
5681
+ return cmdOpen(parsed, args);
5682
+ case "reset-tab":
5683
+ return cmdResetTab(parsed, args);
5684
+ case "exec":
5685
+ return cmdExec(parsed, args);
5686
+ case "screenshot":
5687
+ return cmdScreenshot(parsed, args);
5688
+ case "click":
5689
+ return cmdClick(parsed, args);
5690
+ case "type":
5691
+ return cmdType(parsed, args);
5692
+ case "a11y":
5693
+ return cmdA11y(parsed, args);
5694
+ case "record":
5695
+ return cmdRecord(parsed, args);
5696
+ case "navigate":
5697
+ return cmdNavigate(parsed, args);
5698
+ case "network":
5699
+ return cmdNetwork(parsed, args);
5700
+ case "har":
5701
+ return cmdHar(parsed, args);
5702
+ default:
5703
+ console.log(CDP_USAGE);
5704
+ }
5705
+ }
5706
+ var init_dispatch = __esm({
5707
+ "src/cdp/dispatch.ts"() {
5708
+ init_args();
5709
+ init_usage();
5710
+ init_tabs();
5711
+ init_exec();
5712
+ init_ui();
5713
+ init_traffic();
5714
+ init_har();
5715
+ }
5716
+ });
5717
+
5718
+ // src/cdp.ts
5719
+ var cdp_exports = {};
5720
+ __export(cdp_exports, {
5721
+ CDPClient: () => CDPClient,
5722
+ ConsoleRecorder: () => ConsoleRecorder,
5723
+ HARRecorder: () => HARRecorder,
5724
+ acquireTabLock: () => acquireTabLock,
5725
+ captureScreenshot: () => captureScreenshot,
5726
+ cdpMain: () => cdpMain2,
5727
+ detectCdpPort: () => detectCdpPort,
5728
+ executeInBrowser: () => executeInBrowser,
5729
+ findTab: () => findTab,
5730
+ findTabById: () => findTabById,
5731
+ getAccessibilityTree: () => getAccessibilityTree,
5732
+ isTabLocked: () => isTabLocked,
5733
+ listTargets: () => listTargets,
5734
+ navigateAndRecord: () => navigateAndRecord,
5735
+ navigateAndWait: () => navigateAndWait,
5736
+ openTab: () => openTab,
5737
+ recordTraffic: () => recordTraffic,
5738
+ releaseTabLock: () => releaseTabLock,
5739
+ withTabLock: () => withTabLock
5740
+ });
5741
+ var isMainModule;
5742
+ var init_cdp = __esm({
5743
+ "src/cdp.ts"() {
5744
+ init_client();
5745
+ init_detect();
5746
+ init_targets();
5747
+ init_screenshot();
5748
+ init_a11y();
5749
+ init_execute();
5750
+ init_record();
5751
+ init_har_recorder();
5752
+ init_console_recorder();
5753
+ init_tab_lock();
5754
+ init_dispatch();
5532
5755
  isMainModule = process.argv[1]?.endsWith("cdp.ts") || process.argv[1]?.endsWith("cdp.mjs");
5533
5756
  if (isMainModule) {
5534
- main().then(() => {
5757
+ cdpMain().then(() => {
5535
5758
  if (process.stdout.writableNeedDrain) {
5536
5759
  process.stdout.once("drain", () => process.exit(0));
5537
5760
  } else {
@@ -5546,12 +5769,16 @@ var init_cdp = __esm({
5546
5769
  });
5547
5770
 
5548
5771
  // src/capture.ts
5549
- var fs4 = __toESM(require("fs"));
5772
+ var fs10 = __toESM(require("fs"));
5773
+ var path5 = __toESM(require("path"));
5774
+ init_cdp();
5775
+
5776
+ // src/session/commands.ts
5777
+ var fs9 = __toESM(require("fs"));
5550
5778
  var path4 = __toESM(require("path"));
5551
5779
  var os3 = __toESM(require("os"));
5552
5780
  var import_child_process2 = require("child_process");
5553
5781
  init_har_manager();
5554
- init_cdp();
5555
5782
  init_session_context();
5556
5783
  var CAPTURE_ROOT2 = path4.join(os3.tmpdir(), "capture-sessions");
5557
5784
  function sessionDir(id) {
@@ -5562,10 +5789,10 @@ function sessionMetaPath(id) {
5562
5789
  }
5563
5790
  function readSession(id) {
5564
5791
  const metaPath = sessionMetaPath(id);
5565
- if (!fs4.existsSync(metaPath)) {
5792
+ if (!fs9.existsSync(metaPath)) {
5566
5793
  throw new Error(`No capture session found: ${id}`);
5567
5794
  }
5568
- return JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
5795
+ return JSON.parse(fs9.readFileSync(metaPath, "utf-8"));
5569
5796
  }
5570
5797
  function generateId() {
5571
5798
  return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
@@ -5579,8 +5806,8 @@ async function start(args) {
5579
5806
  }
5580
5807
  const id = generateId();
5581
5808
  const dir = sessionDir(id);
5582
- fs4.mkdirSync(path4.join(dir, "shots"), { recursive: true });
5583
- fs4.mkdirSync(path4.join(dir, "a11y"), { recursive: true });
5809
+ fs9.mkdirSync(path4.join(dir, "shots"), { recursive: true });
5810
+ fs9.mkdirSync(path4.join(dir, "a11y"), { recursive: true });
5584
5811
  let harId = null;
5585
5812
  try {
5586
5813
  const harResult = createHarRecording();
@@ -5609,7 +5836,7 @@ async function start(args) {
5609
5836
  stepCount: 0,
5610
5837
  logPids: []
5611
5838
  };
5612
- fs4.writeFileSync(sessionMetaPath(id), JSON.stringify(session, null, 2));
5839
+ fs9.writeFileSync(sessionMetaPath(id), JSON.stringify(session, null, 2));
5613
5840
  setActiveSession({
5614
5841
  sessionId: id,
5615
5842
  dir,
@@ -5641,7 +5868,7 @@ function logCommand(args) {
5641
5868
  process.exit(1);
5642
5869
  }
5643
5870
  const resolved = path4.resolve(sourcePath);
5644
- if (!fs4.existsSync(resolved)) {
5871
+ if (!fs9.existsSync(resolved)) {
5645
5872
  throw new Error(`Log file not found: ${resolved}`);
5646
5873
  }
5647
5874
  let name = null;
@@ -5660,19 +5887,19 @@ function logCommand(args) {
5660
5887
  const session = readSession(sessionId);
5661
5888
  name = name ?? path4.basename(resolved, path4.extname(resolved));
5662
5889
  const logsDir = path4.join(session.dir, "logs");
5663
- fs4.mkdirSync(logsDir, { recursive: true });
5890
+ fs9.mkdirSync(logsDir, { recursive: true });
5664
5891
  const destPath = path4.join(logsDir, `${name}.log`);
5665
- const outFd = fs4.openSync(destPath, "a");
5892
+ const outFd = fs9.openSync(destPath, "a");
5666
5893
  const child = (0, import_child_process2.spawn)(
5667
5894
  "sh",
5668
5895
  ["-c", `tail -f "${resolved}" | perl -MPOSIX -ne 'print strftime("%Y-%m-%dT%H:%M:%SZ",gmtime())." ".$_'`],
5669
5896
  { detached: true, stdio: ["ignore", outFd, "ignore"] }
5670
5897
  );
5671
5898
  child.unref();
5672
- fs4.closeSync(outFd);
5899
+ fs9.closeSync(outFd);
5673
5900
  const pid = child.pid;
5674
5901
  session.logPids.push({ pid, name, sourcePath: resolved });
5675
- fs4.writeFileSync(sessionMetaPath(session.id), JSON.stringify(session, null, 2));
5902
+ fs9.writeFileSync(sessionMetaPath(session.id), JSON.stringify(session, null, 2));
5676
5903
  console.log(JSON.stringify({ name, sourcePath: resolved, destPath, pid }, null, 2));
5677
5904
  }
5678
5905
  async function stop(args) {
@@ -5695,16 +5922,16 @@ async function stop(args) {
5695
5922
  await new Promise((r) => setTimeout(r, 200));
5696
5923
  }
5697
5924
  const shotsDir = path4.join(session.dir, "shots");
5698
- const screenshots = fs4.existsSync(shotsDir) ? fs4.readdirSync(shotsDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg")).map((f) => ({ name: f, path: path4.join(shotsDir, f) })) : [];
5925
+ const screenshots = fs9.existsSync(shotsDir) ? fs9.readdirSync(shotsDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg")).map((f) => ({ name: f, path: path4.join(shotsDir, f) })) : [];
5699
5926
  const a11yDir = path4.join(session.dir, "a11y");
5700
- const a11y = fs4.existsSync(a11yDir) ? fs4.readdirSync(a11yDir).filter((f) => f.endsWith(".json") || f.endsWith(".txt")).map((f) => ({ name: f, path: path4.join(a11yDir, f) })) : [];
5927
+ const a11y = fs9.existsSync(a11yDir) ? fs9.readdirSync(a11yDir).filter((f) => f.endsWith(".json") || f.endsWith(".txt")).map((f) => ({ name: f, path: path4.join(a11yDir, f) })) : [];
5701
5928
  let har = null;
5702
5929
  if (session.harId) {
5703
5930
  try {
5704
5931
  const harData = readHarRecording(session.harId);
5705
5932
  if (harData) {
5706
5933
  const harPath = path4.join(session.dir, "har.json");
5707
- fs4.writeFileSync(harPath, JSON.stringify(harData, null, 2));
5934
+ fs9.writeFileSync(harPath, JSON.stringify(harData, null, 2));
5708
5935
  har = { id: session.harId, path: harPath, entryCount: harData.log.entries.length };
5709
5936
  try {
5710
5937
  deleteHarRecording(session.harId);
@@ -5716,15 +5943,15 @@ async function stop(args) {
5716
5943
  }
5717
5944
  }
5718
5945
  const logsDir = path4.join(session.dir, "logs");
5719
- const logs = fs4.existsSync(logsDir) ? fs4.readdirSync(logsDir).filter((f) => f.endsWith(".log")).map((f) => {
5946
+ const logs = fs9.existsSync(logsDir) ? fs9.readdirSync(logsDir).filter((f) => f.endsWith(".log")).map((f) => {
5720
5947
  const filePath = path4.join(logsDir, f);
5721
- const content = fs4.readFileSync(filePath, "utf-8");
5948
+ const content = fs9.readFileSync(filePath, "utf-8");
5722
5949
  const lines = content ? content.split("\n").filter(Boolean).length : 0;
5723
5950
  return { name: f, path: filePath, lines };
5724
5951
  }) : [];
5725
5952
  const knownDirs = /* @__PURE__ */ new Set(["shots", "a11y", "logs"]);
5726
5953
  const knownFiles = /* @__PURE__ */ new Set([".session.json", "har.json", "bundle.json"]);
5727
- const other = fs4.readdirSync(session.dir).filter((f) => !knownDirs.has(f) && !knownFiles.has(f)).map((f) => ({ name: f, path: path4.join(session.dir, f) }));
5954
+ const other = fs9.readdirSync(session.dir).filter((f) => !knownDirs.has(f) && !knownFiles.has(f)).map((f) => ({ name: f, path: path4.join(session.dir, f) }));
5728
5955
  clearActiveSession();
5729
5956
  const manifest = {
5730
5957
  id: session.id,
@@ -5739,7 +5966,7 @@ async function stop(args) {
5739
5966
  other
5740
5967
  };
5741
5968
  const bundlePath = path4.join(session.dir, "bundle.json");
5742
- fs4.writeFileSync(bundlePath, JSON.stringify(manifest, null, 2));
5969
+ fs9.writeFileSync(bundlePath, JSON.stringify(manifest, null, 2));
5743
5970
  console.log(JSON.stringify({
5744
5971
  bundlePath,
5745
5972
  summary: {
@@ -5756,13 +5983,13 @@ Bundle written: ${bundlePath}`);
5756
5983
  console.error(`Read it: capture session view ${id}`);
5757
5984
  }
5758
5985
  function list() {
5759
- if (!fs4.existsSync(CAPTURE_ROOT2)) {
5986
+ if (!fs9.existsSync(CAPTURE_ROOT2)) {
5760
5987
  console.log("[]");
5761
5988
  return;
5762
5989
  }
5763
- const sessions = fs4.readdirSync(CAPTURE_ROOT2).filter((d) => fs4.existsSync(sessionMetaPath(d))).map((d) => {
5990
+ const sessions = fs9.readdirSync(CAPTURE_ROOT2).filter((d) => fs9.existsSync(sessionMetaPath(d))).map((d) => {
5764
5991
  const session = readSession(d);
5765
- const hasBundled = fs4.existsSync(path4.join(session.dir, "bundle.json"));
5992
+ const hasBundled = fs9.existsSync(path4.join(session.dir, "bundle.json"));
5766
5993
  return { id: session.id, startedAt: session.startedAt, url: session.url, status: hasBundled ? "stopped" : "active" };
5767
5994
  });
5768
5995
  console.log(JSON.stringify(sessions, null, 2));
@@ -5775,11 +6002,11 @@ function view(args) {
5775
6002
  }
5776
6003
  const session = readSession(id);
5777
6004
  const bundlePath = path4.join(session.dir, "bundle.json");
5778
- if (!fs4.existsSync(bundlePath)) {
6005
+ if (!fs9.existsSync(bundlePath)) {
5779
6006
  console.error(`Session ${id} hasn't been stopped yet. Run: capture session stop ${id}`);
5780
6007
  process.exit(1);
5781
6008
  }
5782
- const manifest = JSON.parse(fs4.readFileSync(bundlePath, "utf-8"));
6009
+ const manifest = JSON.parse(fs9.readFileSync(bundlePath, "utf-8"));
5783
6010
  const filter = args.find((_, i) => args[i - 1] === "--filter");
5784
6011
  if (filter) {
5785
6012
  const section = manifest[filter];
@@ -5802,34 +6029,44 @@ async function sessionMain(args) {
5802
6029
  default:
5803
6030
  console.log(`capture session \u2014 manage capture sessions
5804
6031
 
5805
- Commands:
5806
- start [--url <url>] Start a capture session
5807
- stop <session-id> Finalize and bundle all artifacts
5808
- list List active and stopped sessions
5809
- view <id> [--filter section] View bundle manifest
5810
- log <path> [--name label] Tail a log file into the session
6032
+ Sub-commands:
6033
+ start [--url <url>] Start a session (opens tab, records HAR, sets active context)
6034
+ stop <session-id> Finalize and bundle artifacts (screenshots, HAR, a11y, logs)
6035
+ list List active and stopped sessions
6036
+ view <id> [--filter section] View bundle manifest; section = screenshots|har|a11y|logs
5811
6037
 
5812
- Workflow:
6038
+ Why sessions: once started, every subsequent capture command auto-fills
6039
+ --target (the tab) and --har (the recording). No manual flag threading.
6040
+
6041
+ Typical flow:
5813
6042
  1. capture session start --url http://localhost:3000
5814
- 2. Use CDP commands directly:
5815
- capture screenshot --url localhost --out $CAPTURE_DIR/shots/homepage.png --har $HAR_ID
5816
- capture exec "document.querySelector('.btn').click()" --url localhost --har $HAR_ID
5817
- capture a11y --url localhost --json > $CAPTURE_DIR/a11y/after-login.json
6043
+ 2. Interact \u2014 no --target / --har needed:
6044
+ capture a11y --interactive
6045
+ capture click "Sign in"
6046
+ capture type "hi@me.com" --into "Email"
6047
+ capture screenshot
6048
+ capture navigate https://app.example.com/dashboard
6049
+ capture har read --filter-url /api
5818
6050
  3. capture session stop <session-id>
5819
- 4. capture session view <session-id>`);
6051
+ 4. capture session view <session-id>
6052
+
6053
+ Related: capture log <path> [--name label] Tail a log into the active session
6054
+ See also: capture --help Full command list`);
5820
6055
  }
5821
6056
  }
5822
- async function main2() {
6057
+
6058
+ // src/capture.ts
6059
+ async function main() {
5823
6060
  const [command, ...args] = process.argv.slice(2);
5824
6061
  if (command === "--version" || command === "-v" || command === "version") {
5825
- const declared = "1.3.2";
6062
+ const declared = "1.3.4";
5826
6063
  if (declared) {
5827
6064
  console.log(declared);
5828
6065
  return;
5829
6066
  }
5830
6067
  try {
5831
- const pkgPath = path4.resolve(__dirname, "..", "package.json");
5832
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
6068
+ const pkgPath = path5.resolve(__dirname, "..", "package.json");
6069
+ const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
5833
6070
  console.log(pkg.version);
5834
6071
  } catch {
5835
6072
  console.log("unknown");
@@ -5855,60 +6092,78 @@ async function main2() {
5855
6092
  case "har":
5856
6093
  case "network":
5857
6094
  case "list":
5858
- return main();
6095
+ return cdpMain2();
5859
6096
  default:
5860
- console.log(`Capture \u2014 session management and CDP browser automation
6097
+ console.log(`Capture \u2014 browser automation over CDP for validating UI features.
5861
6098
 
5862
- Session commands:
5863
- session start [--url <url>] Start a capture session
5864
- session stop <session-id> Finalize and bundle all artifacts
5865
- session list List active and stopped sessions
5866
- session view <id> [--filter ...] View bundle manifest
5867
- log <path> [--name label] Tail a log file into the session
6099
+ Most tasks start with a session. Once a session is active, every command
6100
+ auto-fills --target (the tab) and --har (the recording) \u2014 don't thread
6101
+ those flags yourself.
5868
6102
 
5869
- CDP commands:
5870
- detect Detect CDP port
5871
- exec <code> Execute JS in a browser tab
5872
- exec --file <path> Execute JS from file
5873
- list List all browser tabs
5874
- open <url> Open URL in browser
5875
- reset-tab <url> Abandon stuck tab, open fresh one
5876
- screenshot Capture screenshot
5877
- click "name" Click element by accessible name
5878
- type "text" Type text into focused element
5879
- a11y Get accessibility tree
5880
- record Passive HAR recording
5881
- navigate <url> Navigate to URL and record HAR
5882
- network <offline|online> Toggle network (simulate disconnect)
5883
- har create|read|delete Manage HAR recordings
6103
+ TYPICAL WORKFLOW
5884
6104
 
5885
- Options (CDP):
5886
- --port <port> Override CDP port
5887
- --target <tabId> Target tab by exact ID (preferred, parallel-safe)
5888
- --url <pattern> Target tab by URL pattern (fuzzy match)
5889
- --har <id> Append traffic to a HAR recording
5890
- --out <path> Output path (screenshot)
5891
- --json JSON output (a11y)
5892
- --interactive Interactive elements only (a11y)
5893
- --role <role> ARIA role filter (click)
5894
- --into "field" Target field by name (type)
5895
- --no-screenshot Skip auto-screenshot (click, type)
5896
- --filter-url <pat> Filter HAR by URL substring/regex (har read)
5897
- --filter-status <s> Filter HAR by status code, prefix, or range (har read)
5898
- --filter-method <m> Filter HAR by HTTP method (har read)
5899
- --limit <N> Limit HAR entries returned (har read)
6105
+ 1. Start a session (opens tab, starts HAR, marks it active):
6106
+ capture session start --url http://localhost:3000
5900
6107
 
5901
- Examples:
5902
- capture session start --url http://localhost:3000
5903
- capture session stop <session-id>
5904
- capture detect
5905
- capture list
5906
- capture exec "document.title" --url example
5907
- capture screenshot --url example --out /tmp/shot.png
5908
- capture a11y --url example --interactive`);
6108
+ 2. Interact \u2014 no --target / --har needed:
6109
+ capture a11y --interactive See what's on the page
6110
+ capture click "Sign in" Click by accessible name (auto-screenshots)
6111
+ capture type "hi@me.com" --into "Email"
6112
+ capture screenshot Save current state
6113
+ capture navigate https://... Navigate within the session
6114
+ capture exec "document.title" Run JS (supports await)
6115
+ capture har read --filter-url /api Inspect recorded traffic
6116
+
6117
+ 3. Bundle and inspect:
6118
+ capture session stop <session-id>
6119
+ capture session view <session-id>
6120
+
6121
+ SESSION COMMANDS
6122
+
6123
+ session start [--url <url>] Start a session
6124
+ session stop <session-id> Finalize and bundle artifacts
6125
+ session list List active and stopped sessions
6126
+ session view <id> [--filter section] section = screenshots|har|a11y|logs
6127
+ log <path> [--name label] Tail a log file into the active session
6128
+
6129
+ INTERACTION COMMANDS (work inside or outside a session)
6130
+
6131
+ a11y [--interactive] [--json] Accessibility tree (use this first to see elements)
6132
+ click "name" [--role <role>] Click by accessible name
6133
+ type "text" [--into "Field"] Type into focused element or named field
6134
+ screenshot [--out <path>] [--full-page] Screenshot (viewport: desktop|desktop-wide|tablet|mobile)
6135
+ exec <code> | exec --file <path> Evaluate JS; await is supported
6136
+ navigate <url> [--settle <ms>] Navigate the current tab + record HAR
6137
+
6138
+ DIAGNOSTICS & ONE-OFFS (no session needed)
6139
+
6140
+ detect Find running CDP endpoints
6141
+ list List open tabs across endpoints
6142
+ open <url> [--new] Open a URL, return its tab id
6143
+ reset-tab <url> Abandon a stuck tab, open fresh (updates session)
6144
+ record [--duration <secs>] Passive HAR recording; parallel-safe capture
6145
+ network <offline|online> Toggle connectivity for a tab
6146
+ har create | read [id] | delete <id> Manage standalone HAR recordings
6147
+
6148
+ TARGETING (only when NOT in a session, or picking a parallel tab)
6149
+
6150
+ --target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
6151
+ --url <pattern> Fuzzy URL match against open tabs
6152
+ NOTE: on \`session start\`, --url is a URL to OPEN, not a pattern.
6153
+
6154
+ HELP
6155
+
6156
+ capture <command> --help Per-command usage, e.g. capture click --help, capture har --help
6157
+ capture --version Print version
6158
+
6159
+ PREREQ \u2014 a browser with CDP must be running:
6160
+ Arc enabled by default
6161
+ Chrome / Chromium --remote-debugging-port=9222
6162
+ Electron apps CDP exposed automatically
6163
+ Verify with: capture detect`);
5909
6164
  }
5910
6165
  }
5911
- main2().catch((err) => {
6166
+ main().catch((err) => {
5912
6167
  console.error(err.message);
5913
6168
  process.exit(1);
5914
6169
  });