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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cdp-direct.js - Direct Chrome DevTools Protocol connection
|
|
4
|
+
* No relay, no extension, no bullshit. Just straight to Chrome.
|
|
5
|
+
*
|
|
6
|
+
* Chrome must be running with --remote-debugging-port=9222
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
const http = require('http');
|
|
11
|
+
|
|
12
|
+
const DEBUG_PORT = process.env.GLIDER_DEBUG_PORT || 9222;
|
|
13
|
+
const DEBUG_HOST = '127.0.0.1';
|
|
14
|
+
|
|
15
|
+
class DirectCDP {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.messageId = 0;
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
this.targetId = null;
|
|
21
|
+
this.sessionId = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get list of debuggable targets from Chrome
|
|
25
|
+
async getTargets() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
http.get(`http://${DEBUG_HOST}:${DEBUG_PORT}/json/list`, (res) => {
|
|
28
|
+
let data = '';
|
|
29
|
+
res.on('data', chunk => data += chunk);
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try {
|
|
32
|
+
resolve(JSON.parse(data));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
reject(new Error('Failed to parse targets'));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}).on('error', reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get Chrome version info
|
|
42
|
+
async getVersion() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
http.get(`http://${DEBUG_HOST}:${DEBUG_PORT}/json/version`, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', chunk => data += chunk);
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
resolve(JSON.parse(data));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
reject(new Error('Failed to parse version'));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}).on('error', reject);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Connect to a specific target (tab)
|
|
59
|
+
async connect(targetOrUrl) {
|
|
60
|
+
let wsUrl;
|
|
61
|
+
|
|
62
|
+
if (typeof targetOrUrl === 'string' && targetOrUrl.startsWith('ws://')) {
|
|
63
|
+
wsUrl = targetOrUrl;
|
|
64
|
+
} else {
|
|
65
|
+
// Find target by URL pattern or use first page
|
|
66
|
+
const targets = await this.getTargets();
|
|
67
|
+
let target;
|
|
68
|
+
|
|
69
|
+
if (typeof targetOrUrl === 'string') {
|
|
70
|
+
target = targets.find(t => t.url?.includes(targetOrUrl) && t.type === 'page');
|
|
71
|
+
}
|
|
72
|
+
if (!target) {
|
|
73
|
+
target = targets.find(t => t.type === 'page' && !t.url?.startsWith('chrome://') && !t.url?.startsWith('devtools://'));
|
|
74
|
+
}
|
|
75
|
+
if (!target) {
|
|
76
|
+
target = targets.find(t => t.type === 'page');
|
|
77
|
+
}
|
|
78
|
+
if (!target) {
|
|
79
|
+
throw new Error('No debuggable page found');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
wsUrl = target.webSocketDebuggerUrl;
|
|
83
|
+
this.targetId = target.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
this.ws = new WebSocket(wsUrl);
|
|
88
|
+
|
|
89
|
+
this.ws.on('open', async () => {
|
|
90
|
+
// Enable required domains
|
|
91
|
+
await this.send('Runtime.enable');
|
|
92
|
+
await this.send('Page.enable');
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.ws.on('error', reject);
|
|
97
|
+
|
|
98
|
+
this.ws.on('message', (data) => {
|
|
99
|
+
const msg = JSON.parse(data.toString());
|
|
100
|
+
if (msg.id !== undefined) {
|
|
101
|
+
const pending = this.pending.get(msg.id);
|
|
102
|
+
if (pending) {
|
|
103
|
+
this.pending.delete(msg.id);
|
|
104
|
+
if (msg.error) {
|
|
105
|
+
pending.reject(new Error(msg.error.message));
|
|
106
|
+
} else {
|
|
107
|
+
pending.resolve(msg.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.ws.on('close', () => {
|
|
114
|
+
this.ws = null;
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Send CDP command
|
|
120
|
+
async send(method, params = {}) {
|
|
121
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
122
|
+
throw new Error('Not connected');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const id = ++this.messageId;
|
|
126
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
this.pending.delete(id);
|
|
131
|
+
reject(new Error(`Timeout: ${method}`));
|
|
132
|
+
}, 30000);
|
|
133
|
+
|
|
134
|
+
this.pending.set(id, {
|
|
135
|
+
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
136
|
+
reject: (e) => { clearTimeout(timer); reject(e); }
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// High-level helpers
|
|
142
|
+
async evaluate(expression) {
|
|
143
|
+
const result = await this.send('Runtime.evaluate', {
|
|
144
|
+
expression,
|
|
145
|
+
returnByValue: true,
|
|
146
|
+
awaitPromise: true
|
|
147
|
+
});
|
|
148
|
+
if (result.exceptionDetails) {
|
|
149
|
+
throw new Error(result.exceptionDetails.text || 'Evaluation failed');
|
|
150
|
+
}
|
|
151
|
+
return result.result?.value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async navigate(url) {
|
|
155
|
+
return this.send('Page.navigate', { url });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async screenshot(format = 'png') {
|
|
159
|
+
return this.send('Page.captureScreenshot', { format });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getTitle() {
|
|
163
|
+
return this.evaluate('document.title');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getUrl() {
|
|
167
|
+
return this.evaluate('window.location.href');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getText() {
|
|
171
|
+
return this.evaluate('document.body.innerText');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getHtml(selector) {
|
|
175
|
+
if (selector) {
|
|
176
|
+
return this.evaluate(`document.querySelector('${selector}')?.outerHTML`);
|
|
177
|
+
}
|
|
178
|
+
return this.evaluate('document.documentElement.outerHTML');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async click(selector) {
|
|
182
|
+
return this.evaluate(`
|
|
183
|
+
(() => {
|
|
184
|
+
const el = document.querySelector('${selector}');
|
|
185
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
186
|
+
el.click();
|
|
187
|
+
return true;
|
|
188
|
+
})()
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async type(selector, text) {
|
|
193
|
+
return this.evaluate(`
|
|
194
|
+
(() => {
|
|
195
|
+
const el = document.querySelector('${selector}');
|
|
196
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
197
|
+
el.focus();
|
|
198
|
+
el.value = '${text.replace(/'/g, "\\'")}';
|
|
199
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
200
|
+
return true;
|
|
201
|
+
})()
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
close() {
|
|
206
|
+
if (this.ws) {
|
|
207
|
+
this.ws.close();
|
|
208
|
+
this.ws = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if Chrome debugging is available
|
|
214
|
+
async function checkChrome() {
|
|
215
|
+
try {
|
|
216
|
+
const cdp = new DirectCDP();
|
|
217
|
+
const version = await cdp.getVersion();
|
|
218
|
+
return { ok: true, version };
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { ok: false, error: e.message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Export for use as module
|
|
225
|
+
module.exports = { DirectCDP, checkChrome, DEBUG_PORT, DEBUG_HOST };
|
|
226
|
+
|
|
227
|
+
// CLI mode
|
|
228
|
+
if (require.main === module) {
|
|
229
|
+
const cmd = process.argv[2];
|
|
230
|
+
const arg = process.argv.slice(3).join(' ');
|
|
231
|
+
|
|
232
|
+
(async () => {
|
|
233
|
+
const cdp = new DirectCDP();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (cmd === 'check' || cmd === 'status') {
|
|
237
|
+
const check = await checkChrome();
|
|
238
|
+
if (check.ok) {
|
|
239
|
+
console.log('Chrome debugging available');
|
|
240
|
+
console.log('Browser:', check.version.Browser);
|
|
241
|
+
const targets = await cdp.getTargets();
|
|
242
|
+
console.log('Tabs:', targets.filter(t => t.type === 'page').length);
|
|
243
|
+
} else {
|
|
244
|
+
console.error('Chrome debugging not available:', check.error);
|
|
245
|
+
console.error('Run: glider chrome-start');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (cmd === 'targets' || cmd === 'tabs') {
|
|
252
|
+
const targets = await cdp.getTargets();
|
|
253
|
+
targets.filter(t => t.type === 'page').forEach((t, i) => {
|
|
254
|
+
console.log(`[${i + 1}] ${t.title}`);
|
|
255
|
+
console.log(` ${t.url}`);
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Commands that need connection
|
|
261
|
+
await cdp.connect();
|
|
262
|
+
|
|
263
|
+
switch (cmd) {
|
|
264
|
+
case 'eval':
|
|
265
|
+
const result = await cdp.evaluate(arg || 'document.title');
|
|
266
|
+
console.log(JSON.stringify(result, null, 2));
|
|
267
|
+
break;
|
|
268
|
+
case 'title':
|
|
269
|
+
console.log(await cdp.getTitle());
|
|
270
|
+
break;
|
|
271
|
+
case 'url':
|
|
272
|
+
console.log(await cdp.getUrl());
|
|
273
|
+
break;
|
|
274
|
+
case 'text':
|
|
275
|
+
console.log(await cdp.getText());
|
|
276
|
+
break;
|
|
277
|
+
case 'html':
|
|
278
|
+
console.log(await cdp.getHtml(arg));
|
|
279
|
+
break;
|
|
280
|
+
case 'goto':
|
|
281
|
+
await cdp.navigate(arg);
|
|
282
|
+
console.log('Navigated to:', arg);
|
|
283
|
+
break;
|
|
284
|
+
case 'click':
|
|
285
|
+
await cdp.click(arg);
|
|
286
|
+
console.log('Clicked:', arg);
|
|
287
|
+
break;
|
|
288
|
+
case 'screenshot':
|
|
289
|
+
const ss = await cdp.screenshot();
|
|
290
|
+
const path = arg || `/tmp/screenshot-${Date.now()}.png`;
|
|
291
|
+
require('fs').writeFileSync(path, Buffer.from(ss.data, 'base64'));
|
|
292
|
+
console.log('Screenshot saved:', path);
|
|
293
|
+
break;
|
|
294
|
+
default:
|
|
295
|
+
console.log('Usage: cdp-direct <command> [args]');
|
|
296
|
+
console.log('Commands: check, targets, eval, title, url, text, html, goto, click, screenshot');
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.error('Error:', e.message);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
} finally {
|
|
302
|
+
cdp.close();
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Glider daemon - respawns relay forever, fuck launchd throttling
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
BSERVE="$SCRIPT_DIR/bserve.js"
|
|
6
|
+
LOG_DIR="$HOME/.glider"
|
|
7
|
+
PID_FILE="$LOG_DIR/daemon.pid"
|
|
8
|
+
|
|
9
|
+
mkdir -p "$LOG_DIR"
|
|
10
|
+
|
|
11
|
+
# Kill any existing
|
|
12
|
+
if [ -f "$PID_FILE" ]; then
|
|
13
|
+
kill $(cat "$PID_FILE") 2>/dev/null
|
|
14
|
+
rm "$PID_FILE"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo $$ > "$PID_FILE"
|
|
18
|
+
|
|
19
|
+
cleanup() {
|
|
20
|
+
rm -f "$PID_FILE"
|
|
21
|
+
exit 0
|
|
22
|
+
}
|
|
23
|
+
trap cleanup SIGTERM SIGINT
|
|
24
|
+
|
|
25
|
+
while true; do
|
|
26
|
+
echo "[$(date)] Starting relay..." >> "$LOG_DIR/daemon.log"
|
|
27
|
+
node "$BSERVE" >> "$LOG_DIR/daemon.log" 2>&1
|
|
28
|
+
EXIT_CODE=$?
|
|
29
|
+
echo "[$(date)] Relay exited with code $EXIT_CODE, restarting in 2s..." >> "$LOG_DIR/daemon.log"
|
|
30
|
+
sleep 2
|
|
31
|
+
done
|
package/package.json
CHANGED