glidercli 0.1.5 → 0.3.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/README.md +70 -9
- package/bin/glider.js +785 -57
- package/lib/bcdp.js +482 -0
- package/lib/beval.js +78 -0
- package/lib/bexplore.js +815 -0
- package/lib/bextract.js +236 -0
- package/lib/bfetch.js +274 -0
- package/lib/bserve.js +347 -0
- package/lib/bspawn.js +154 -0
- package/lib/bwindow.js +335 -0
- package/lib/cdp-direct.js +305 -0
- package/lib/glider-daemon.sh +31 -0
- package/package.json +8 -2
- package/.git-personal-enforced +0 -4
- package/.github/hooks/post-checkout +0 -24
- package/.github/hooks/post-commit +0 -13
- package/.github/hooks/pre-commit +0 -30
- package/.github/hooks/pre-push +0 -13
- package/.github/scripts/health-check.sh +0 -127
- package/.github/scripts/setup.sh +0 -19
- package/.github/workflows/release.yml +0 -19
- package/assets/icons/.gitkeep +0 -0
- package/assets/icons/claude.webp +0 -0
- package/assets/icons/glider-blue-squircle.webp +0 -0
- package/assets/icons/ralph-wiggum.webp +0 -0
- package/repo.config.json +0 -31
package/lib/bserve.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* browser-relay-server.js
|
|
4
|
+
* Minimal CDP relay server - connects to Chrome extension for browser automation
|
|
5
|
+
* Based on playwriter architecture but stripped down for direct scripting
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { WebSocketServer, WebSocket } = require('ws');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
|
|
11
|
+
const PORT = process.env.RELAY_PORT || 19988;
|
|
12
|
+
const HOST = '127.0.0.1';
|
|
13
|
+
|
|
14
|
+
// State
|
|
15
|
+
let extensionWs = null;
|
|
16
|
+
const playwrightClients = new Map();
|
|
17
|
+
const connectedTargets = new Map();
|
|
18
|
+
const pendingRequests = new Map();
|
|
19
|
+
let messageId = 0;
|
|
20
|
+
|
|
21
|
+
// Create HTTP server
|
|
22
|
+
const server = http.createServer((req, res) => {
|
|
23
|
+
if (req.url === '/') {
|
|
24
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
25
|
+
res.end('OK');
|
|
26
|
+
} else if (req.url === '/status') {
|
|
27
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
28
|
+
res.end(JSON.stringify({
|
|
29
|
+
extension: extensionWs !== null,
|
|
30
|
+
targets: connectedTargets.size,
|
|
31
|
+
clients: playwrightClients.size
|
|
32
|
+
}));
|
|
33
|
+
} else if (req.url === '/targets') {
|
|
34
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
35
|
+
res.end(JSON.stringify(Array.from(connectedTargets.values())));
|
|
36
|
+
} else if (req.url === '/attach' && req.method === 'POST') {
|
|
37
|
+
// Trigger extension to attach active tab
|
|
38
|
+
(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await sendToExtension({ method: 'attachActiveTab', params: {} });
|
|
41
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify(result));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
} else if (req.url === '/cdp' && req.method === 'POST') {
|
|
49
|
+
// HTTP POST endpoint for CDP commands
|
|
50
|
+
let body = '';
|
|
51
|
+
req.on('data', chunk => body += chunk);
|
|
52
|
+
req.on('end', async () => {
|
|
53
|
+
try {
|
|
54
|
+
const { method, params, sessionId } = JSON.parse(body);
|
|
55
|
+
const result = await routeCDPCommand({ method, params, sessionId });
|
|
56
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify(result));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
60
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
res.writeHead(404);
|
|
65
|
+
res.end('Not Found');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// WebSocket server
|
|
70
|
+
const wss = new WebSocketServer({ server });
|
|
71
|
+
|
|
72
|
+
wss.on('connection', (ws, req) => {
|
|
73
|
+
const path = req.url;
|
|
74
|
+
|
|
75
|
+
if (path === '/extension') {
|
|
76
|
+
handleExtensionConnection(ws);
|
|
77
|
+
} else if (path.startsWith('/cdp')) {
|
|
78
|
+
const clientId = path.split('/')[2] || 'default';
|
|
79
|
+
handleCDPConnection(ws, clientId);
|
|
80
|
+
} else {
|
|
81
|
+
ws.close(1000, 'Unknown path');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function handleExtensionConnection(ws) {
|
|
86
|
+
if (extensionWs) {
|
|
87
|
+
console.log('[relay] Replacing existing extension connection');
|
|
88
|
+
extensionWs.close(4001, 'Replaced');
|
|
89
|
+
connectedTargets.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
extensionWs = ws;
|
|
93
|
+
console.log('[relay] Extension connected');
|
|
94
|
+
|
|
95
|
+
// Ping to keep alive
|
|
96
|
+
const pingInterval = setInterval(() => {
|
|
97
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
98
|
+
ws.send(JSON.stringify({ method: 'ping' }));
|
|
99
|
+
}
|
|
100
|
+
}, 5000);
|
|
101
|
+
|
|
102
|
+
ws.on('message', (data) => {
|
|
103
|
+
try {
|
|
104
|
+
const msg = JSON.parse(data.toString());
|
|
105
|
+
handleExtensionMessage(msg);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error('[relay] Error parsing extension message:', e);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ws.on('close', () => {
|
|
112
|
+
console.log('[relay] Extension disconnected');
|
|
113
|
+
clearInterval(pingInterval);
|
|
114
|
+
extensionWs = null;
|
|
115
|
+
connectedTargets.clear();
|
|
116
|
+
|
|
117
|
+
// Notify all clients
|
|
118
|
+
for (const client of playwrightClients.values()) {
|
|
119
|
+
client.ws.close(1000, 'Extension disconnected');
|
|
120
|
+
}
|
|
121
|
+
playwrightClients.clear();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleExtensionMessage(msg) {
|
|
126
|
+
console.log('[relay] Extension message:', JSON.stringify(msg).slice(0, 200));
|
|
127
|
+
|
|
128
|
+
// Response to our request
|
|
129
|
+
if (msg.id !== undefined) {
|
|
130
|
+
const pending = pendingRequests.get(msg.id);
|
|
131
|
+
if (pending) {
|
|
132
|
+
pendingRequests.delete(msg.id);
|
|
133
|
+
if (msg.error) {
|
|
134
|
+
pending.reject(new Error(msg.error));
|
|
135
|
+
} else {
|
|
136
|
+
pending.resolve(msg.result);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pong
|
|
143
|
+
if (msg.method === 'pong') return;
|
|
144
|
+
|
|
145
|
+
// Log from extension
|
|
146
|
+
if (msg.method === 'log') {
|
|
147
|
+
console.log(`[ext:${msg.params.level}]`, ...msg.params.args);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// CDP event from extension
|
|
152
|
+
if (msg.method === 'forwardCDPEvent') {
|
|
153
|
+
const { method, params, sessionId } = msg.params;
|
|
154
|
+
|
|
155
|
+
// Track targets
|
|
156
|
+
if (method === 'Target.attachedToTarget') {
|
|
157
|
+
connectedTargets.set(params.sessionId, {
|
|
158
|
+
sessionId: params.sessionId,
|
|
159
|
+
targetId: params.targetInfo.targetId,
|
|
160
|
+
targetInfo: params.targetInfo
|
|
161
|
+
});
|
|
162
|
+
console.log(`[relay] Target attached: ${params.targetInfo.url}`);
|
|
163
|
+
} else if (method === 'Target.detachedFromTarget') {
|
|
164
|
+
connectedTargets.delete(params.sessionId);
|
|
165
|
+
console.log(`[relay] Target detached: ${params.sessionId}`);
|
|
166
|
+
} else if (method === 'Target.targetInfoChanged') {
|
|
167
|
+
const target = Array.from(connectedTargets.values())
|
|
168
|
+
.find(t => t.targetId === params.targetInfo.targetId);
|
|
169
|
+
if (target) {
|
|
170
|
+
target.targetInfo = params.targetInfo;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Forward to all CDP clients
|
|
175
|
+
const cdpEvent = { method, params, sessionId };
|
|
176
|
+
for (const client of playwrightClients.values()) {
|
|
177
|
+
client.ws.send(JSON.stringify(cdpEvent));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleCDPConnection(ws, clientId) {
|
|
183
|
+
if (playwrightClients.has(clientId)) {
|
|
184
|
+
ws.close(1000, 'Client ID already connected');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
playwrightClients.set(clientId, { id: clientId, ws });
|
|
189
|
+
console.log(`[relay] CDP client connected: ${clientId}`);
|
|
190
|
+
|
|
191
|
+
ws.on('message', async (data) => {
|
|
192
|
+
try {
|
|
193
|
+
const msg = JSON.parse(data.toString());
|
|
194
|
+
const { id, method, params, sessionId } = msg;
|
|
195
|
+
|
|
196
|
+
if (!extensionWs) {
|
|
197
|
+
ws.send(JSON.stringify({ id, error: { message: 'Extension not connected' } }));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await routeCDPCommand({ method, params, sessionId });
|
|
203
|
+
ws.send(JSON.stringify({ id, sessionId, result }));
|
|
204
|
+
|
|
205
|
+
// Send attachedToTarget events after setAutoAttach
|
|
206
|
+
if (method === 'Target.setAutoAttach' && !sessionId) {
|
|
207
|
+
for (const target of connectedTargets.values()) {
|
|
208
|
+
ws.send(JSON.stringify({
|
|
209
|
+
method: 'Target.attachedToTarget',
|
|
210
|
+
params: {
|
|
211
|
+
sessionId: target.sessionId,
|
|
212
|
+
targetInfo: { ...target.targetInfo, attached: true },
|
|
213
|
+
waitingForDebugger: false
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
ws.send(JSON.stringify({ id, sessionId, error: { message: e.message } }));
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error('[relay] Error handling CDP message:', e);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
ws.on('close', () => {
|
|
227
|
+
playwrightClients.delete(clientId);
|
|
228
|
+
console.log(`[relay] CDP client disconnected: ${clientId}`);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function sendToExtension({ method, params, timeout = 30000 }) {
|
|
233
|
+
if (!extensionWs) throw new Error('Extension not connected');
|
|
234
|
+
|
|
235
|
+
const id = ++messageId;
|
|
236
|
+
extensionWs.send(JSON.stringify({ id, method, params }));
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const timer = setTimeout(() => {
|
|
240
|
+
pendingRequests.delete(id);
|
|
241
|
+
reject(new Error(`Timeout: ${method}`));
|
|
242
|
+
}, timeout);
|
|
243
|
+
|
|
244
|
+
pendingRequests.set(id, {
|
|
245
|
+
resolve: (result) => { clearTimeout(timer); resolve(result); },
|
|
246
|
+
reject: (error) => { clearTimeout(timer); reject(error); }
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function routeCDPCommand({ method, params, sessionId }) {
|
|
252
|
+
// Target.* commands that operate at browser level don't need sessionId
|
|
253
|
+
const browserLevelCommands = [
|
|
254
|
+
'Target.createTarget',
|
|
255
|
+
'Target.closeTarget',
|
|
256
|
+
'Target.activateTarget',
|
|
257
|
+
'Target.getTargets',
|
|
258
|
+
'Target.setAutoAttach',
|
|
259
|
+
'Target.setDiscoverTargets',
|
|
260
|
+
'Target.attachToTarget',
|
|
261
|
+
'Target.getTargetInfo',
|
|
262
|
+
'Browser.getVersion'
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
// Auto-pick first session only for session-scoped commands
|
|
266
|
+
if (!sessionId && connectedTargets.size > 0 && !browserLevelCommands.includes(method)) {
|
|
267
|
+
sessionId = Array.from(connectedTargets.values())[0].sessionId;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Handle some commands locally
|
|
271
|
+
switch (method) {
|
|
272
|
+
case 'Browser.getVersion':
|
|
273
|
+
return {
|
|
274
|
+
protocolVersion: '1.3',
|
|
275
|
+
product: 'Chrome/Extension-Bridge',
|
|
276
|
+
revision: '1.0.0',
|
|
277
|
+
userAgent: 'CDP-Bridge/1.0.0',
|
|
278
|
+
jsVersion: 'V8'
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
case 'Target.setAutoAttach':
|
|
282
|
+
case 'Target.setDiscoverTargets':
|
|
283
|
+
return {};
|
|
284
|
+
|
|
285
|
+
case 'Target.getTargets':
|
|
286
|
+
return {
|
|
287
|
+
targetInfos: Array.from(connectedTargets.values())
|
|
288
|
+
.map(t => ({ ...t.targetInfo, attached: true }))
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
case 'Target.attachToTarget':
|
|
292
|
+
// Forward to extension - it handles the actual attachment
|
|
293
|
+
// The extension will send back Target.attachedToTarget event
|
|
294
|
+
console.log(`[relay] Forwarding Target.attachToTarget to extension for target: ${params?.targetId}`);
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'Target.getTargetInfo':
|
|
298
|
+
if (params?.targetId) {
|
|
299
|
+
for (const target of connectedTargets.values()) {
|
|
300
|
+
if (target.targetId === params.targetId) {
|
|
301
|
+
return { targetInfo: target.targetInfo };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (sessionId) {
|
|
306
|
+
const target = connectedTargets.get(sessionId);
|
|
307
|
+
if (target) return { targetInfo: target.targetInfo };
|
|
308
|
+
}
|
|
309
|
+
const first = Array.from(connectedTargets.values())[0];
|
|
310
|
+
return { targetInfo: first?.targetInfo };
|
|
311
|
+
|
|
312
|
+
case 'Target.createTarget':
|
|
313
|
+
case 'Target.closeTarget':
|
|
314
|
+
case 'Target.activateTarget':
|
|
315
|
+
// These MUST go to extension - they control browser windows/tabs
|
|
316
|
+
console.log(`[relay] Forwarding ${method} to extension:`, JSON.stringify(params).slice(0, 100));
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Forward to extension
|
|
321
|
+
return await sendToExtension({
|
|
322
|
+
method: 'forwardCDPCommand',
|
|
323
|
+
params: { sessionId, method, params }
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Export for use as module
|
|
328
|
+
module.exports = { server, wss, routeCDPCommand };
|
|
329
|
+
|
|
330
|
+
// Start server if run directly
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
server.listen(PORT, HOST, () => {
|
|
333
|
+
console.log(`[relay] CDP relay server running on ws://${HOST}:${PORT}`);
|
|
334
|
+
console.log('[relay] Endpoints:');
|
|
335
|
+
console.log(` - Extension: ws://${HOST}:${PORT}/extension`);
|
|
336
|
+
console.log(` - CDP: ws://${HOST}:${PORT}/cdp`);
|
|
337
|
+
console.log(` - Status: http://${HOST}:${PORT}/status`);
|
|
338
|
+
console.log(` - Targets: http://${HOST}:${PORT}/targets`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
process.on('SIGINT', () => {
|
|
342
|
+
console.log('\n[relay] Shutting down...');
|
|
343
|
+
wss.close();
|
|
344
|
+
server.close();
|
|
345
|
+
process.exit(0);
|
|
346
|
+
});
|
|
347
|
+
}
|
package/lib/bspawn.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bspawn.js - Spawn multiple browser tabs in parallel via Glider
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bspawn <url1> <url2> ... # Spawn tabs for each URL
|
|
7
|
+
* bspawn -f urls.txt # Read URLs from file (one per line)
|
|
8
|
+
* bspawn --json '["url1","url2"]' # URLs as JSON array
|
|
9
|
+
* cat urls.txt | bspawn - # Read from stdin
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --wait <ms> Wait time after spawning (default: 3000)
|
|
13
|
+
* --status Show status after spawning
|
|
14
|
+
* --quiet Suppress output
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* bspawn https://example.com https://google.com
|
|
18
|
+
* bspawn -f /tmp/orr-urls.txt --wait 5000
|
|
19
|
+
* echo "https://example.com" | bspawn -
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const WebSocket = require('ws');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
26
|
+
const DEFAULT_WAIT = 3000;
|
|
27
|
+
|
|
28
|
+
async function spawnTabs(urls, options = {}) {
|
|
29
|
+
const { wait = DEFAULT_WAIT, quiet = false } = options;
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const ws = new WebSocket(RELAY_URL);
|
|
33
|
+
let id = 0;
|
|
34
|
+
const results = [];
|
|
35
|
+
|
|
36
|
+
ws.on('open', () => {
|
|
37
|
+
if (!quiet) console.error(`[bspawn] Spawning ${urls.length} tabs...`);
|
|
38
|
+
urls.forEach(url => {
|
|
39
|
+
ws.send(JSON.stringify({
|
|
40
|
+
id: ++id,
|
|
41
|
+
method: 'Target.createTarget',
|
|
42
|
+
params: { url }
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
ws.on('message', (data) => {
|
|
48
|
+
const msg = JSON.parse(data.toString());
|
|
49
|
+
if (msg.result?.targetId) {
|
|
50
|
+
results.push({ id: msg.id, targetId: msg.result.targetId });
|
|
51
|
+
if (!quiet) console.error(`[bspawn] Tab ${results.length}/${urls.length} created`);
|
|
52
|
+
}
|
|
53
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
54
|
+
if (!quiet) console.error(`[bspawn] Attached: ${msg.params?.targetInfo?.url?.substring(0, 60)}...`);
|
|
55
|
+
}
|
|
56
|
+
if (results.length === urls.length) {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
ws.close();
|
|
59
|
+
resolve(results);
|
|
60
|
+
}, wait);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ws.on('error', (err) => reject(new Error(`WebSocket error: ${err.message}`)));
|
|
65
|
+
|
|
66
|
+
// Timeout after 30s
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
ws.close();
|
|
69
|
+
if (results.length > 0) resolve(results);
|
|
70
|
+
else reject(new Error('Timeout waiting for tabs to spawn'));
|
|
71
|
+
}, 30000);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getStatus() {
|
|
76
|
+
const http = require('http');
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
http.get('http://127.0.0.1:19988/targets', (res) => {
|
|
79
|
+
let data = '';
|
|
80
|
+
res.on('data', chunk => data += chunk);
|
|
81
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
82
|
+
}).on('error', () => resolve([]));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const args = process.argv.slice(2);
|
|
88
|
+
let urls = [];
|
|
89
|
+
let wait = DEFAULT_WAIT;
|
|
90
|
+
let showStatus = false;
|
|
91
|
+
let quiet = false;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
const arg = args[i];
|
|
95
|
+
if (arg === '--wait' || arg === '-w') {
|
|
96
|
+
wait = parseInt(args[++i], 10);
|
|
97
|
+
} else if (arg === '--status' || arg === '-s') {
|
|
98
|
+
showStatus = true;
|
|
99
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
100
|
+
quiet = true;
|
|
101
|
+
} else if (arg === '-f' || arg === '--file') {
|
|
102
|
+
const file = args[++i];
|
|
103
|
+
urls = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean);
|
|
104
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
105
|
+
urls = JSON.parse(args[++i]);
|
|
106
|
+
} else if (arg === '-') {
|
|
107
|
+
// Read from stdin
|
|
108
|
+
urls = fs.readFileSync(0, 'utf8').trim().split('\n').filter(Boolean);
|
|
109
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
110
|
+
console.log(`
|
|
111
|
+
bspawn - Spawn multiple browser tabs in parallel via Glider
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
bspawn <url1> <url2> ... # Spawn tabs for each URL
|
|
115
|
+
bspawn -f urls.txt # Read URLs from file
|
|
116
|
+
bspawn --json '["url1","url2"]' # URLs as JSON array
|
|
117
|
+
cat urls.txt | bspawn - # Read from stdin
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
-w, --wait <ms> Wait time after spawning (default: 3000)
|
|
121
|
+
-s, --status Show status after spawning
|
|
122
|
+
-q, --quiet Suppress output
|
|
123
|
+
-h, --help Show this help
|
|
124
|
+
`);
|
|
125
|
+
process.exit(0);
|
|
126
|
+
} else if (arg.startsWith('http')) {
|
|
127
|
+
urls.push(arg);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (urls.length === 0) {
|
|
132
|
+
console.error('Error: No URLs provided');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const results = await spawnTabs(urls, { wait, quiet });
|
|
138
|
+
|
|
139
|
+
if (showStatus) {
|
|
140
|
+
const targets = await getStatus();
|
|
141
|
+
console.log(JSON.stringify(targets.map(t => ({
|
|
142
|
+
sessionId: t.sessionId,
|
|
143
|
+
url: t.targetInfo?.url
|
|
144
|
+
})), null, 2));
|
|
145
|
+
} else if (!quiet) {
|
|
146
|
+
console.log(JSON.stringify(results, null, 2));
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`Error: ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main();
|