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/bextract.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bextract.js - Extract content from multiple browser tabs in parallel via Glider
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bextract # Extract from all connected tabs
|
|
7
|
+
* bextract --sessions s1,s2,s3 # Extract from specific sessions
|
|
8
|
+
* bextract --exclude session-1 # Exclude specific sessions
|
|
9
|
+
* bextract --js 'document.title' # Custom JS expression
|
|
10
|
+
* bextract --selector '.content' # Extract specific element
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --js <expr> JavaScript expression to evaluate (default: document.body.innerText)
|
|
14
|
+
* --selector <sel> CSS selector to extract
|
|
15
|
+
* --sessions <list> Comma-separated session IDs
|
|
16
|
+
* --exclude <list> Comma-separated sessions to exclude
|
|
17
|
+
* --limit <n> Max characters per result (default: 10000)
|
|
18
|
+
* --timeout <ms> Timeout per extraction (default: 15000)
|
|
19
|
+
* --json Output as JSON
|
|
20
|
+
* --quiet Suppress progress output
|
|
21
|
+
*
|
|
22
|
+
* Examples:
|
|
23
|
+
* bextract --exclude session-1 --limit 5000
|
|
24
|
+
* bextract --js 'document.title' --json
|
|
25
|
+
* bextract --selector 'article' --sessions session-2,session-3
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const WebSocket = require('ws');
|
|
29
|
+
const http = require('http');
|
|
30
|
+
|
|
31
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
32
|
+
const DEFAULT_LIMIT = 10000;
|
|
33
|
+
const DEFAULT_TIMEOUT = 15000;
|
|
34
|
+
|
|
35
|
+
async function getTargets() {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
http.get('http://127.0.0.1:19988/targets', (res) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
res.on('data', chunk => data += chunk);
|
|
40
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
41
|
+
}).on('error', () => resolve([]));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function extractFromSession(sessionId, jsExpr, options = {}) {
|
|
46
|
+
const { timeout = DEFAULT_TIMEOUT, limit = DEFAULT_LIMIT } = options;
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const ws = new WebSocket(RELAY_URL);
|
|
50
|
+
let resolved = false;
|
|
51
|
+
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
if (!resolved) {
|
|
54
|
+
resolved = true;
|
|
55
|
+
ws.close();
|
|
56
|
+
reject(new Error(`Timeout for ${sessionId}`));
|
|
57
|
+
}
|
|
58
|
+
}, timeout);
|
|
59
|
+
|
|
60
|
+
ws.on('open', () => {
|
|
61
|
+
ws.send(JSON.stringify({
|
|
62
|
+
id: 1,
|
|
63
|
+
sessionId,
|
|
64
|
+
method: 'Runtime.evaluate',
|
|
65
|
+
params: {
|
|
66
|
+
expression: jsExpr,
|
|
67
|
+
returnByValue: true
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
ws.on('message', (data) => {
|
|
73
|
+
const msg = JSON.parse(data.toString());
|
|
74
|
+
if (msg.id === 1 && !resolved) {
|
|
75
|
+
resolved = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
ws.close();
|
|
78
|
+
|
|
79
|
+
if (msg.error) {
|
|
80
|
+
reject(new Error(msg.error.message));
|
|
81
|
+
} else {
|
|
82
|
+
let value = msg.result?.result?.value;
|
|
83
|
+
if (typeof value === 'string' && value.length > limit) {
|
|
84
|
+
value = value.substring(0, limit) + `\n... [truncated at ${limit} chars]`;
|
|
85
|
+
}
|
|
86
|
+
resolve(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
ws.on('error', (err) => {
|
|
92
|
+
if (!resolved) {
|
|
93
|
+
resolved = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
reject(err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function extractParallel(sessions, jsExpr, options = {}) {
|
|
102
|
+
const { quiet = false } = options;
|
|
103
|
+
const results = {};
|
|
104
|
+
|
|
105
|
+
const promises = sessions.map(async ({ sessionId, url }) => {
|
|
106
|
+
try {
|
|
107
|
+
if (!quiet) console.error(`[bextract] Extracting from ${sessionId}...`);
|
|
108
|
+
const content = await extractFromSession(sessionId, jsExpr, options);
|
|
109
|
+
results[sessionId] = { url, content, error: null };
|
|
110
|
+
} catch (err) {
|
|
111
|
+
results[sessionId] = { url, content: null, error: err.message };
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await Promise.all(promises);
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const args = process.argv.slice(2);
|
|
121
|
+
|
|
122
|
+
let jsExpr = 'document.body.innerText';
|
|
123
|
+
let selector = null;
|
|
124
|
+
let sessions = null;
|
|
125
|
+
let exclude = [];
|
|
126
|
+
let limit = DEFAULT_LIMIT;
|
|
127
|
+
let timeout = DEFAULT_TIMEOUT;
|
|
128
|
+
let outputJson = false;
|
|
129
|
+
let quiet = false;
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < args.length; i++) {
|
|
132
|
+
const arg = args[i];
|
|
133
|
+
if (arg === '--js' || arg === '-e') {
|
|
134
|
+
jsExpr = args[++i];
|
|
135
|
+
} else if (arg === '--selector' || arg === '-s') {
|
|
136
|
+
selector = args[++i];
|
|
137
|
+
} else if (arg === '--sessions') {
|
|
138
|
+
sessions = args[++i].split(',');
|
|
139
|
+
} else if (arg === '--exclude' || arg === '-x') {
|
|
140
|
+
exclude = args[++i].split(',');
|
|
141
|
+
} else if (arg === '--limit' || arg === '-l') {
|
|
142
|
+
limit = parseInt(args[++i], 10);
|
|
143
|
+
} else if (arg === '--timeout' || arg === '-t') {
|
|
144
|
+
timeout = parseInt(args[++i], 10);
|
|
145
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
146
|
+
outputJson = true;
|
|
147
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
148
|
+
quiet = true;
|
|
149
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
150
|
+
console.log(`
|
|
151
|
+
bextract - Extract content from multiple browser tabs in parallel
|
|
152
|
+
|
|
153
|
+
Usage:
|
|
154
|
+
bextract # Extract from all connected tabs
|
|
155
|
+
bextract --sessions s1,s2,s3 # Extract from specific sessions
|
|
156
|
+
bextract --exclude session-1 # Exclude specific sessions
|
|
157
|
+
bextract --js 'document.title' # Custom JS expression
|
|
158
|
+
bextract --selector '.content' # Extract specific element
|
|
159
|
+
|
|
160
|
+
Options:
|
|
161
|
+
-e, --js <expr> JavaScript expression (default: document.body.innerText)
|
|
162
|
+
-s, --selector <sel> CSS selector to extract
|
|
163
|
+
--sessions <list> Comma-separated session IDs
|
|
164
|
+
-x, --exclude <list> Sessions to exclude
|
|
165
|
+
-l, --limit <n> Max chars per result (default: ${DEFAULT_LIMIT})
|
|
166
|
+
-t, --timeout <ms> Timeout per extraction (default: ${DEFAULT_TIMEOUT})
|
|
167
|
+
-j, --json Output as JSON
|
|
168
|
+
-q, --quiet Suppress progress output
|
|
169
|
+
-h, --help Show this help
|
|
170
|
+
`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build JS expression
|
|
176
|
+
if (selector) {
|
|
177
|
+
jsExpr = `document.querySelector('${selector}')?.innerText || ''`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Get targets
|
|
182
|
+
const targets = await getTargets();
|
|
183
|
+
if (targets.length === 0) {
|
|
184
|
+
console.error('Error: No browser tabs connected. Click extension icon on tabs first.');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Filter sessions
|
|
189
|
+
let filteredTargets = targets;
|
|
190
|
+
if (sessions) {
|
|
191
|
+
filteredTargets = targets.filter(t => sessions.includes(t.sessionId));
|
|
192
|
+
}
|
|
193
|
+
if (exclude.length > 0) {
|
|
194
|
+
filteredTargets = filteredTargets.filter(t => !exclude.includes(t.sessionId));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (filteredTargets.length === 0) {
|
|
198
|
+
console.error('Error: No matching sessions found');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!quiet) {
|
|
203
|
+
console.error(`[bextract] Extracting from ${filteredTargets.length} tabs in parallel...`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract in parallel
|
|
207
|
+
const results = await extractParallel(
|
|
208
|
+
filteredTargets.map(t => ({ sessionId: t.sessionId, url: t.targetInfo?.url })),
|
|
209
|
+
jsExpr,
|
|
210
|
+
{ limit, timeout, quiet }
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Output
|
|
214
|
+
if (outputJson) {
|
|
215
|
+
console.log(JSON.stringify(results, null, 2));
|
|
216
|
+
} else {
|
|
217
|
+
for (const [sessionId, data] of Object.entries(results)) {
|
|
218
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
219
|
+
console.log(`SESSION: ${sessionId}`);
|
|
220
|
+
console.log(`URL: ${data.url}`);
|
|
221
|
+
console.log('='.repeat(60));
|
|
222
|
+
if (data.error) {
|
|
223
|
+
console.log(`ERROR: ${data.error}`);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(data.content);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`Error: ${err.message}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main();
|
package/lib/bfetch.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fetch-via-browser.js
|
|
4
|
+
* Fetch URLs using your authenticated browser session via CDP relay
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ./fetch-via-browser.js <url> [--output file.json]
|
|
8
|
+
* ./fetch-via-browser.js https://www.reddit.com/r/programming.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const WebSocket = require('ws');
|
|
12
|
+
|
|
13
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
14
|
+
|
|
15
|
+
class BrowserFetcher {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.messageId = 0;
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
this.sessionId = null;
|
|
21
|
+
this.targetId = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async connect() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
27
|
+
|
|
28
|
+
this.ws.on('open', () => {
|
|
29
|
+
console.error('[fetcher] Connected to relay');
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.ws.on('error', (err) => {
|
|
34
|
+
reject(new Error(`Connection failed: ${err.message}`));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.ws.on('message', (data) => {
|
|
38
|
+
const msg = JSON.parse(data.toString());
|
|
39
|
+
|
|
40
|
+
// Response to our command
|
|
41
|
+
if (msg.id !== undefined) {
|
|
42
|
+
const pending = this.pending.get(msg.id);
|
|
43
|
+
if (pending) {
|
|
44
|
+
this.pending.delete(msg.id);
|
|
45
|
+
if (msg.error) {
|
|
46
|
+
pending.reject(new Error(msg.error.message));
|
|
47
|
+
} else {
|
|
48
|
+
pending.resolve(msg.result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Event
|
|
55
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
56
|
+
if (!this.sessionId) {
|
|
57
|
+
this.sessionId = msg.params.sessionId;
|
|
58
|
+
this.targetId = msg.params.targetInfo.targetId;
|
|
59
|
+
console.error(`[fetcher] Got target: ${msg.params.targetInfo.url}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async send(method, params = {}, sessionId = null) {
|
|
67
|
+
const id = ++this.messageId;
|
|
68
|
+
const msg = { id, method, params };
|
|
69
|
+
if (sessionId) msg.sessionId = sessionId;
|
|
70
|
+
|
|
71
|
+
this.ws.send(JSON.stringify(msg));
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
this.pending.delete(id);
|
|
76
|
+
reject(new Error(`Timeout: ${method}`));
|
|
77
|
+
}, 30000);
|
|
78
|
+
|
|
79
|
+
this.pending.set(id, {
|
|
80
|
+
resolve: (result) => { clearTimeout(timer); resolve(result); },
|
|
81
|
+
reject: (error) => { clearTimeout(timer); reject(error); }
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async init() {
|
|
87
|
+
// Initialize connection to browser
|
|
88
|
+
await this.send('Target.setAutoAttach', {
|
|
89
|
+
autoAttach: true,
|
|
90
|
+
waitForDebuggerOnStart: false,
|
|
91
|
+
flatten: true
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Wait for target
|
|
95
|
+
if (!this.sessionId) {
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this.sessionId) {
|
|
100
|
+
throw new Error('No browser tab connected. Click the extension icon on a tab first.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Enable Runtime for evaluation
|
|
104
|
+
await this.send('Runtime.enable', {}, this.sessionId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async fetch(url) {
|
|
108
|
+
console.error(`[fetcher] Fetching: ${url}`);
|
|
109
|
+
|
|
110
|
+
// Use browser's fetch API with its cookies
|
|
111
|
+
const script = `
|
|
112
|
+
(async () => {
|
|
113
|
+
const response = await fetch(${JSON.stringify(url)}, {
|
|
114
|
+
credentials: 'include',
|
|
115
|
+
headers: {
|
|
116
|
+
'Accept': 'application/json, text/plain, */*'
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const contentType = response.headers.get('content-type') || '';
|
|
121
|
+
const status = response.status;
|
|
122
|
+
|
|
123
|
+
let body;
|
|
124
|
+
if (contentType.includes('application/json')) {
|
|
125
|
+
body = await response.json();
|
|
126
|
+
} else {
|
|
127
|
+
body = await response.text();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { status, contentType, body };
|
|
131
|
+
})()
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
const result = await this.send('Runtime.evaluate', {
|
|
135
|
+
expression: script,
|
|
136
|
+
awaitPromise: true,
|
|
137
|
+
returnByValue: true
|
|
138
|
+
}, this.sessionId);
|
|
139
|
+
|
|
140
|
+
if (result.exceptionDetails) {
|
|
141
|
+
throw new Error(result.exceptionDetails.text || 'Evaluation failed');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result.result.value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async navigate(url) {
|
|
148
|
+
console.error(`[fetcher] Navigating to: ${url}`);
|
|
149
|
+
await this.send('Page.navigate', { url }, this.sessionId);
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getPageContent() {
|
|
154
|
+
const result = await this.send('Runtime.evaluate', {
|
|
155
|
+
expression: 'document.documentElement.outerHTML',
|
|
156
|
+
returnByValue: true
|
|
157
|
+
}, this.sessionId);
|
|
158
|
+
return result.result.value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async screenshot(path) {
|
|
162
|
+
const result = await this.send('Page.captureScreenshot', {
|
|
163
|
+
format: 'png'
|
|
164
|
+
}, this.sessionId);
|
|
165
|
+
|
|
166
|
+
const fs = require('fs');
|
|
167
|
+
fs.writeFileSync(path, Buffer.from(result.data, 'base64'));
|
|
168
|
+
console.error(`[fetcher] Screenshot saved: ${path}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
close() {
|
|
172
|
+
if (this.ws) {
|
|
173
|
+
this.ws.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main() {
|
|
179
|
+
const args = process.argv.slice(2);
|
|
180
|
+
|
|
181
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
182
|
+
console.log(`
|
|
183
|
+
Usage: fetch-via-browser [options] <url>
|
|
184
|
+
|
|
185
|
+
Fetch URLs using your authenticated browser session.
|
|
186
|
+
Requires the relay server running and extension connected.
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
--output, -o <file> Save output to file
|
|
190
|
+
--raw Output raw response (don't pretty-print JSON)
|
|
191
|
+
--navigate Navigate to URL instead of fetching via XHR
|
|
192
|
+
--screenshot <file> Take screenshot after fetching
|
|
193
|
+
--help, -h Show this help
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
fetch-via-browser https://www.reddit.com/r/programming.json
|
|
197
|
+
fetch-via-browser -o result.json https://www.reddit.com/user/spez/about.json
|
|
198
|
+
fetch-via-browser --navigate --screenshot shot.png https://old.reddit.com/r/webdev
|
|
199
|
+
`);
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let url = null;
|
|
204
|
+
let outputFile = null;
|
|
205
|
+
let raw = false;
|
|
206
|
+
let navigate = false;
|
|
207
|
+
let screenshot = null;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < args.length; i++) {
|
|
210
|
+
if (args[i] === '--output' || args[i] === '-o') {
|
|
211
|
+
outputFile = args[++i];
|
|
212
|
+
} else if (args[i] === '--raw') {
|
|
213
|
+
raw = true;
|
|
214
|
+
} else if (args[i] === '--navigate') {
|
|
215
|
+
navigate = true;
|
|
216
|
+
} else if (args[i] === '--screenshot') {
|
|
217
|
+
screenshot = args[++i];
|
|
218
|
+
} else if (!args[i].startsWith('-')) {
|
|
219
|
+
url = args[i];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!url) {
|
|
224
|
+
console.error('Error: URL required');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const fetcher = new BrowserFetcher();
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await fetcher.connect();
|
|
232
|
+
await fetcher.init();
|
|
233
|
+
|
|
234
|
+
let result;
|
|
235
|
+
|
|
236
|
+
if (navigate) {
|
|
237
|
+
await fetcher.navigate(url);
|
|
238
|
+
result = await fetcher.getPageContent();
|
|
239
|
+
} else {
|
|
240
|
+
result = await fetcher.fetch(url);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (screenshot) {
|
|
244
|
+
await fetcher.screenshot(screenshot);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Output
|
|
248
|
+
let output;
|
|
249
|
+
if (typeof result === 'object') {
|
|
250
|
+
if (result.body && typeof result.body === 'object') {
|
|
251
|
+
output = raw ? JSON.stringify(result.body) : JSON.stringify(result.body, null, 2);
|
|
252
|
+
} else {
|
|
253
|
+
output = raw ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
output = result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (outputFile) {
|
|
260
|
+
require('fs').writeFileSync(outputFile, output);
|
|
261
|
+
console.error(`[fetcher] Saved to: ${outputFile}`);
|
|
262
|
+
} else {
|
|
263
|
+
console.log(output);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`Error: ${err.message}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
} finally {
|
|
270
|
+
fetcher.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main();
|