chrome-cdp-manager 1.2.3 → 1.2.6

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/README.md CHANGED
@@ -49,6 +49,11 @@ chrome-cdp open https://example.com
49
49
  # Same, headless (no window/Dock/taskbar)
50
50
  chrome-cdp open https://example.com --headless
51
51
 
52
+ # Route all traffic through a proxy (defaults to socks5://127.0.0.1:1080)
53
+ chrome-cdp open https://example.com --proxy
54
+ chrome-cdp open https://example.com --proxy socks5://127.0.0.1:9050
55
+ chrome-cdp open https://example.com --proxy http://10.0.0.1:8080
56
+
52
57
  # Fetch a page's rendered HTML over CDP (headless by default)
53
58
  chrome-cdp html example.com -o page.html
54
59
 
@@ -108,6 +113,36 @@ Playwright over CDP, and returns `{ browser, context, page, config }`. In
108
113
  zero-install (npx) setups where Playwright can't be resolved automatically,
109
114
  inject it: `connect({ chromium })`.
110
115
 
116
+ ## Playwright CLI (`playwright-cli`)
117
+
118
+ The package also installs a **`playwright-cli`** command that runs
119
+ [Playwright's MCP CLI client](https://playwright.dev) against the ChromeCDP
120
+ instance managed here. It's a thin wrapper that, before handing off, guarantees:
121
+
122
+ 1. A **headed** ChromeCDP browser is reachable over CDP. If one is **already
123
+ running on the port, it just attaches to it** (no relaunch); otherwise it
124
+ launches a fresh headed instance.
125
+ 2. The environment points Playwright at that browser instead of spawning its own:
126
+
127
+ ```bash
128
+ PLAYWRIGHT_MCP_CDP_ENDPOINT=http://localhost:9222 # from config (default 9222)
129
+ PLAYWRIGHT_MCP_ISOLATED=false
130
+ ```
131
+
132
+ Everything after `playwright-cli` is forwarded verbatim to the Playwright CLI:
133
+
134
+ ```bash
135
+ # Drive the attached browser
136
+ playwright-cli goto https://example.com
137
+ playwright-cli click "Sign in"
138
+
139
+ # Help / version forward straight through (no browser launch)
140
+ playwright-cli --help
141
+ ```
142
+
143
+ Requires the optional `playwright` peer dependency (`npm i playwright`). The
144
+ endpoint port follows your saved config (`~/.config/chrome-cdp-manager/config.json`).
145
+
111
146
  ## Common options
112
147
 
113
148
  | Option | Description |
@@ -117,6 +152,8 @@ inject it: `connect({ chromium })`.
117
152
  | `-b, --browser <name>` | Browser: `chrome`, `edge`, `brave`, `chromium`, `vivaldi`, `opera`, `arc` |
118
153
  | `--path <path>` | Explicit browser executable (overrides `--browser`) |
119
154
  | `--target <path>` | Launcher location (`.app` on macOS, `.lnk` on Windows) |
155
+ | `--proxy [server]` | Route traffic through a proxy; omit the value for `socks5://127.0.0.1:1080`. Accepts a port (`1080`), `host:port`, or `scheme://host:port` (`socks5`, `socks4`, `http`, `https`) |
156
+ | `--no-proxy` | Disable any configured proxy for this run |
120
157
  | `-t, --timeout <secs>` | Startup / load timeout (`open`, `html`) |
121
158
 
122
159
  `-c, --chrome` and `--bundle` remain as aliases for `--path` and `--target`.
@@ -126,6 +163,17 @@ persisted (`~/.config/chrome-cdp-manager/config.json`) at `setup` time so every
126
163
  command agrees on the same environment. Re-run `setup --force` after changing
127
164
  them.
128
165
 
166
+ ### Proxy
167
+
168
+ `--proxy` passes Chrome's `--proxy-server` flag at launch so traffic routes
169
+ through it. The default is a local SOCKS5 proxy on port `1080` (the common
170
+ `ssh -D` / shadowsocks default). The proxy is a **per-launch** flag — it is not
171
+ baked into the launcher, so a normal (non-proxied) launch is never affected by
172
+ it. Before launching, the proxy port is probed and a warning is printed if
173
+ nothing is listening; `status` reports the configured proxy and whether it's
174
+ reachable. Persist a default proxy with `chrome-cdp setup --proxy …`, or opt out
175
+ of a persisted proxy for a single run with `--no-proxy`.
176
+
129
177
  ## How the icon stays consistent
130
178
 
131
179
  **macOS** — the bundle's executable is a small bash launcher:
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `playwright-cli` — run Playwright's MCP CLI client against the ChromeCDP
4
+ * instance managed by this package.
5
+ *
6
+ * The Playwright CLI itself has nothing to do with this repo; this wrapper only
7
+ * guarantees the two things it needs before handing off:
8
+ *
9
+ * 1. A *headed* ChromeCDP browser is running and reachable over CDP.
10
+ * 2. The env points Playwright at that browser instead of spawning its own:
11
+ * PLAYWRIGHT_MCP_CDP_ENDPOINT=http://localhost:<cdpPort> (default 9222)
12
+ * PLAYWRIGHT_MCP_ISOLATED=false
13
+ *
14
+ * Everything after `playwright-cli` is forwarded verbatim — the cli-client reads
15
+ * process.argv.slice(2) directly, which already holds those args here.
16
+ */
17
+ import { createRequire } from "node:module";
18
+
19
+ import { assertSupportedPlatform } from "../src/platform.js";
20
+ import { loadConfig } from "../src/config.js";
21
+ import { getLauncher } from "../src/launcher.js";
22
+ import { ensureBrowserRunning, probeCdp } from "../src/browser.js";
23
+
24
+ const require = createRequire(import.meta.url);
25
+
26
+ /** Plain help/version requests don't need a browser — just forward them. */
27
+ function isHelpOrVersionOnly(args) {
28
+ if (args.length === 0) return true;
29
+ const hasCommand = args.some((a) => !a.startsWith("-"));
30
+ if (!hasCommand) return true;
31
+ return args.some((a) => ["-h", "--help", "-v", "--version"].includes(a));
32
+ }
33
+
34
+ async function main() {
35
+ const args = process.argv.slice(2);
36
+
37
+ if (!isHelpOrVersionOnly(args)) {
38
+ assertSupportedPlatform();
39
+ const config = loadConfig();
40
+ const endpoint = `http://localhost:${config.cdpPort}`;
41
+
42
+ // If a CDP browser is already up on this port, attach to it as-is — never
43
+ // relaunch (even if it's headless). Only launch a fresh headed instance when
44
+ // nothing is listening.
45
+ const existing = await probeCdp(config.cdpPort);
46
+ if (existing) {
47
+ console.error(`ChromeCDP already running on port ${config.cdpPort}.`);
48
+ } else {
49
+ getLauncher().ensure(config, { force: false });
50
+ await ensureBrowserRunning(config, { headless: false, timeoutMs: 30_000 });
51
+ console.error(`Launched ChromeCDP (headed) on port ${config.cdpPort}.`);
52
+ }
53
+ console.error(`playwright-cli attaching to ${endpoint} ...`);
54
+
55
+ process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT = endpoint;
56
+ process.env.PLAYWRIGHT_MCP_ISOLATED = "false";
57
+ }
58
+
59
+ let program;
60
+ try {
61
+ ({ program } = require("playwright-core/lib/tools/cli-client/program"));
62
+ } catch {
63
+ throw new Error(
64
+ "Playwright not found. The `playwright-cli` command needs Playwright " +
65
+ "installed:\n npm i playwright",
66
+ );
67
+ }
68
+
69
+ const packageJson = require("../package.json");
70
+ await program({ embedderVersion: packageJson.version });
71
+ }
72
+
73
+ main().catch((error) => {
74
+ console.error(`\nError: ${error.message}`);
75
+ process.exit(1);
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-cdp-manager",
3
- "version": "1.2.3",
3
+ "version": "1.2.6",
4
4
  "description": "Set up and drive a Chrome DevTools Protocol (CDP) instance on macOS or Windows through a dedicated launcher (consistent Dock/taskbar icon). Works with any Chromium-based browser — Chrome, Edge, Brave, Chromium, Vivaldi, Opera, Arc.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "bin": {
20
20
  "chrome-cdp": "bin/cli.js",
21
- "chrome-cdp-manager": "bin/cli.js"
21
+ "chrome-cdp-manager": "bin/cli.js",
22
+ "playwright-cli": "bin/playwright-cli.js"
22
23
  },
23
24
  "files": [
24
25
  "bin",
@@ -57,6 +58,7 @@
57
58
  ],
58
59
  "license": "MIT",
59
60
  "dependencies": {
61
+ "cheerio": "^1.2.0",
60
62
  "commander": "^12.1.0"
61
63
  },
62
64
  "peerDependencies": {
@@ -68,6 +70,7 @@
68
70
  }
69
71
  },
70
72
  "devDependencies": {
71
- "typescript": "^5.6.0"
73
+ "playwright": "^1.61.0",
74
+ "typescript": "^5.9.3"
72
75
  }
73
76
  }
