glidercli 0.2.0 → 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 +615 -68
- package/lib/bexplore.js +815 -0
- package/lib/bextract.js +236 -0
- package/lib/bfetch.js +274 -0
- package/lib/bserve.js +43 -7
- 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 +1 -1
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();
|
package/lib/bwindow.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bwindow.js - Multi-window/tab management for Glider
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* glider window new [url] Create new browser window
|
|
7
|
+
* glider window close <targetId> Close specific tab/window
|
|
8
|
+
* glider window closeall Close all tabs created by Glider
|
|
9
|
+
* glider window list List all windows/tabs
|
|
10
|
+
* glider window focus <targetId> Bring tab to foreground
|
|
11
|
+
*
|
|
12
|
+
* The key insight: tabs created with newWindow:true CAN be closed via Target.closeTarget
|
|
13
|
+
* Tabs created in the main window CANNOT be closed (Chrome security)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const WebSocket = require('ws');
|
|
17
|
+
|
|
18
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
19
|
+
|
|
20
|
+
class WindowManager {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.ws = null;
|
|
23
|
+
this.messageId = 0;
|
|
24
|
+
this.pending = new Map();
|
|
25
|
+
this.targets = new Map(); // targetId -> { sessionId, url, windowId, createdByGlider }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async connect() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
31
|
+
this.ws.on('open', resolve);
|
|
32
|
+
this.ws.on('error', reject);
|
|
33
|
+
this.ws.on('message', (data) => this._handleMessage(JSON.parse(data.toString())));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_handleMessage(msg) {
|
|
38
|
+
if (msg.id !== undefined) {
|
|
39
|
+
const pending = this.pending.get(msg.id);
|
|
40
|
+
if (pending) {
|
|
41
|
+
this.pending.delete(msg.id);
|
|
42
|
+
msg.error ? pending.reject(new Error(JSON.stringify(msg.error))) : pending.resolve(msg.result);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Track targets
|
|
48
|
+
if (msg.method === 'Target.targetCreated') {
|
|
49
|
+
const info = msg.params.targetInfo;
|
|
50
|
+
if (info.type === 'page') {
|
|
51
|
+
this.targets.set(info.targetId, {
|
|
52
|
+
targetId: info.targetId,
|
|
53
|
+
url: info.url,
|
|
54
|
+
type: info.type
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
60
|
+
const { sessionId, targetInfo } = msg.params;
|
|
61
|
+
if (this.targets.has(targetInfo.targetId)) {
|
|
62
|
+
this.targets.get(targetInfo.targetId).sessionId = sessionId;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (msg.method === 'Target.targetDestroyed') {
|
|
67
|
+
this.targets.delete(msg.params.targetId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async send(method, params = {}) {
|
|
72
|
+
const id = ++this.messageId;
|
|
73
|
+
const msg = { id, method, params };
|
|
74
|
+
this.ws.send(JSON.stringify(msg));
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
this.pending.delete(id);
|
|
79
|
+
reject(new Error(`Timeout: ${method}`));
|
|
80
|
+
}, 30000);
|
|
81
|
+
this.pending.set(id, {
|
|
82
|
+
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
83
|
+
reject: (e) => { clearTimeout(timer); reject(e); }
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async init() {
|
|
89
|
+
// Enable target discovery
|
|
90
|
+
await this.send('Target.setDiscoverTargets', { discover: true });
|
|
91
|
+
await new Promise(r => setTimeout(r, 300));
|
|
92
|
+
|
|
93
|
+
// Get existing targets
|
|
94
|
+
try {
|
|
95
|
+
const { targetInfos } = await this.send('Target.getTargets');
|
|
96
|
+
for (const info of targetInfos) {
|
|
97
|
+
if (info.type === 'page') {
|
|
98
|
+
this.targets.set(info.targetId, {
|
|
99
|
+
targetId: info.targetId,
|
|
100
|
+
url: info.url,
|
|
101
|
+
type: info.type
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// Target.getTargets may not be supported by relay
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a new browser window (not just a tab)
|
|
112
|
+
* Tabs in this window CAN be closed via Target.closeTarget
|
|
113
|
+
*/
|
|
114
|
+
async createWindow(url = 'about:blank') {
|
|
115
|
+
const { targetId } = await this.send('Target.createTarget', {
|
|
116
|
+
url,
|
|
117
|
+
newWindow: true // CRITICAL: creates separate window
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Wait for target to be ready
|
|
121
|
+
await new Promise(r => setTimeout(r, 500));
|
|
122
|
+
|
|
123
|
+
// Attach to get sessionId
|
|
124
|
+
try {
|
|
125
|
+
const { sessionId } = await this.send('Target.attachToTarget', {
|
|
126
|
+
targetId,
|
|
127
|
+
flatten: true
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.targets.set(targetId, {
|
|
131
|
+
targetId,
|
|
132
|
+
sessionId,
|
|
133
|
+
url,
|
|
134
|
+
createdByGlider: true,
|
|
135
|
+
isWindow: true
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { targetId, sessionId };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// May already be attached
|
|
141
|
+
return { targetId };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a tab in the current window (will go to focused window)
|
|
147
|
+
*/
|
|
148
|
+
async createTab(url = 'about:blank') {
|
|
149
|
+
const { targetId } = await this.send('Target.createTarget', { url });
|
|
150
|
+
await new Promise(r => setTimeout(r, 500));
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const { sessionId } = await this.send('Target.attachToTarget', {
|
|
154
|
+
targetId,
|
|
155
|
+
flatten: true
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.targets.set(targetId, {
|
|
159
|
+
targetId,
|
|
160
|
+
sessionId,
|
|
161
|
+
url,
|
|
162
|
+
createdByGlider: true,
|
|
163
|
+
isWindow: false
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { targetId, sessionId };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { targetId };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Close a specific tab/window
|
|
174
|
+
* Only works for tabs created with newWindow:true or via Target.createTarget
|
|
175
|
+
*/
|
|
176
|
+
async closeTarget(targetId) {
|
|
177
|
+
try {
|
|
178
|
+
await this.send('Target.closeTarget', { targetId });
|
|
179
|
+
this.targets.delete(targetId);
|
|
180
|
+
return { success: true, targetId };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Fallback: try window.close() via Runtime.evaluate
|
|
183
|
+
const target = this.targets.get(targetId);
|
|
184
|
+
if (target?.sessionId) {
|
|
185
|
+
try {
|
|
186
|
+
await this.send('Runtime.evaluate', {
|
|
187
|
+
expression: 'window.close()',
|
|
188
|
+
returnByValue: true
|
|
189
|
+
}, target.sessionId);
|
|
190
|
+
this.targets.delete(targetId);
|
|
191
|
+
return { success: true, targetId, method: 'window.close' };
|
|
192
|
+
} catch (e2) {
|
|
193
|
+
return { success: false, targetId, error: e2.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { success: false, targetId, error: e.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Close all tabs created by Glider
|
|
202
|
+
*/
|
|
203
|
+
async closeAll() {
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const [targetId, info] of this.targets) {
|
|
206
|
+
if (info.createdByGlider) {
|
|
207
|
+
const result = await this.closeTarget(targetId);
|
|
208
|
+
results.push(result);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Bring a tab to the foreground
|
|
216
|
+
*/
|
|
217
|
+
async focusTarget(targetId) {
|
|
218
|
+
try {
|
|
219
|
+
await this.send('Target.activateTarget', { targetId });
|
|
220
|
+
return { success: true, targetId };
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { success: false, targetId, error: e.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* List all targets
|
|
228
|
+
*/
|
|
229
|
+
list() {
|
|
230
|
+
return Array.from(this.targets.values());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
close() {
|
|
234
|
+
if (this.ws) this.ws.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// CLI
|
|
239
|
+
async function main() {
|
|
240
|
+
const args = process.argv.slice(2);
|
|
241
|
+
const cmd = args[0];
|
|
242
|
+
|
|
243
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
244
|
+
console.log(`
|
|
245
|
+
bwindow - Multi-window/tab management for Glider
|
|
246
|
+
|
|
247
|
+
Commands:
|
|
248
|
+
new [url] Create new browser window (closeable)
|
|
249
|
+
tab [url] Create new tab in current window
|
|
250
|
+
close <targetId> Close specific tab/window
|
|
251
|
+
closeall Close all Glider-created tabs
|
|
252
|
+
list List all windows/tabs
|
|
253
|
+
focus <targetId> Bring tab to foreground
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
bwindow new https://google.com
|
|
257
|
+
bwindow close ABC123DEF456
|
|
258
|
+
bwindow list
|
|
259
|
+
`);
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const wm = new WindowManager();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await wm.connect();
|
|
267
|
+
await wm.init();
|
|
268
|
+
|
|
269
|
+
switch (cmd) {
|
|
270
|
+
case 'new':
|
|
271
|
+
case 'window': {
|
|
272
|
+
const url = args[1] || 'about:blank';
|
|
273
|
+
const result = await wm.createWindow(url);
|
|
274
|
+
console.log(JSON.stringify(result, null, 2));
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'tab': {
|
|
279
|
+
const url = args[1] || 'about:blank';
|
|
280
|
+
const result = await wm.createTab(url);
|
|
281
|
+
console.log(JSON.stringify(result, null, 2));
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'close': {
|
|
286
|
+
const targetId = args[1];
|
|
287
|
+
if (!targetId) {
|
|
288
|
+
console.error('Error: targetId required');
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const result = await wm.closeTarget(targetId);
|
|
292
|
+
console.log(JSON.stringify(result, null, 2));
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'closeall': {
|
|
297
|
+
const results = await wm.closeAll();
|
|
298
|
+
console.log(JSON.stringify(results, null, 2));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'list': {
|
|
303
|
+
const targets = wm.list();
|
|
304
|
+
console.log(JSON.stringify(targets, null, 2));
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case 'focus': {
|
|
309
|
+
const targetId = args[1];
|
|
310
|
+
if (!targetId) {
|
|
311
|
+
console.error('Error: targetId required');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
const result = await wm.focusTarget(targetId);
|
|
315
|
+
console.log(JSON.stringify(result, null, 2));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
default:
|
|
320
|
+
console.error(`Unknown command: ${cmd}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(`Error: ${err.message}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
} finally {
|
|
327
|
+
wm.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
main();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { WindowManager };
|