@bewa/cdp-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/browser.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync, spawn } = require('node:child_process');
6
+
7
+ const BROWSERS = {
8
+ darwin: {
9
+ canary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
10
+ chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
11
+ chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
12
+ edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
13
+ },
14
+ linux: {
15
+ canary: 'google-chrome-canary',
16
+ chrome: ['google-chrome', 'google-chrome-stable'],
17
+ chromium: ['chromium', 'chromium-browser'],
18
+ edge: 'microsoft-edge',
19
+ },
20
+ win32: {
21
+ canary: path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome SxS', 'Application', 'chrome.exe'),
22
+ chrome: path.join(process.env.PROGRAMFILES || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
23
+ chromium: path.join(process.env.PROGRAMFILES || '', 'Chromium', 'Application', 'chrome.exe'),
24
+ edge: path.join(process.env['PROGRAMFILES(X86)'] || process.env.PROGRAMFILES || '', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
25
+ },
26
+ };
27
+
28
+ /**
29
+ * Find browser executable. Returns absolute path or command name.
30
+ */
31
+ function detect(preference) {
32
+ // If preference is an absolute path, use it directly
33
+ if (preference && preference !== 'auto' && (preference.startsWith('/') || preference.includes('\\'))) {
34
+ if (fs.existsSync(preference)) return preference;
35
+ throw new Error(`Browser not found at: ${preference}`);
36
+ }
37
+
38
+ const platform = process.platform;
39
+ const candidates = BROWSERS[platform];
40
+ if (!candidates) throw new Error(`Unsupported platform: ${platform}`);
41
+
42
+ // If a specific browser name is given, try that first
43
+ const order = (preference && preference !== 'auto')
44
+ ? [preference]
45
+ : ['canary', 'chrome', 'chromium', 'edge'];
46
+
47
+ for (const name of order) {
48
+ const paths = candidates[name];
49
+ if (!paths) continue;
50
+
51
+ const pathList = Array.isArray(paths) ? paths : [paths];
52
+ for (const p of pathList) {
53
+ if (platform === 'linux') {
54
+ // On Linux, browser names are commands in PATH
55
+ try {
56
+ execFileSync('which', [p], { stdio: 'ignore' });
57
+ return p;
58
+ } catch { /* not found */ }
59
+ } else {
60
+ if (fs.existsSync(p)) return p;
61
+ }
62
+ }
63
+ }
64
+
65
+ throw new Error(
66
+ `No browser found. Install Chrome/Chromium or specify path: cdp-cli launch --browser /path/to/chrome`
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Launch browser with CDP enabled. Returns child process PID.
72
+ */
73
+ function launch(browserPath, config) {
74
+ const args = [
75
+ `--remote-debugging-port=${config.port}`,
76
+ `--user-data-dir=${config.userDataDir}`,
77
+ ];
78
+
79
+ if (config.extensions.length) {
80
+ args.push(`--load-extension=${config.extensions.join(',')}`);
81
+ }
82
+
83
+ if (config.launchArgs.length) {
84
+ args.push(...config.launchArgs);
85
+ }
86
+
87
+ const child = spawn(browserPath, args, {
88
+ detached: true,
89
+ stdio: 'ignore',
90
+ });
91
+ child.unref();
92
+
93
+ return child.pid;
94
+ }
95
+
96
+ module.exports = { detect, launch };
package/lib/cdp.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const http = require('node:http');
4
+ const { MinimalWebSocket } = require('./ws');
5
+
6
+ /**
7
+ * HTTP GET to CDP endpoint. Returns parsed JSON.
8
+ */
9
+ function httpGet(port, urlPath) {
10
+ return new Promise((resolve, reject) => {
11
+ http.get(`http://127.0.0.1:${port}${urlPath}`, res => {
12
+ let body = '';
13
+ res.on('data', c => body += c);
14
+ res.on('end', () => {
15
+ try { resolve(JSON.parse(body)); }
16
+ catch { resolve(body); }
17
+ });
18
+ }).on('error', reject);
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Check if CDP is listening on port.
24
+ */
25
+ async function isAlive(port) {
26
+ try {
27
+ await httpGet(port, '/json/version');
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * List page targets.
36
+ */
37
+ async function getTargets(port) {
38
+ const list = await httpGet(port, '/json');
39
+ return list
40
+ .filter(t => t.type === 'page')
41
+ .map(t => ({
42
+ id: t.id.substring(0, 8),
43
+ fullId: t.id,
44
+ title: t.title,
45
+ url: t.url,
46
+ wsUrl: t.webSocketDebuggerUrl,
47
+ }));
48
+ }
49
+
50
+ /**
51
+ * Find a target by ID prefix (minimum 4 chars).
52
+ */
53
+ async function findTarget(port, prefix) {
54
+ const targets = await getTargets(port);
55
+ const match = targets.find(t => t.fullId.startsWith(prefix) || t.id.startsWith(prefix));
56
+ if (!match) {
57
+ throw new Error(`No target matching "${prefix}". Run: cdp-cli targets`);
58
+ }
59
+ return match;
60
+ }
61
+
62
+ /**
63
+ * Open a WebSocket connection to a target and send a CDP command.
64
+ * Returns the result.
65
+ */
66
+ async function sendCommand(wsUrl, method, params = {}, timeout = 30000) {
67
+ const ws = new MinimalWebSocket(wsUrl);
68
+ await ws.connect();
69
+
70
+ return new Promise((resolve, reject) => {
71
+ const timer = setTimeout(() => {
72
+ ws.close();
73
+ reject(new Error(`CDP command timed out after ${timeout}ms`));
74
+ }, timeout);
75
+
76
+ ws.onMessage(raw => {
77
+ const msg = JSON.parse(raw);
78
+ if (msg.id !== 1) return;
79
+ clearTimeout(timer);
80
+ ws.close();
81
+
82
+ if (msg.error) {
83
+ reject(new Error(msg.error.message));
84
+ } else {
85
+ resolve(msg.result);
86
+ }
87
+ });
88
+
89
+ ws.send(JSON.stringify({ id: 1, method, params }));
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Evaluate JS in a page. Handles async, JSON auto-parsing.
95
+ */
96
+ async function evaluate(wsUrl, expression, timeout = 30000) {
97
+ const result = await sendCommand(wsUrl, 'Runtime.evaluate', {
98
+ expression,
99
+ returnByValue: true,
100
+ awaitPromise: true,
101
+ }, timeout);
102
+
103
+ if (result.exceptionDetails) {
104
+ const desc = result.exceptionDetails.exception?.description
105
+ || result.exceptionDetails.text
106
+ || 'Evaluation error';
107
+ throw new Error(desc);
108
+ }
109
+
110
+ const val = result.result;
111
+ if (val.type === 'undefined') return undefined;
112
+ if (val.value !== undefined) return val.value;
113
+ return val;
114
+ }
115
+
116
+ module.exports = { httpGet, isAlive, getTargets, findTarget, sendCommand, evaluate };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const { resolve } = require('../config');
5
+ const { findTarget, evaluate } = require('../cdp');
6
+ const { fail } = require('../output');
7
+
8
+ async function run(argv) {
9
+ const config = resolve(argv);
10
+ const args = config.positionals;
11
+
12
+ // Determine if this is eval or eval-file (check original command)
13
+ const isFile = process.argv[2] === 'eval-file';
14
+
15
+ if (args.length < (isFile ? 2 : 2)) {
16
+ const cmd = isFile ? 'eval-file <id> <file>' : 'eval <id> <expression>';
17
+ fail(`Usage: cdp-cli ${cmd}`);
18
+ }
19
+
20
+ const targetId = args[0];
21
+ let expression;
22
+
23
+ if (isFile) {
24
+ const filePath = args[1];
25
+ try {
26
+ expression = fs.readFileSync(filePath, 'utf-8');
27
+ } catch (e) {
28
+ fail(`Cannot read file: ${filePath} — ${e.message}`);
29
+ }
30
+ } else {
31
+ expression = args.slice(1).join(' ');
32
+ }
33
+
34
+ const target = await findTarget(config.port, targetId);
35
+ const result = await evaluate(target.wsUrl, expression, config.timeout);
36
+
37
+ // Output the result
38
+ if (result === undefined) {
39
+ // Undefined — no output
40
+ return;
41
+ }
42
+
43
+ if (typeof result === 'string') {
44
+ // Try to parse as JSON for pretty-printing
45
+ try {
46
+ const parsed = JSON.parse(result);
47
+ console.log(JSON.stringify(parsed, null, 2));
48
+ } catch {
49
+ console.log(result);
50
+ }
51
+ } else {
52
+ console.log(JSON.stringify(result, null, 2));
53
+ }
54
+ }
55
+
56
+ module.exports = { run };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { resolve, DEFAULTS } = require('../config');
6
+ const { print } = require('../output');
7
+
8
+ const AGENT_CONFIGS = [
9
+ { file: 'CLAUDE.md', name: 'Claude Code' },
10
+ { file: '.cursorrules', name: 'Cursor' },
11
+ { file: '.github/copilot-instructions.md', name: 'GitHub Copilot' },
12
+ { file: 'AGENTS.md', name: 'Codex' },
13
+ ];
14
+
15
+ const AGENT_SNIPPET = `
16
+ ## Browser Debugging
17
+
18
+ This project uses \`cdp-cli\` for browser debugging via Chrome DevTools Protocol.
19
+ Run \`cdp-cli --help\` for commands. Common workflow:
20
+
21
+ \`\`\`bash
22
+ cdp-cli launch # Start browser with CDP
23
+ cdp-cli targets # List pages
24
+ cdp-cli eval <id> "js" # Evaluate JS in page
25
+ cdp-cli reload [id] # Reload after code changes
26
+ \`\`\`
27
+ `;
28
+
29
+ async function run(argv) {
30
+ const config = resolve(argv);
31
+ const cwd = process.cwd();
32
+
33
+ // Write .cdprc.json
34
+ const rcPath = path.join(cwd, '.cdprc.json');
35
+ if (fs.existsSync(rcPath)) {
36
+ print({ ok: true, config: 'exists', path: rcPath }, config.human);
37
+ } else {
38
+ const rcContent = {
39
+ browser: 'auto',
40
+ port: DEFAULTS.port,
41
+ extensions: [],
42
+ };
43
+ fs.writeFileSync(rcPath, JSON.stringify(rcContent, null, 2) + '\n');
44
+ print({ ok: true, config: 'created', path: rcPath }, config.human);
45
+ }
46
+
47
+ // --agent: inject reference into AI assistant config files
48
+ if (config.agent) {
49
+ const injected = [];
50
+
51
+ for (const { file, name } of AGENT_CONFIGS) {
52
+ const filePath = path.join(cwd, file);
53
+ const dir = path.dirname(filePath);
54
+
55
+ if (fs.existsSync(filePath)) {
56
+ const content = fs.readFileSync(filePath, 'utf-8');
57
+ if (content.includes('cdp-cli')) {
58
+ continue; // already present
59
+ }
60
+ fs.appendFileSync(filePath, AGENT_SNIPPET);
61
+ injected.push({ file, name });
62
+ }
63
+ }
64
+
65
+ if (injected.length) {
66
+ print({ ok: true, agent: injected.map(i => i.file) }, config.human);
67
+ } else {
68
+ print({ ok: true, agent: 'no_config_files_found' }, config.human);
69
+ }
70
+ }
71
+ }
72
+
73
+ module.exports = { run };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { resolve } = require('../config');
4
+ const { detect, launch } = require('../browser');
5
+ const { isAlive } = require('../cdp');
6
+ const { print, fail } = require('../output');
7
+
8
+ async function run(argv) {
9
+ const config = resolve(argv);
10
+
11
+ // Already running?
12
+ if (await isAlive(config.port)) {
13
+ print({ ok: true, status: 'already_running', port: config.port }, config.human);
14
+ return;
15
+ }
16
+
17
+ const browserPath = detect(config.browser);
18
+ const browserName = browserPath.toLowerCase().includes('canary') ? 'canary'
19
+ : browserPath.toLowerCase().includes('edge') ? 'edge'
20
+ : browserPath.toLowerCase().includes('chromium') ? 'chromium'
21
+ : 'chrome';
22
+
23
+ const pid = launch(browserPath, config);
24
+
25
+ // Wait for CDP to be ready
26
+ for (let i = 0; i < 60; i++) {
27
+ if (await isAlive(config.port)) {
28
+ print({
29
+ ok: true,
30
+ status: 'launched',
31
+ browser: browserName,
32
+ port: config.port,
33
+ pid,
34
+ extensions: config.extensions.length || undefined,
35
+ }, config.human);
36
+ return;
37
+ }
38
+ await new Promise(r => setTimeout(r, 500));
39
+ }
40
+
41
+ fail(`Browser started (pid ${pid}) but CDP port ${config.port} not responding after 30s`);
42
+ }
43
+
44
+ module.exports = { run };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const { resolve } = require('../config');
4
+ const { findTarget, sendCommand } = require('../cdp');
5
+ const { print, fail } = require('../output');
6
+
7
+ async function run(argv) {
8
+ const config = resolve(argv);
9
+ const args = config.positionals;
10
+
11
+ if (args.length < 2) {
12
+ fail('Usage: cdp-cli navigate <id> <url>');
13
+ }
14
+
15
+ const target = await findTarget(config.port, args[0]);
16
+ const url = args[1];
17
+
18
+ await sendCommand(target.wsUrl, 'Page.navigate', { url });
19
+ print({ ok: true, id: target.id, url }, config.human);
20
+ }
21
+
22
+ module.exports = { run };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { resolve } = require('../config');
4
+ const { getTargets, findTarget, sendCommand } = require('../cdp');
5
+ const { print } = require('../output');
6
+
7
+ async function run(argv) {
8
+ const config = resolve(argv);
9
+ const args = config.positionals;
10
+
11
+ let targets;
12
+
13
+ if (args[0]) {
14
+ // Reload specific target
15
+ const t = await findTarget(config.port, args[0]);
16
+ targets = [t];
17
+ } else {
18
+ // Reload all non-chrome:// pages
19
+ const all = await getTargets(config.port);
20
+ targets = all.filter(t => !t.url.startsWith('chrome://'));
21
+ }
22
+
23
+ const reloaded = [];
24
+ for (const target of targets) {
25
+ await sendCommand(target.wsUrl, 'Page.reload', {});
26
+ reloaded.push(target.id);
27
+ }
28
+
29
+ print({ ok: true, reloaded }, config.human);
30
+ }
31
+
32
+ module.exports = { run };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { resolve } = require('../config');
4
+ const { getTargets } = require('../cdp');
5
+ const { print, fail } = require('../output');
6
+
7
+ async function run(argv) {
8
+ const config = resolve(argv);
9
+ const targets = await getTargets(config.port);
10
+
11
+ if (!targets.length) {
12
+ print([], config.human);
13
+ return;
14
+ }
15
+
16
+ const out = targets.map(t => ({ id: t.id, title: t.title, url: t.url }));
17
+ print(out, config.human);
18
+ }
19
+
20
+ module.exports = { run };
package/lib/config.js ADDED
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs } = require('node:util');
6
+
7
+ const DEFAULTS = {
8
+ browser: 'auto',
9
+ port: 9222,
10
+ userDataDir: '/tmp/cdp-debug-profile',
11
+ extensions: [],
12
+ launchArgs: [],
13
+ timeout: 30000,
14
+ };
15
+
16
+ // Common CLI options shared across commands
17
+ const CLI_OPTIONS = {
18
+ port: { type: 'string', short: 'p' },
19
+ browser: { type: 'string', short: 'b' },
20
+ timeout: { type: 'string', short: 't' },
21
+ extension: { type: 'string', multiple: true },
22
+ human: { type: 'boolean' },
23
+ agent: { type: 'boolean' },
24
+ };
25
+
26
+ /**
27
+ * Search upward from cwd for .cdprc.json
28
+ */
29
+ function findConfig() {
30
+ let dir = process.cwd();
31
+ const root = path.parse(dir).root;
32
+
33
+ while (dir !== root) {
34
+ const configPath = path.join(dir, '.cdprc.json');
35
+ if (fs.existsSync(configPath)) {
36
+ try {
37
+ const raw = fs.readFileSync(configPath, 'utf-8');
38
+ const config = JSON.parse(raw);
39
+ config._dir = dir; // for resolving relative paths
40
+ return config;
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+ dir = path.dirname(dir);
46
+ }
47
+ return {};
48
+ }
49
+
50
+ /**
51
+ * Merge: CLI flags > .cdprc.json > defaults
52
+ * Returns resolved config object.
53
+ */
54
+ function resolve(argv) {
55
+ const { values, positionals } = parseArgs({
56
+ args: argv,
57
+ options: CLI_OPTIONS,
58
+ allowPositionals: true,
59
+ strict: false,
60
+ });
61
+
62
+ const file = findConfig();
63
+ const configDir = file._dir || process.cwd();
64
+
65
+ const config = { ...DEFAULTS };
66
+
67
+ // Layer 1: file config
68
+ if (file.browser) config.browser = file.browser;
69
+ if (file.port) config.port = file.port;
70
+ if (file.userDataDir) config.userDataDir = file.userDataDir;
71
+ if (file.timeout) config.timeout = file.timeout;
72
+ if (file.launchArgs) config.launchArgs = file.launchArgs;
73
+ if (file.extensions) {
74
+ config.extensions = file.extensions.map(e =>
75
+ path.isAbsolute(e) ? e : path.resolve(configDir, e)
76
+ );
77
+ }
78
+
79
+ // Layer 2: CLI flags
80
+ if (values.port) config.port = parseInt(values.port, 10);
81
+ if (values.browser) config.browser = values.browser;
82
+ if (values.timeout) config.timeout = parseInt(values.timeout, 10);
83
+ if (values.extension) {
84
+ config.extensions = values.extension.map(e => path.resolve(e));
85
+ }
86
+
87
+ config.human = !!values.human;
88
+ config.agent = !!values.agent;
89
+ config.positionals = positionals;
90
+
91
+ return config;
92
+ }
93
+
94
+ module.exports = { resolve, DEFAULTS, findConfig };
package/lib/output.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Output a result object. JSON by default, human-readable with --human.
5
+ */
6
+ function print(data, human) {
7
+ if (human) {
8
+ if (Array.isArray(data)) {
9
+ data.forEach(item => console.log(formatHuman(item)));
10
+ } else {
11
+ console.log(formatHuman(data));
12
+ }
13
+ } else {
14
+ if (Array.isArray(data)) {
15
+ data.forEach(item => console.log(JSON.stringify(item)));
16
+ } else {
17
+ console.log(JSON.stringify(data));
18
+ }
19
+ }
20
+ }
21
+
22
+ function formatHuman(obj) {
23
+ if (typeof obj === 'string') return obj;
24
+ if (obj.id && obj.title && obj.url) {
25
+ return `${obj.id} ${obj.title.substring(0, 50).padEnd(50)} ${obj.url.substring(0, 80)}`;
26
+ }
27
+ return JSON.stringify(obj, null, 2);
28
+ }
29
+
30
+ /**
31
+ * Print error to stderr and exit.
32
+ */
33
+ function fail(message, code = 1) {
34
+ console.error(JSON.stringify({ error: message }));
35
+ process.exit(code);
36
+ }
37
+
38
+ module.exports = { print, fail };