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/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 };