@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/AGENT.md +97 -0
- package/LICENSE +190 -0
- package/README.md +259 -0
- package/bin/cdp-cli.js +66 -0
- package/lib/browser.js +96 -0
- package/lib/cdp.js +116 -0
- package/lib/commands/eval.js +56 -0
- package/lib/commands/init.js +73 -0
- package/lib/commands/launch.js +44 -0
- package/lib/commands/navigate.js +22 -0
- package/lib/commands/reload.js +32 -0
- package/lib/commands/targets.js +20 -0
- package/lib/config.js +94 -0
- package/lib/output.js +38 -0
- package/lib/ws.js +166 -0
- package/package.json +34 -0
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 };
|