package/src/browser.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { getLauncher } from "./launcher.js";
2
+ import { closeBrowser } from "./cdp.js";
3
+
2
4
 
3
5
  /** CDP HTTP endpoint helper. */
4
6
  function versionUrl(cdpPort) {
@@ -38,7 +40,21 @@ export async function waitForCdp(cdpPort, timeoutMs = 30_000) {
38
40
  */
39
41
  export async function ensureBrowserRunning(config, { headless, timeoutMs } = {}) {
40
42
  const existing = await probeCdp(config.cdpPort);
41
- if (existing) return { launched: false, version: existing };
43
+ if (existing) {
44
+ const isHeadless =
45
+ /HeadlessChrome/i.test(existing.Browser) ||
46
+ /HeadlessChrome/i.test(existing["User-Agent"] || "");
47
+ if (isHeadless && !headless) {
48
+ try {
49
+ await closeBrowser(config.cdpPort);
50
+ await delay(500);
51
+ } catch (err) {
52
+ // ignore
53
+ }
54
+ } else {
55
+ return { launched: false, version: existing };
56
+ }
57
+ }
42
58
 
43
59
  getLauncher().launch(config, { headless });
44
60
  const version = await waitForCdp(config.cdpPort, timeoutMs);
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@ import { Command } from "commander";
3
3
 
4
4
  import { DEFAULTS } from "./config.js";
5
5
  import { browserKeys, DEFAULT_BROWSER } from "./browsers.js";
6
+ import { DEFAULT_PROXY } from "./proxy.js";
6
7
  import {
7
8
  setupCommand,
8
9
  openCommand,
@@ -61,7 +62,13 @@ Run 'chrome-cdp <command> --help' to see all options for a specific command.`,
61
62
  `Launcher ${TARGET_LABEL} path (default: ${DEFAULTS.launcherPath})`,
62
63
  )
63
64
  // Back-compat alias for --target.
64
- .option("--bundle <path>", "Alias for --target");
65
+ .option("--bundle <path>", "Alias for --target")
66
+ .option(
67
+ "--proxy [server]",
68
+ `Route traffic through a proxy; value omitted uses ${DEFAULT_PROXY}. ` +
69
+ `Accepts a port (1080), host:port, or scheme://host:port`,
70
+ )
71
+ .option("--no-proxy", "Disable any configured proxy for this run");
65
72
 
66
73
  withCommon(
67
74
  program
package/src/commands.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { getLauncher } from "./launcher.js";
13
13
  import { ensureBrowserRunning, probeCdp } from "./browser.js";
14
14
  import { openUrl, getPageHtml, closeBrowser } from "./cdp.js";
15
+ import { parseProxy, detectProxy } from "./proxy.js";
15
16
 
16
17
  /** Merge persisted config with CLI overrides into a fully-resolved config. */
17
18
  function resolveConfig(opts = {}) {
@@ -51,6 +52,14 @@ function resolveConfig(opts = {}) {
51
52
  const target = opts.target || opts.bundle;
52
53
  if (target) resolved.launcherPath = path.resolve(target);
53
54
  if (opts.port !== undefined) resolved.cdpPort = parsePort(opts.port);
55
+
56
+ // --no-proxy => opts.proxy === false; --proxy [server] => true or a string;
57
+ // absent => undefined (no proxy — it is never persisted).
58
+ if (opts.proxy === false) {
59
+ resolved.proxy = null;
60
+ } else if (opts.proxy !== undefined) {
61
+ resolved.proxy = parseProxy(opts.proxy).server;
62
+ }
54
63
  return resolved;
55
64
  }
56
65
 
@@ -86,6 +95,21 @@ function ensureLauncherReady(config) {
86
95
  }
87
96
  }
88
97
 
98
+ /** Probe the configured proxy and report whether it's reachable. */
99
+ async function reportProxy(config) {
100
+ if (!config.proxy) return;
101
+ const { server, host, port } = parseProxy(config.proxy);
102
+ const reachable = await detectProxy({ host, port });
103
+ if (reachable) {
104
+ console.error(`Proxy: routing traffic through ${server} (detected listening).`);
105
+ } else {
106
+ console.error(
107
+ `Proxy: ${server} configured, but nothing is listening on ${host}:${port}. ` +
108
+ `Pages won't load until the proxy is up — or pass --no-proxy.`,
109
+ );
110
+ }
111
+ }
112
+
89
113
  // MARK: setup
90
114
  export async function setupCommand(opts) {
91
115
  assertSupportedPlatform();
@@ -109,6 +133,7 @@ export async function openCommand(url, opts) {
109
133
  const timeoutMs = parseTimeoutMs(opts.timeout);
110
134
 
111
135
  ensureLauncherReady(config);
136
+ await reportProxy(config);
112
137
 
113
138
  const { launched, version } = await ensureBrowserRunning(config, { headless, timeoutMs });
114
139
  if (launched) {
@@ -136,6 +161,7 @@ export async function htmlCommand(url, opts) {
136
161
  const target = normalizeUrl(url);
137
162
 
138
163
  ensureLauncherReady(config);
164
+ await reportProxy(config);
139
165
  const { launched } = await ensureBrowserRunning(config, { headless, timeoutMs });
140
166
 
141
167
  console.error(`Fetching HTML for ${target} ...`);
@@ -186,6 +212,14 @@ export async function statusCommand(opts) {
186
212
  console.log(` Profile: ${config.profileDir}`);
187
213
  console.log(` CDP port: ${config.cdpPort}`);
188
214
 
215
+ if (config.proxy) {
216
+ const { server, host, port } = parseProxy(config.proxy);
217
+ const reachable = await detectProxy({ host, port });
218
+ console.log(` Proxy: ${server} (${reachable ? "reachable" : "NOT reachable"})`);
219
+ } else {
220
+ console.log(` Proxy: none (direct connection)`);
221
+ }
222
+
189
223
  const isHeadless =
190
224
  /HeadlessChrome/i.test(version.Browser) ||
191
225
  /HeadlessChrome/i.test(version["User-Agent"] || "");
package/src/config.js CHANGED
@@ -46,6 +46,8 @@ export function computeDefaults({
46
46
  bundleId: BUNDLE_ID,
47
47
  cdpPort: CDP_PORT,
48
48
  profileDir: path.join(os.homedir(), ".chrome_cdp_profile"),
49
+ // Proxy is opt-in: null means launch with a direct connection.
50
+ proxy: null,
49
51
  };
50
52
  }
51
53
 
@@ -88,6 +90,8 @@ export function saveConfig(config) {
88
90
  bundleId: config.bundleId,
89
91
  cdpPort: config.cdpPort,
90
92
  profileDir: config.profileDir,
93
+ // Proxy is intentionally not persisted: it's a per-invocation flag, so it
94
+ // never leaks into other launches.
91
95
  };
92
96
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
93
97
  fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(toStore, null, 2)}\n`);
@@ -29,6 +29,36 @@ BROWSER=${shellQuote(browserPath)}
29
29
  PORT=${shellQuote(String(cdpPort))}
30
30
  PROFILE=${shellQuote(profileDir)}
31
31
 
32
+ # Gracefully close any background headless Chrome instance using the same port.
33
+ NODE_BIN="node"
34
+ if ! command -v node &> /dev/null; then
35
+ for path in "/opt/homebrew/bin/node" "/usr/local/bin/node" "$HOME/.nvm/versions/node"/*/bin/node "$HOME/.nvs"/*/bin/node; do
36
+ if [ -x "$path" ]; then
37
+ NODE_BIN="$path"
38
+ break
39
+ fi
40
+ done
41
+ fi
42
+
43
+ "$NODE_BIN" --no-warnings -e "(async () => {
44
+ try {
45
+ const res = await fetch('http://127.0.0.1:${cdpPort}/json/version');
46
+ if (res.ok) {
47
+ const info = await res.json();
48
+ const isHeadless = /HeadlessChrome/i.test(info.Browser) || /HeadlessChrome/i.test(info['User-Agent'] || '');
49
+ if (isHeadless) {
50
+ const ws = new WebSocket(info.webSocketDebuggerUrl);
51
+ ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: 'Browser.close' }));
52
+ ws.onmessage = () => {
53
+ ws.close();
54
+ process.exit(0);
55
+ };
56
+ await new Promise(r => setTimeout(r, 800));
57
+ }
58
+ }
59
+ } catch {}
60
+ })()" || true
61
+
32
62
  exec ${archPrefix}"$BROWSER" \\
33
63
  --remote-debugging-port="$PORT" \\
34
64
  --user-data-dir="$PROFILE" \\
@@ -164,6 +194,10 @@ export function ensure(config, { force = false } = {}) {
164
194
  * LaunchServices attaches the Dock icon; headless runs the launcher directly.
165
195
  */
166
196
  export function launch(config, { headless } = {}) {
197
+ // The proxy is a per-launch flag, not baked into the (fixed) bundle, so a
198
+ // normal launch is never affected by it.
199
+ const proxyArg = config.proxy ? [`--proxy-server=${config.proxy}`] : [];
200
+
167
201
  if (headless) {
168
202
  const { executable } = bundleLayout(config.launcherPath);
169
203
  const args = ["--headless=new"];
@@ -172,6 +206,7 @@ export function launch(config, { headless } = {}) {
172
206
  args.push(`--window-size=${size.width},${size.height}`);
173
207
  args.push(`--screen-info={0,0 ${size.width}x${size.height}}`);
174
208
  }
209
+ args.push(...proxyArg);
175
210
  const child = spawn(executable, args, {
176
211
  detached: true,
177
212
  stdio: "ignore",
@@ -181,7 +216,10 @@ export function launch(config, { headless } = {}) {
181
216
  }
182
217
 
183
218
  // `open` returns immediately; readiness is awaited via waitForCdp().
184
- const child = spawn("open", [config.launcherPath], { stdio: "ignore" });
219
+ // `--args` forwards the proxy flag to the bundle's launcher ("$@").
220
+ const openArgs = [config.launcherPath];
221
+ if (proxyArg.length) openArgs.push("--args", ...proxyArg);
222
+ const child = spawn("open", openArgs, { stdio: "ignore" });
185
223
  child.unref();
186
224
  }
187
225
 
@@ -85,14 +85,19 @@ export function ensure(config, { force = false } = {}) {
85
85
  /**
86
86
  * Launch the browser. Headed goes through the shortcut so the taskbar uses its
87
87
  * icon; headless runs the exe directly with the same flags + `--headless=new`.
88
+ *
89
+ * The proxy is a per-launch flag, never baked into the (fixed) shortcut. A
90
+ * `.lnk` can't take extra args at launch time, so a proxied headed launch runs
91
+ * the exe directly (the dedicated profile still gives a separate taskbar entry).
88
92
  */
89
93
  export function launch(config, { headless } = {}) {
90
- if (headless) {
94
+ if (headless || config.proxy) {
91
95
  const args = [
92
96
  `--remote-debugging-port=${config.cdpPort}`,
93
97
  `--user-data-dir=${config.profileDir}`,
94
- "--headless=new",
95
98
  ];
99
+ if (headless) args.push("--headless=new");
100
+ if (config.proxy) args.push(`--proxy-server=${config.proxy}`);
96
101
  const size = getWindowSizeFromProfile(config.profileDir);
97
102
  if (size) {
98
103
  args.push(`--window-size=${size.width},${size.height}`);
package/src/proxy.js ADDED
@@ -0,0 +1,93 @@
1
+ import net from "node:net";
2
+
3
+ /**
4
+ * Proxy support: normalise a user-supplied proxy value into a Chrome
5
+ * `--proxy-server` string and probe whether something is actually listening on
6
+ * it. The default is a local SOCKS5 proxy on port 1080 (the common SSH/`ssh -D`
7
+ * and shadowsocks default).
8
+ */
9
+
10
+ export const DEFAULT_PROXY_HOST = "127.0.0.1";
11
+ export const DEFAULT_PROXY_PORT = 1080;
12
+ export const DEFAULT_PROXY_SCHEME = "socks5";
13
+ export const DEFAULT_PROXY = `${DEFAULT_PROXY_SCHEME}://${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`;
14
+
15
+ /** Chrome accepts these proxy schemes for `--proxy-server`. */
16
+ const KNOWN_SCHEMES = new Set(["socks5", "socks4", "http", "https"]);
17
+
18
+ /**
19
+ * Parse a proxy value into its parts. Accepts, in order of convenience:
20
+ * - `true` / "" / "default" → the default SOCKS5 proxy
21
+ * - "1080" → socks5://127.0.0.1:1080
22
+ * - "host:1080" → socks5://host:1080
23
+ * - "socks5://host:1080" → as given
24
+ * - "http://host:8080" → an HTTP proxy
25
+ * @returns {{ server: string, scheme: string, host: string, port: number }}
26
+ */
27
+ export function parseProxy(value) {
28
+ if (value === true || value === undefined || value === null) {
29
+ value = DEFAULT_PROXY;
30
+ }
31
+ let raw = String(value).trim();
32
+ if (raw === "" || raw.toLowerCase() === "default") raw = DEFAULT_PROXY;
33
+
34
+ // Port-only shorthand: "1080".
35
+ if (/^\d+$/.test(raw)) {
36
+ raw = `${DEFAULT_PROXY_SCHEME}://${DEFAULT_PROXY_HOST}:${raw}`;
37
+ } else if (!raw.includes("://")) {
38
+ // "host:port" shorthand — default the scheme to socks5.
39
+ raw = `${DEFAULT_PROXY_SCHEME}://${raw}`;
40
+ }
41
+
42
+ let url;
43
+ try {
44
+ url = new URL(raw);
45
+ } catch {
46
+ throw new Error(
47
+ `Invalid --proxy "${value}". Use a port (1080), host:port, or scheme://host:port.`,
48
+ );
49
+ }
50
+
51
+ const scheme = url.protocol.replace(/:$/, "").toLowerCase();
52
+ if (!KNOWN_SCHEMES.has(scheme)) {
53
+ throw new Error(
54
+ `Unsupported proxy scheme "${scheme}". Use one of: ${[...KNOWN_SCHEMES].join(", ")}.`,
55
+ );
56
+ }
57
+
58
+ const host = url.hostname || DEFAULT_PROXY_HOST;
59
+ const port = url.port
60
+ ? Number(url.port)
61
+ : scheme.startsWith("socks")
62
+ ? DEFAULT_PROXY_PORT
63
+ : 8080;
64
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
65
+ throw new Error(`Invalid proxy port in "${value}". Use an integer 1-65535.`);
66
+ }
67
+
68
+ return { server: `${scheme}://${host}:${port}`, scheme, host, port };
69
+ }
70
+
71
+ /**
72
+ * Probe a TCP port to see whether a proxy is actually listening there. This is
73
+ * a connectivity check only — it can't verify the protocol — but it catches the
74
+ * common case of routing traffic through a proxy that isn't running.
75
+ * @returns {Promise<boolean>}
76
+ */
77
+ export function detectProxy({ host, port, timeoutMs = 1500 } = {}) {
78
+ return new Promise((resolve) => {
79
+ const socket = new net.Socket();
80
+ let settled = false;
81
+ const finish = (reachable) => {
82
+ if (settled) return;
83
+ settled = true;
84
+ socket.destroy();
85
+ resolve(reachable);
86
+ };
87
+ socket.setTimeout(timeoutMs);
88
+ socket.once("connect", () => finish(true));
89
+ socket.once("timeout", () => finish(false));
90
+ socket.once("error", () => finish(false));
91
+ socket.connect(port, host);
92
+ });
93
+ }
package/types/config.d.ts CHANGED
@@ -10,6 +10,7 @@ export function computeDefaults({ browser, platform, }?: {
10
10
  bundleId: string;
11
11
  cdpPort: number;
12
12
  profileDir: any;
13
+ proxy: any;
13
14
  };
14
15
  /** Load persisted config, falling back to {@link DEFAULTS} for missing keys. */
15
16
  export function loadConfig(): any;
@@ -24,5 +25,6 @@ export const DEFAULTS: Readonly<{
24
25
  bundleId: string;
25
26
  cdpPort: number;
26
27
  profileDir: any;
28
+ proxy: any;
27
29
  }>;
28
30
  export const CONFIG_FILE: any;
@@ -14,6 +14,10 @@ export function ensure(config: any, { force }?: {
14
14
  /**
15
15
  * Launch the browser. Headed goes through the shortcut so the taskbar uses its
16
16
  * icon; headless runs the exe directly with the same flags + `--headless=new`.
17
+ *
18
+ * The proxy is a per-launch flag, never baked into the (fixed) shortcut. A
19
+ * `.lnk` can't take extra args at launch time, so a proxied headed launch runs
20
+ * the exe directly (the dedicated profile still gives a separate taskbar entry).
17
21
  */
18
22
  export function launch(config: any, { headless }?: {}): void;
19
23
  export const targetLabel: "Shortcut";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Parse a proxy value into its parts. Accepts, in order of convenience:
3
+ * - `true` / "" / "default" → the default SOCKS5 proxy
4
+ * - "1080" → socks5://127.0.0.1:1080
5
+ * - "host:1080" → socks5://host:1080
6
+ * - "socks5://host:1080" → as given
7
+ * - "http://host:8080" → an HTTP proxy
8
+ * @returns {{ server: string, scheme: string, host: string, port: number }}
9
+ */
10
+ export function parseProxy(value: any): {
11
+ server: string;
12
+ scheme: string;
13
+ host: string;
14
+ port: number;
15
+ };
16
+ /**
17
+ * Probe a TCP port to see whether a proxy is actually listening there. This is
18
+ * a connectivity check only — it can't verify the protocol — but it catches the
19
+ * common case of routing traffic through a proxy that isn't running.
20
+ * @returns {Promise<boolean>}
21
+ */
22
+ export function detectProxy({ host, port, timeoutMs }?: {
23
+ timeoutMs?: number;
24
+ }): Promise<boolean>;
25
+ /**
26
+ * Proxy support: normalise a user-supplied proxy value into a Chrome
27
+ * `--proxy-server` string and probe whether something is actually listening on
28
+ * it. The default is a local SOCKS5 proxy on port 1080 (the common SSH/`ssh -D`
29
+ * and shadowsocks default).
30
+ */
31
+ export const DEFAULT_PROXY_HOST: "127.0.0.1";
32
+ export const DEFAULT_PROXY_PORT: 1080;
33
+ export const DEFAULT_PROXY_SCHEME: "socks5";
34
+ export const DEFAULT_PROXY: "socks5://127.0.0.1:1080";