cdp-skill 1.0.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/src/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CDP Skill CLI
4
+ *
5
+ * JSON interpreter for browser automation. Reads JSON from stdin,
6
+ * executes browser automation steps, and outputs JSON results.
7
+ *
8
+ * Usage:
9
+ * echo '{"steps":[{"goto":"https://example.com"}]}' | node src/cli.js
10
+ */
11
+
12
+ import { createBrowser } from './cdp.js';
13
+ import { createPageController } from './page.js';
14
+ import { createElementLocator, createInputEmulator } from './dom.js';
15
+ import { createScreenshotCapture, createConsoleCapture, createPdfCapture } from './capture.js';
16
+ import { createAriaSnapshot } from './aria.js';
17
+ import { createCookieManager } from './page.js';
18
+ import { runSteps } from './runner.js';
19
+
20
+ const ErrorType = {
21
+ PARSE: 'PARSE',
22
+ VALIDATION: 'VALIDATION',
23
+ CONNECTION: 'CONNECTION',
24
+ EXECUTION: 'EXECUTION'
25
+ };
26
+
27
+ /**
28
+ * Reads entire stdin and returns as string
29
+ */
30
+ async function readStdin() {
31
+ return new Promise((resolve, reject) => {
32
+ const chunks = [];
33
+
34
+ process.stdin.setEncoding('utf8');
35
+
36
+ process.stdin.on('data', chunk => {
37
+ chunks.push(chunk);
38
+ });
39
+
40
+ process.stdin.on('end', () => {
41
+ resolve(chunks.join(''));
42
+ });
43
+
44
+ process.stdin.on('error', err => {
45
+ reject(err);
46
+ });
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Parses JSON input and validates basic structure
52
+ */
53
+ function parseInput(input) {
54
+ const trimmed = input.trim();
55
+
56
+ if (!trimmed) {
57
+ throw { type: ErrorType.PARSE, message: 'Empty input' };
58
+ }
59
+
60
+ let json;
61
+ try {
62
+ json = JSON.parse(trimmed);
63
+ } catch (err) {
64
+ throw { type: ErrorType.PARSE, message: `Invalid JSON: ${err.message}` };
65
+ }
66
+
67
+ if (!json || typeof json !== 'object') {
68
+ throw { type: ErrorType.VALIDATION, message: 'Input must be a JSON object' };
69
+ }
70
+
71
+ if (!json.steps) {
72
+ throw { type: ErrorType.VALIDATION, message: 'Missing required "steps" array' };
73
+ }
74
+
75
+ if (!Array.isArray(json.steps)) {
76
+ throw { type: ErrorType.VALIDATION, message: '"steps" must be an array' };
77
+ }
78
+
79
+ if (json.steps.length === 0) {
80
+ throw { type: ErrorType.VALIDATION, message: '"steps" array cannot be empty' };
81
+ }
82
+
83
+ return json;
84
+ }
85
+
86
+ /**
87
+ * Creates error response JSON
88
+ */
89
+ function errorResponse(type, message) {
90
+ return {
91
+ status: 'error',
92
+ error: { type, message }
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Main CLI execution
98
+ */
99
+ async function main() {
100
+ let browser = null;
101
+ let pageController = null;
102
+
103
+ try {
104
+ // Read and parse input
105
+ const input = await readStdin();
106
+ const json = parseInput(input);
107
+
108
+ // Extract config with defaults
109
+ const config = json.config || {};
110
+ const host = config.host || 'localhost';
111
+ const port = config.port || 9222;
112
+ const timeout = config.timeout || 30000;
113
+
114
+ // Connect to browser
115
+ browser = createBrowser({ host, port, connectTimeout: timeout });
116
+
117
+ try {
118
+ await browser.connect();
119
+ } catch (err) {
120
+ throw {
121
+ type: ErrorType.CONNECTION,
122
+ message: `Chrome not running on ${host}:${port} - ${err.message}`
123
+ };
124
+ }
125
+
126
+ // Get or create page session
127
+ let session;
128
+ if (config.targetId) {
129
+ try {
130
+ session = await browser.attachToPage(config.targetId);
131
+ } catch (err) {
132
+ throw {
133
+ type: ErrorType.CONNECTION,
134
+ message: `Could not attach to tab ${config.targetId}: ${err.message}`
135
+ };
136
+ }
137
+ } else {
138
+ try {
139
+ // If Chrome was just started, it has an empty tab - reuse it instead of creating another
140
+ // Otherwise create a new tab for this test session
141
+ // User should pass targetId from response to subsequent calls to reuse the same tab
142
+ const emptyTab = await browser.findPage(/^(about:blank|chrome:\/\/newtab)/);
143
+ if (emptyTab) {
144
+ session = emptyTab;
145
+ } else {
146
+ session = await browser.newPage();
147
+ }
148
+ } catch (err) {
149
+ // Check if Chrome has no tabs open (common when started with --remote-debugging-port)
150
+ if (err.message.includes('no browser is open')) {
151
+ throw {
152
+ type: ErrorType.CONNECTION,
153
+ message: `Chrome has no tabs open. This can happen when Chrome is started with --remote-debugging-port but no window is visible. Try opening a new Chrome window or restarting Chrome normally.`
154
+ };
155
+ }
156
+ throw {
157
+ type: ErrorType.EXECUTION,
158
+ message: `Failed to create new tab: ${err.message}`
159
+ };
160
+ }
161
+ }
162
+
163
+ // Create dependencies
164
+ pageController = createPageController(session);
165
+ const elementLocator = createElementLocator(session);
166
+ const inputEmulator = createInputEmulator(session);
167
+ const screenshotCapture = createScreenshotCapture(session);
168
+ const consoleCapture = createConsoleCapture(session);
169
+ const pdfCapture = createPdfCapture(session);
170
+ const ariaSnapshot = createAriaSnapshot(session);
171
+ const cookieManager = createCookieManager(session);
172
+
173
+ // Initialize page controller (enables required CDP domains)
174
+ await pageController.initialize();
175
+
176
+ // Start console capture to collect logs during execution
177
+ await consoleCapture.startCapture();
178
+
179
+ const deps = {
180
+ browser,
181
+ pageController,
182
+ elementLocator,
183
+ inputEmulator,
184
+ screenshotCapture,
185
+ consoleCapture,
186
+ pdfCapture,
187
+ ariaSnapshot,
188
+ cookieManager
189
+ };
190
+
191
+ // Run steps
192
+ const result = await runSteps(deps, json.steps, {
193
+ stopOnError: true,
194
+ stepTimeout: timeout
195
+ });
196
+
197
+ // Build output with tab info
198
+ const viewport = await pageController.getViewport();
199
+ const output = {
200
+ status: result.status,
201
+ tab: {
202
+ targetId: session.targetId,
203
+ url: await pageController.getUrl(),
204
+ title: await pageController.getTitle(),
205
+ viewport
206
+ },
207
+ steps: result.steps,
208
+ outputs: result.outputs,
209
+ errors: result.errors,
210
+ screenshots: result.screenshots
211
+ };
212
+
213
+ // Output result
214
+ console.log(JSON.stringify(output));
215
+
216
+ // Cleanup
217
+ await consoleCapture.stopCapture();
218
+ pageController.dispose();
219
+ await browser.disconnect();
220
+
221
+ process.exit(result.status === 'passed' ? 0 : 1);
222
+
223
+ } catch (err) {
224
+ // Cleanup on error
225
+ if (pageController) {
226
+ try { pageController.dispose(); } catch (e) { /* ignore */ }
227
+ }
228
+ if (browser) {
229
+ try { await browser.disconnect(); } catch (e) { /* ignore */ }
230
+ }
231
+
232
+ // Handle known error types
233
+ if (err.type) {
234
+ console.log(JSON.stringify(errorResponse(err.type, err.message)));
235
+ } else {
236
+ // Unknown error
237
+ console.log(JSON.stringify(errorResponse(ErrorType.EXECUTION, err.message || String(err))));
238
+ }
239
+
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ main();