@firekid/scraper 1.0.1
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/LICENSE +21 -0
- package/README.md +767 -0
- package/dist/bin/firekid-scraper.cjs +2264 -0
- package/dist/bin/firekid-scraper.d.mts +1 -0
- package/dist/bin/firekid-scraper.d.ts +1 -0
- package/dist/bin/firekid-scraper.js +2251 -0
- package/dist/index.cjs +2145 -0
- package/dist/index.d.mts +366 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +2107 -0
- package/package.json +90 -0
|
@@ -0,0 +1,2264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var init_cjs_shims = __esm({
|
|
35
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/ghost/seed.ts
|
|
41
|
+
function random(array) {
|
|
42
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
43
|
+
}
|
|
44
|
+
function randomRange(min, max) {
|
|
45
|
+
return Math.random() * (max - min) + min;
|
|
46
|
+
}
|
|
47
|
+
function getNewSeed() {
|
|
48
|
+
const resolution = random(resolutions);
|
|
49
|
+
return {
|
|
50
|
+
id: import_crypto.default.randomUUID(),
|
|
51
|
+
chromeVersion: random(chromeVersions),
|
|
52
|
+
screenWidth: resolution.width,
|
|
53
|
+
screenHeight: resolution.height,
|
|
54
|
+
language: random(languages),
|
|
55
|
+
timezone: random(timezones),
|
|
56
|
+
canvasNoise: randomRange(1e-4, 1e-3),
|
|
57
|
+
webglVendor: random(webglVendors),
|
|
58
|
+
webglRenderer: random(webglRenderers),
|
|
59
|
+
audioNoise: randomRange(1e-5, 1e-4),
|
|
60
|
+
fonts: random(fontSets)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function getSeedForSite(siteHost) {
|
|
64
|
+
if (seedCache.has(siteHost)) {
|
|
65
|
+
return seedCache.get(siteHost);
|
|
66
|
+
}
|
|
67
|
+
const seed = getNewSeed();
|
|
68
|
+
seedCache.set(siteHost, seed);
|
|
69
|
+
return seed;
|
|
70
|
+
}
|
|
71
|
+
var import_crypto, seedCache, chromeVersions, resolutions, languages, timezones, webglVendors, webglRenderers, fontSets;
|
|
72
|
+
var init_seed = __esm({
|
|
73
|
+
"src/ghost/seed.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
init_cjs_shims();
|
|
76
|
+
import_crypto = __toESM(require("crypto"));
|
|
77
|
+
seedCache = /* @__PURE__ */ new Map();
|
|
78
|
+
chromeVersions = [
|
|
79
|
+
"131.0.6778.85",
|
|
80
|
+
"131.0.6778.86",
|
|
81
|
+
"130.0.6723.116",
|
|
82
|
+
"129.0.6668.100"
|
|
83
|
+
];
|
|
84
|
+
resolutions = [
|
|
85
|
+
{ width: 1920, height: 1080 },
|
|
86
|
+
{ width: 1366, height: 768 },
|
|
87
|
+
{ width: 1536, height: 864 },
|
|
88
|
+
{ width: 1440, height: 900 },
|
|
89
|
+
{ width: 2560, height: 1440 }
|
|
90
|
+
];
|
|
91
|
+
languages = ["en-US", "en-GB", "en", "es-ES", "fr-FR", "de-DE"];
|
|
92
|
+
timezones = ["America/New_York", "America/Los_Angeles", "Europe/London", "Europe/Paris"];
|
|
93
|
+
webglVendors = [
|
|
94
|
+
"Google Inc. (NVIDIA)",
|
|
95
|
+
"Google Inc. (Intel)",
|
|
96
|
+
"Google Inc. (AMD)",
|
|
97
|
+
"Google Inc. (Apple)"
|
|
98
|
+
];
|
|
99
|
+
webglRenderers = [
|
|
100
|
+
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
101
|
+
"ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
102
|
+
"ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0, D3D11)"
|
|
103
|
+
];
|
|
104
|
+
fontSets = [
|
|
105
|
+
["Arial", "Calibri", "Cambria", "Consolas", "Georgia", "Times New Roman", "Verdana"],
|
|
106
|
+
["Arial", "Helvetica", "Georgia", "Courier New", "Times", "Comic Sans MS"],
|
|
107
|
+
["Arial", "Tahoma", "Trebuchet MS", "Verdana", "Georgia", "Palatino Linotype"]
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// src/ghost/canvas.ts
|
|
113
|
+
async function applyCanvasSpoof(context, seed) {
|
|
114
|
+
await context.addInitScript((noise) => {
|
|
115
|
+
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
116
|
+
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
117
|
+
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
118
|
+
CanvasRenderingContext2D.prototype.getImageData = function(...args) {
|
|
119
|
+
const imageData = originalGetImageData.apply(this, args);
|
|
120
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
121
|
+
imageData.data[i] += Math.floor(noise * 255 * (Math.random() - 0.5));
|
|
122
|
+
imageData.data[i + 1] += Math.floor(noise * 255 * (Math.random() - 0.5));
|
|
123
|
+
imageData.data[i + 2] += Math.floor(noise * 255 * (Math.random() - 0.5));
|
|
124
|
+
}
|
|
125
|
+
return imageData;
|
|
126
|
+
};
|
|
127
|
+
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
128
|
+
const context2 = this.getContext("2d");
|
|
129
|
+
if (context2) {
|
|
130
|
+
const imageData = context2.getImageData(0, 0, this.width, this.height);
|
|
131
|
+
context2.putImageData(imageData, 0, 0);
|
|
132
|
+
}
|
|
133
|
+
return originalToDataURL.apply(this, args);
|
|
134
|
+
};
|
|
135
|
+
HTMLCanvasElement.prototype.toBlob = function(...args) {
|
|
136
|
+
const context2 = this.getContext("2d");
|
|
137
|
+
if (context2) {
|
|
138
|
+
const imageData = context2.getImageData(0, 0, this.width, this.height);
|
|
139
|
+
context2.putImageData(imageData, 0, 0);
|
|
140
|
+
}
|
|
141
|
+
return originalToBlob.apply(this, args);
|
|
142
|
+
};
|
|
143
|
+
}, seed.canvasNoise);
|
|
144
|
+
}
|
|
145
|
+
var init_canvas = __esm({
|
|
146
|
+
"src/ghost/canvas.ts"() {
|
|
147
|
+
"use strict";
|
|
148
|
+
init_cjs_shims();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// src/ghost/webgl.ts
|
|
153
|
+
async function applyWebGLSpoof(context, seed) {
|
|
154
|
+
await context.addInitScript((params) => {
|
|
155
|
+
const { vendor, renderer } = params;
|
|
156
|
+
const getParameterProxyHandler = {
|
|
157
|
+
apply(target, thisArg, args) {
|
|
158
|
+
const param = args[0];
|
|
159
|
+
if (param === 37445) {
|
|
160
|
+
return vendor;
|
|
161
|
+
}
|
|
162
|
+
if (param === 37446) {
|
|
163
|
+
return renderer;
|
|
164
|
+
}
|
|
165
|
+
return Reflect.apply(target, thisArg, args);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const getExtensionProxyHandler = {
|
|
169
|
+
apply(target, thisArg, args) {
|
|
170
|
+
const result = Reflect.apply(target, thisArg, args);
|
|
171
|
+
if (!result) {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
if (args[0] === "WEBGL_debug_renderer_info") {
|
|
175
|
+
const getParameterProxy = new Proxy(result.getParameter, getParameterProxyHandler);
|
|
176
|
+
result.getParameter = getParameterProxy;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(
|
|
182
|
+
WebGLRenderingContext.prototype.getParameter,
|
|
183
|
+
getParameterProxyHandler
|
|
184
|
+
);
|
|
185
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(
|
|
186
|
+
WebGL2RenderingContext.prototype.getParameter,
|
|
187
|
+
getParameterProxyHandler
|
|
188
|
+
);
|
|
189
|
+
WebGLRenderingContext.prototype.getExtension = new Proxy(
|
|
190
|
+
WebGLRenderingContext.prototype.getExtension,
|
|
191
|
+
getExtensionProxyHandler
|
|
192
|
+
);
|
|
193
|
+
WebGL2RenderingContext.prototype.getExtension = new Proxy(
|
|
194
|
+
WebGL2RenderingContext.prototype.getExtension,
|
|
195
|
+
getExtensionProxyHandler
|
|
196
|
+
);
|
|
197
|
+
}, { vendor: seed.webglVendor, renderer: seed.webglRenderer });
|
|
198
|
+
}
|
|
199
|
+
var init_webgl = __esm({
|
|
200
|
+
"src/ghost/webgl.ts"() {
|
|
201
|
+
"use strict";
|
|
202
|
+
init_cjs_shims();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// src/ghost/audio.ts
|
|
207
|
+
async function applyAudioSpoof(context, seed) {
|
|
208
|
+
await context.addInitScript((noise) => {
|
|
209
|
+
const context2 = window.AudioContext || window.webkitAudioContext;
|
|
210
|
+
if (context2) {
|
|
211
|
+
const originalCreateDynamicsCompressor = context2.prototype.createDynamicsCompressor;
|
|
212
|
+
const originalCreateOscillator = context2.prototype.createOscillator;
|
|
213
|
+
context2.prototype.createDynamicsCompressor = function() {
|
|
214
|
+
const compressor = originalCreateDynamicsCompressor.apply(this, arguments);
|
|
215
|
+
if (compressor.reduction) {
|
|
216
|
+
Object.defineProperty(compressor.reduction, "value", {
|
|
217
|
+
get() {
|
|
218
|
+
return this._value + noise * (Math.random() - 0.5);
|
|
219
|
+
},
|
|
220
|
+
set(v) {
|
|
221
|
+
this._value = v;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return compressor;
|
|
226
|
+
};
|
|
227
|
+
context2.prototype.createOscillator = function() {
|
|
228
|
+
const oscillator = originalCreateOscillator.apply(this, arguments);
|
|
229
|
+
const originalStart = oscillator.start;
|
|
230
|
+
oscillator.start = function() {
|
|
231
|
+
if (oscillator.frequency) {
|
|
232
|
+
oscillator.frequency.value += noise * (Math.random() - 0.5);
|
|
233
|
+
}
|
|
234
|
+
return originalStart.apply(this, arguments);
|
|
235
|
+
};
|
|
236
|
+
return oscillator;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}, seed.audioNoise);
|
|
240
|
+
}
|
|
241
|
+
var init_audio = __esm({
|
|
242
|
+
"src/ghost/audio.ts"() {
|
|
243
|
+
"use strict";
|
|
244
|
+
init_cjs_shims();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// src/ghost/fonts.ts
|
|
249
|
+
async function applyFontSpoof(context, seed) {
|
|
250
|
+
await context.addInitScript((fonts) => {
|
|
251
|
+
const originalGetComputedStyle = window.getComputedStyle;
|
|
252
|
+
window.getComputedStyle = function(element, pseudoElt) {
|
|
253
|
+
const styles = originalGetComputedStyle.call(this, element, pseudoElt);
|
|
254
|
+
const originalGetPropertyValue = styles.getPropertyValue;
|
|
255
|
+
styles.getPropertyValue = function(property) {
|
|
256
|
+
if (property === "font-family") {
|
|
257
|
+
const value = originalGetPropertyValue.call(this, property);
|
|
258
|
+
const families = value.split(",").map((f) => f.trim());
|
|
259
|
+
const filtered = families.filter((family) => {
|
|
260
|
+
const cleanFamily = family.replace(/['"]/g, "");
|
|
261
|
+
return fonts.some((f) => cleanFamily.includes(f));
|
|
262
|
+
});
|
|
263
|
+
return filtered.length > 0 ? filtered.join(", ") : value;
|
|
264
|
+
}
|
|
265
|
+
return originalGetPropertyValue.call(this, property);
|
|
266
|
+
};
|
|
267
|
+
return styles;
|
|
268
|
+
};
|
|
269
|
+
}, seed.fonts);
|
|
270
|
+
}
|
|
271
|
+
var init_fonts = __esm({
|
|
272
|
+
"src/ghost/fonts.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
init_cjs_shims();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/ghost/navigator.ts
|
|
279
|
+
async function applyNavigatorSpoof(context, seed) {
|
|
280
|
+
await context.addInitScript((seedData) => {
|
|
281
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
282
|
+
get: () => void 0
|
|
283
|
+
});
|
|
284
|
+
Object.defineProperty(navigator, "plugins", {
|
|
285
|
+
get: () => [1, 2, 3, 4, 5]
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(navigator, "languages", {
|
|
288
|
+
get: () => [seedData.language, "en"]
|
|
289
|
+
});
|
|
290
|
+
Object.defineProperty(navigator, "platform", {
|
|
291
|
+
get: () => "Win32"
|
|
292
|
+
});
|
|
293
|
+
Object.defineProperty(navigator, "hardwareConcurrency", {
|
|
294
|
+
get: () => 8
|
|
295
|
+
});
|
|
296
|
+
Object.defineProperty(navigator, "deviceMemory", {
|
|
297
|
+
get: () => 8
|
|
298
|
+
});
|
|
299
|
+
const originalQuery = window.navigator.permissions.query;
|
|
300
|
+
window.navigator.permissions.query = (parameters) => parameters.name === "notifications" ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters);
|
|
301
|
+
if (window.chrome) {
|
|
302
|
+
delete window.chrome.runtime;
|
|
303
|
+
}
|
|
304
|
+
Object.defineProperty(screen, "width", {
|
|
305
|
+
get: () => seedData.screenWidth
|
|
306
|
+
});
|
|
307
|
+
Object.defineProperty(screen, "height", {
|
|
308
|
+
get: () => seedData.screenHeight
|
|
309
|
+
});
|
|
310
|
+
Object.defineProperty(screen, "availWidth", {
|
|
311
|
+
get: () => seedData.screenWidth
|
|
312
|
+
});
|
|
313
|
+
Object.defineProperty(screen, "availHeight", {
|
|
314
|
+
get: () => seedData.screenHeight - 40
|
|
315
|
+
});
|
|
316
|
+
}, seed);
|
|
317
|
+
}
|
|
318
|
+
var init_navigator = __esm({
|
|
319
|
+
"src/ghost/navigator.ts"() {
|
|
320
|
+
"use strict";
|
|
321
|
+
init_cjs_shims();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// src/ghost/consistency.ts
|
|
326
|
+
function validateConsistency(seed) {
|
|
327
|
+
if (!seed.id || !seed.chromeVersion) {
|
|
328
|
+
throw new Error("Invalid seed: missing required fields");
|
|
329
|
+
}
|
|
330
|
+
if (seed.screenWidth < 800 || seed.screenHeight < 600) {
|
|
331
|
+
throw new Error("Invalid seed: screen resolution too small");
|
|
332
|
+
}
|
|
333
|
+
if (seed.canvasNoise < 0 || seed.canvasNoise > 1) {
|
|
334
|
+
throw new Error("Invalid seed: canvas noise out of range");
|
|
335
|
+
}
|
|
336
|
+
if (seed.audioNoise < 0 || seed.audioNoise > 1) {
|
|
337
|
+
throw new Error("Invalid seed: audio noise out of range");
|
|
338
|
+
}
|
|
339
|
+
if (!seed.fonts || seed.fonts.length === 0) {
|
|
340
|
+
throw new Error("Invalid seed: no fonts specified");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
var init_consistency = __esm({
|
|
344
|
+
"src/ghost/consistency.ts"() {
|
|
345
|
+
"use strict";
|
|
346
|
+
init_cjs_shims();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/ghost/behavior.ts
|
|
351
|
+
var HumanBehavior;
|
|
352
|
+
var init_behavior = __esm({
|
|
353
|
+
"src/ghost/behavior.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
init_cjs_shims();
|
|
356
|
+
HumanBehavior = class {
|
|
357
|
+
seed;
|
|
358
|
+
profile;
|
|
359
|
+
constructor(seed) {
|
|
360
|
+
this.seed = seed;
|
|
361
|
+
this.profile = this.generateProfile();
|
|
362
|
+
}
|
|
363
|
+
generateProfile() {
|
|
364
|
+
return {
|
|
365
|
+
typingSpeed: { min: 50, max: 150 },
|
|
366
|
+
mouseMovements: [],
|
|
367
|
+
scrollPatterns: [],
|
|
368
|
+
pauseDistribution: [],
|
|
369
|
+
clickTiming: []
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async randomDelay(min = 500, max = 2e3) {
|
|
373
|
+
const delay = Math.random() * (max - min) + min;
|
|
374
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
375
|
+
}
|
|
376
|
+
async humanClick(page, selector) {
|
|
377
|
+
const element = await page.locator(selector);
|
|
378
|
+
const box = await element.boundingBox();
|
|
379
|
+
if (box) {
|
|
380
|
+
const x = box.x + Math.random() * box.width;
|
|
381
|
+
const y = box.y + Math.random() * box.height;
|
|
382
|
+
await page.mouse.move(x, y, { steps: 10 });
|
|
383
|
+
await this.randomDelay(100, 300);
|
|
384
|
+
await page.mouse.click(x, y);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async humanType(page, selector, text2) {
|
|
388
|
+
await page.focus(selector);
|
|
389
|
+
for (const char of text2) {
|
|
390
|
+
await page.keyboard.type(char);
|
|
391
|
+
await this.randomDelay(
|
|
392
|
+
this.profile.typingSpeed.min,
|
|
393
|
+
this.profile.typingSpeed.max
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async randomScroll(page) {
|
|
398
|
+
const scrolls = Math.floor(Math.random() * 3) + 1;
|
|
399
|
+
for (let i = 0; i < scrolls; i++) {
|
|
400
|
+
const scrollY = Math.random() * 500;
|
|
401
|
+
await page.evaluate((y) => window.scrollBy(0, y), scrollY);
|
|
402
|
+
await this.randomDelay(500, 1e3);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async randomMouseMovement(page) {
|
|
406
|
+
const x = Math.random() * 1920;
|
|
407
|
+
const y = Math.random() * 1080;
|
|
408
|
+
await page.mouse.move(x, y, { steps: 20 });
|
|
409
|
+
await this.randomDelay(200, 500);
|
|
410
|
+
}
|
|
411
|
+
getProfile() {
|
|
412
|
+
return this.profile;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// src/config.ts
|
|
419
|
+
var import_dotenv, config;
|
|
420
|
+
var init_config = __esm({
|
|
421
|
+
"src/config.ts"() {
|
|
422
|
+
"use strict";
|
|
423
|
+
init_cjs_shims();
|
|
424
|
+
import_dotenv = __toESM(require("dotenv"));
|
|
425
|
+
import_dotenv.default.config();
|
|
426
|
+
config = {
|
|
427
|
+
browser: {
|
|
428
|
+
headless: process.env.HEADLESS === "true",
|
|
429
|
+
timeout: parseInt(process.env.BROWSER_TIMEOUT || "30000", 10),
|
|
430
|
+
maxWorkers: parseInt(process.env.MAX_QUEUE_WORKERS || "5", 10)
|
|
431
|
+
},
|
|
432
|
+
cloudflare: {
|
|
433
|
+
bypass: process.env.CF_BYPASS || "auto",
|
|
434
|
+
turnstileSolver: process.env.TURNSTILE_SOLVER || "manual"
|
|
435
|
+
},
|
|
436
|
+
captcha: {
|
|
437
|
+
apiKey: process.env.CAPTCHA_API_KEY || ""
|
|
438
|
+
},
|
|
439
|
+
server: {
|
|
440
|
+
enabled: process.env.API_ENABLED === "true",
|
|
441
|
+
port: parseInt(process.env.API_PORT || "3000", 10),
|
|
442
|
+
apiKey: process.env.API_KEY || ""
|
|
443
|
+
},
|
|
444
|
+
proxy: {
|
|
445
|
+
enabled: process.env.PROXY_ENABLED === "true",
|
|
446
|
+
url: process.env.PROXY_URL || ""
|
|
447
|
+
},
|
|
448
|
+
storage: {
|
|
449
|
+
dataDir: process.env.DATA_DIR || "./data",
|
|
450
|
+
patternsDb: process.env.PATTERNS_DB || "./data/patterns.db",
|
|
451
|
+
sessionsDb: process.env.SESSIONS_DB || "./data/sessions.db"
|
|
452
|
+
},
|
|
453
|
+
logging: {
|
|
454
|
+
level: process.env.LOG_LEVEL || "info"
|
|
455
|
+
},
|
|
456
|
+
recording: {
|
|
457
|
+
autoHideAfterSolve: process.env.AUTO_HIDE_AFTER_SOLVE !== "false",
|
|
458
|
+
recordScreenshots: process.env.RECORD_SCREENSHOTS === "true"
|
|
459
|
+
},
|
|
460
|
+
rateLimit: {
|
|
461
|
+
enabled: process.env.RATE_LIMIT_ENABLED !== "false",
|
|
462
|
+
max: parseInt(process.env.RATE_LIMIT_MAX || "100", 10),
|
|
463
|
+
window: parseInt(process.env.RATE_LIMIT_WINDOW || "3600000", 10)
|
|
464
|
+
},
|
|
465
|
+
advanced: {
|
|
466
|
+
enableTelemetry: process.env.ENABLE_TELEMETRY === "true",
|
|
467
|
+
enableAnalytics: process.env.ENABLE_ANALYTICS === "true"
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// src/logger/logger.ts
|
|
474
|
+
function step(url, action, meta) {
|
|
475
|
+
logger.info(`[${url}] ${action}`, meta);
|
|
476
|
+
}
|
|
477
|
+
function highlight(url, data) {
|
|
478
|
+
logger.info(`[${url}] EXTRACTED:`, data);
|
|
479
|
+
}
|
|
480
|
+
var import_winston, logger;
|
|
481
|
+
var init_logger = __esm({
|
|
482
|
+
"src/logger/logger.ts"() {
|
|
483
|
+
"use strict";
|
|
484
|
+
init_cjs_shims();
|
|
485
|
+
import_winston = __toESM(require("winston"));
|
|
486
|
+
init_config();
|
|
487
|
+
logger = import_winston.default.createLogger({
|
|
488
|
+
level: config.logging.level,
|
|
489
|
+
format: import_winston.default.format.combine(
|
|
490
|
+
import_winston.default.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
491
|
+
import_winston.default.format.errors({ stack: true }),
|
|
492
|
+
import_winston.default.format.splat(),
|
|
493
|
+
import_winston.default.format.printf(({ level, message, timestamp, ...meta }) => {
|
|
494
|
+
let msg = `${timestamp} [${level.toUpperCase()}] ${message}`;
|
|
495
|
+
if (Object.keys(meta).length > 0) {
|
|
496
|
+
msg += ` ${JSON.stringify(meta)}`;
|
|
497
|
+
}
|
|
498
|
+
return msg;
|
|
499
|
+
})
|
|
500
|
+
),
|
|
501
|
+
transports: [
|
|
502
|
+
new import_winston.default.transports.Console({
|
|
503
|
+
format: import_winston.default.format.combine(
|
|
504
|
+
import_winston.default.format.colorize(),
|
|
505
|
+
import_winston.default.format.printf(({ level, message, timestamp }) => {
|
|
506
|
+
return `${timestamp} ${level}: ${message}`;
|
|
507
|
+
})
|
|
508
|
+
)
|
|
509
|
+
}),
|
|
510
|
+
new import_winston.default.transports.File({
|
|
511
|
+
filename: "logs/error.log",
|
|
512
|
+
level: "error"
|
|
513
|
+
}),
|
|
514
|
+
new import_winston.default.transports.File({
|
|
515
|
+
filename: "logs/combined.log"
|
|
516
|
+
})
|
|
517
|
+
]
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// src/ghost/index.ts
|
|
523
|
+
async function applyGhost(context, options2 = {}) {
|
|
524
|
+
const seed = options2.fresh || !options2.siteHost ? getNewSeed() : getSeedForSite(options2.siteHost);
|
|
525
|
+
validateConsistency(seed);
|
|
526
|
+
logger.info(`[ghost] Applying identity seed: ${seed.id.slice(0, 8)}... | Chrome ${seed.chromeVersion} | ${seed.screenWidth}x${seed.screenHeight}`);
|
|
527
|
+
await applyCanvasSpoof(context, seed);
|
|
528
|
+
await applyWebGLSpoof(context, seed);
|
|
529
|
+
await applyAudioSpoof(context, seed);
|
|
530
|
+
await applyFontSpoof(context, seed);
|
|
531
|
+
await applyNavigatorSpoof(context, seed);
|
|
532
|
+
await context.setExtraHTTPHeaders({
|
|
533
|
+
"Accept-Language": `${seed.language},en;q=0.9`,
|
|
534
|
+
"sec-ch-ua": `"Chromium";v="${seed.chromeVersion.split(".")[0]}", "Google Chrome";v="${seed.chromeVersion.split(".")[0]}", "Not-A.Brand";v="99"`,
|
|
535
|
+
"sec-ch-ua-mobile": "?0",
|
|
536
|
+
"sec-ch-ua-platform": '"Windows"'
|
|
537
|
+
});
|
|
538
|
+
logger.info("[ghost] All fingerprint spoofs applied");
|
|
539
|
+
return new HumanBehavior(seed);
|
|
540
|
+
}
|
|
541
|
+
var init_ghost = __esm({
|
|
542
|
+
"src/ghost/index.ts"() {
|
|
543
|
+
"use strict";
|
|
544
|
+
init_cjs_shims();
|
|
545
|
+
init_seed();
|
|
546
|
+
init_canvas();
|
|
547
|
+
init_webgl();
|
|
548
|
+
init_audio();
|
|
549
|
+
init_fonts();
|
|
550
|
+
init_navigator();
|
|
551
|
+
init_consistency();
|
|
552
|
+
init_behavior();
|
|
553
|
+
init_logger();
|
|
554
|
+
init_seed();
|
|
555
|
+
init_behavior();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// src/cloudflare/cloudflare.ts
|
|
560
|
+
var CloudflareManager;
|
|
561
|
+
var init_cloudflare = __esm({
|
|
562
|
+
"src/cloudflare/cloudflare.ts"() {
|
|
563
|
+
"use strict";
|
|
564
|
+
init_cjs_shims();
|
|
565
|
+
init_logger();
|
|
566
|
+
CloudflareManager = class {
|
|
567
|
+
async detect(page) {
|
|
568
|
+
const url = page.url();
|
|
569
|
+
try {
|
|
570
|
+
const title = await page.title();
|
|
571
|
+
const content = await page.content();
|
|
572
|
+
const cfIndicators = [
|
|
573
|
+
title.includes("Just a moment"),
|
|
574
|
+
title.includes("Attention Required"),
|
|
575
|
+
content.includes("cf-browser-verification"),
|
|
576
|
+
content.includes("cloudflare"),
|
|
577
|
+
content.includes("cf_chl_opt"),
|
|
578
|
+
content.includes("__cf_bm"),
|
|
579
|
+
content.includes("Ray ID")
|
|
580
|
+
];
|
|
581
|
+
const detected = cfIndicators.some(Boolean);
|
|
582
|
+
if (detected) {
|
|
583
|
+
logger.warn(`[cloudflare] CF protection detected on ${url}`);
|
|
584
|
+
}
|
|
585
|
+
return detected;
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async waitForClearance(page, timeoutMs = 3e4) {
|
|
591
|
+
logger.info("[cloudflare] Waiting for CF challenge to resolve...");
|
|
592
|
+
const start = Date.now();
|
|
593
|
+
while (Date.now() - start < timeoutMs) {
|
|
594
|
+
const title = await page.title().catch(() => "");
|
|
595
|
+
const isCFPage = title.includes("Just a moment") || title.includes("Attention Required");
|
|
596
|
+
if (!isCFPage) {
|
|
597
|
+
logger.info("[cloudflare] CF challenge cleared");
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
await page.waitForTimeout(1e3);
|
|
601
|
+
}
|
|
602
|
+
logger.error("[cloudflare] CF challenge timeout - could not clear in time");
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
async extractTokens(context) {
|
|
606
|
+
const cookies = await context.cookies();
|
|
607
|
+
const tokens = {};
|
|
608
|
+
for (const cookie of cookies) {
|
|
609
|
+
if (cookie.name === "cf_clearance") tokens.cfClearance = cookie.value;
|
|
610
|
+
if (cookie.name === "__cf_bm") tokens.cfBm = cookie.value;
|
|
611
|
+
}
|
|
612
|
+
if (tokens.cfClearance) {
|
|
613
|
+
logger.info(`[cloudflare] Captured cf_clearance: ${tokens.cfClearance.slice(0, 20)}...`);
|
|
614
|
+
}
|
|
615
|
+
return tokens;
|
|
616
|
+
}
|
|
617
|
+
async detectWAF(page) {
|
|
618
|
+
const content = await page.content().catch(() => "");
|
|
619
|
+
if (content.includes("cloudflare") || content.includes("cf-ray")) return "Cloudflare";
|
|
620
|
+
if (content.includes("akamai") || content.includes("ak_bmsc")) return "Akamai";
|
|
621
|
+
if (content.includes("sucuri")) return "Sucuri";
|
|
622
|
+
if (content.includes("incapsula")) return "Imperva/Incapsula";
|
|
623
|
+
if (content.includes("distil")) return "Distil Networks";
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
async handleCloudflare(page, url) {
|
|
627
|
+
const isProtected = await this.detect(page);
|
|
628
|
+
if (!isProtected) return true;
|
|
629
|
+
logger.info("[cloudflare] Cloudflare detected");
|
|
630
|
+
const cleared = await this.waitForClearance(page);
|
|
631
|
+
if (cleared) {
|
|
632
|
+
logger.info("[cloudflare] JS challenge auto-cleared");
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
const hasTurnstile = await this.detectTurnstile(page);
|
|
636
|
+
if (hasTurnstile) {
|
|
637
|
+
logger.info("[cloudflare] Turnstile CAPTCHA detected - opening browser for manual solve");
|
|
638
|
+
return await this.handleTurnstile(page, url);
|
|
639
|
+
}
|
|
640
|
+
logger.warn("[cloudflare] Unknown Cloudflare challenge");
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
async detectTurnstile(page) {
|
|
644
|
+
const turnstileFrame = await page.locator('iframe[src*="challenges.cloudflare.com"]').count();
|
|
645
|
+
const turnstileDiv = await page.locator('[id*="turnstile"]').count();
|
|
646
|
+
return turnstileFrame > 0 || turnstileDiv > 0;
|
|
647
|
+
}
|
|
648
|
+
async handleTurnstile(page, url) {
|
|
649
|
+
logger.info("[cloudflare] Waiting for manual Turnstile solve...");
|
|
650
|
+
console.log("\n===========================================");
|
|
651
|
+
console.log(" PLEASE SOLVE THE CAPTCHA");
|
|
652
|
+
console.log(" Waiting for you to complete it...");
|
|
653
|
+
console.log("===========================================\n");
|
|
654
|
+
await this.waitForTurnstileSolved(page);
|
|
655
|
+
logger.info("[cloudflare] CAPTCHA solved! Continuing...");
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
async isTurnstileSolved(page) {
|
|
659
|
+
try {
|
|
660
|
+
const turnstileExists = await page.locator('iframe[src*="challenges.cloudflare.com"]').count();
|
|
661
|
+
if (turnstileExists === 0) return true;
|
|
662
|
+
const hasToken = await page.evaluate(() => {
|
|
663
|
+
const input = document.querySelector('input[name="cf-turnstile-response"]');
|
|
664
|
+
return input && input.value !== "";
|
|
665
|
+
});
|
|
666
|
+
if (hasToken) return true;
|
|
667
|
+
const contentVisible = await page.evaluate(() => {
|
|
668
|
+
const body = document.body;
|
|
669
|
+
return body && !body.classList.contains("no-scroll");
|
|
670
|
+
});
|
|
671
|
+
return contentVisible;
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async waitForTurnstileSolved(page) {
|
|
677
|
+
while (true) {
|
|
678
|
+
const solved = await this.isTurnstileSolved(page);
|
|
679
|
+
if (solved) {
|
|
680
|
+
await page.waitForTimeout(2e3);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
await page.waitForTimeout(1e3);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// src/engine/cmd-parser.ts
|
|
691
|
+
var import_fs, import_path, CommandParser;
|
|
692
|
+
var init_cmd_parser = __esm({
|
|
693
|
+
"src/engine/cmd-parser.ts"() {
|
|
694
|
+
"use strict";
|
|
695
|
+
init_cjs_shims();
|
|
696
|
+
import_fs = __toESM(require("fs"));
|
|
697
|
+
import_path = __toESM(require("path"));
|
|
698
|
+
init_logger();
|
|
699
|
+
CommandParser = class {
|
|
700
|
+
variables = {};
|
|
701
|
+
setVariable(key, value) {
|
|
702
|
+
this.variables[key] = value;
|
|
703
|
+
}
|
|
704
|
+
resolve(text2) {
|
|
705
|
+
return text2.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
706
|
+
return this.variables[key] ?? `{{${key}}}`;
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
parseLine(line, lineNum) {
|
|
710
|
+
if (line.trim().startsWith("//")) return null;
|
|
711
|
+
const parts = line.trim().split(/\s+/);
|
|
712
|
+
const action = parts[0].toUpperCase();
|
|
713
|
+
const args = parts.slice(1).map((a) => this.resolve(a));
|
|
714
|
+
const validActions = [
|
|
715
|
+
"GOTO",
|
|
716
|
+
"BACK",
|
|
717
|
+
"FORWARD",
|
|
718
|
+
"REFRESH",
|
|
719
|
+
"CLICK",
|
|
720
|
+
"TYPE",
|
|
721
|
+
"PRESS",
|
|
722
|
+
"SELECT",
|
|
723
|
+
"CHECK",
|
|
724
|
+
"UPLOAD",
|
|
725
|
+
"WAIT",
|
|
726
|
+
"WAITLOAD",
|
|
727
|
+
"SCROLL",
|
|
728
|
+
"SCROLLDOWN",
|
|
729
|
+
"SCAN",
|
|
730
|
+
"EXTRACT",
|
|
731
|
+
"SCREENSHOT",
|
|
732
|
+
"PAGINATE",
|
|
733
|
+
"INFINITESCROLL",
|
|
734
|
+
"FETCH",
|
|
735
|
+
"DOWNLOAD",
|
|
736
|
+
"REFERER",
|
|
737
|
+
"BYPASS_CLOUDFLARE",
|
|
738
|
+
"REPEAT",
|
|
739
|
+
"IF",
|
|
740
|
+
"LOOP"
|
|
741
|
+
];
|
|
742
|
+
if (!validActions.includes(action)) {
|
|
743
|
+
logger.warn(`Unknown action "${action}" at line ${lineNum} - skipping`);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
return { action, args, line: lineNum };
|
|
747
|
+
}
|
|
748
|
+
parse(content, filePath = "unknown") {
|
|
749
|
+
const lines = content.split("\n");
|
|
750
|
+
const steps = [];
|
|
751
|
+
let i = 0;
|
|
752
|
+
while (i < lines.length) {
|
|
753
|
+
const raw = lines[i];
|
|
754
|
+
const lineNum = i + 1;
|
|
755
|
+
if (raw.trim().startsWith("//")) {
|
|
756
|
+
i++;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const trimmed = raw.trimEnd();
|
|
760
|
+
if (!trimmed.trim()) {
|
|
761
|
+
i++;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const indent = raw.match(/^(\s*)/)?.[1].length ?? 0;
|
|
765
|
+
if (indent === 0) {
|
|
766
|
+
const step2 = this.parseLine(trimmed, lineNum);
|
|
767
|
+
if (step2) {
|
|
768
|
+
if (step2.action === "REPEAT" || step2.action === "IF" || step2.action === "LOOP") {
|
|
769
|
+
step2.children = [];
|
|
770
|
+
i++;
|
|
771
|
+
while (i < lines.length) {
|
|
772
|
+
const childRaw = lines[i];
|
|
773
|
+
const childIndent = childRaw.match(/^(\s*)/)?.[1].length ?? 0;
|
|
774
|
+
if (childIndent === 0) break;
|
|
775
|
+
const childStep = this.parseLine(childRaw.trim(), i + 1);
|
|
776
|
+
if (childStep) step2.children.push(childStep);
|
|
777
|
+
i++;
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
i++;
|
|
781
|
+
}
|
|
782
|
+
steps.push(step2);
|
|
783
|
+
} else {
|
|
784
|
+
i++;
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
i++;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const site = import_path.default.basename(filePath, ".cmd");
|
|
791
|
+
logger.info(`Parsed ${steps.length} steps from ${filePath}`);
|
|
792
|
+
return { site, steps, raw: content };
|
|
793
|
+
}
|
|
794
|
+
load(filePath) {
|
|
795
|
+
if (!import_fs.default.existsSync(filePath)) {
|
|
796
|
+
throw new Error(`File not found: ${filePath}`);
|
|
797
|
+
}
|
|
798
|
+
const content = import_fs.default.readFileSync(filePath, "utf8");
|
|
799
|
+
return this.parse(content, filePath);
|
|
800
|
+
}
|
|
801
|
+
findAll(dir = "./commands") {
|
|
802
|
+
if (!import_fs.default.existsSync(dir)) return [];
|
|
803
|
+
return import_fs.default.readdirSync(dir).filter((f) => f.endsWith(".cmd")).map((f) => import_path.default.join(dir, f));
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// src/network/smart-fetch.ts
|
|
810
|
+
var import_fs2, import_path2, SmartFetch;
|
|
811
|
+
var init_smart_fetch = __esm({
|
|
812
|
+
"src/network/smart-fetch.ts"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
init_cjs_shims();
|
|
815
|
+
init_logger();
|
|
816
|
+
import_fs2 = __toESM(require("fs"));
|
|
817
|
+
import_path2 = __toESM(require("path"));
|
|
818
|
+
SmartFetch = class {
|
|
819
|
+
pageContext = null;
|
|
820
|
+
lastReferer = "";
|
|
821
|
+
setPageContext(page) {
|
|
822
|
+
this.pageContext = page;
|
|
823
|
+
}
|
|
824
|
+
async fetch(options2) {
|
|
825
|
+
const {
|
|
826
|
+
url,
|
|
827
|
+
referer,
|
|
828
|
+
autoReferer = true,
|
|
829
|
+
method = "GET",
|
|
830
|
+
headers = {},
|
|
831
|
+
cookies = {},
|
|
832
|
+
body,
|
|
833
|
+
followRedirects = true,
|
|
834
|
+
timeout = 3e4
|
|
835
|
+
} = options2;
|
|
836
|
+
const finalReferer = referer || (autoReferer && this.pageContext ? this.pageContext.url() : this.lastReferer);
|
|
837
|
+
const finalHeaders = {
|
|
838
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
839
|
+
"Accept": "*/*",
|
|
840
|
+
...headers
|
|
841
|
+
};
|
|
842
|
+
if (finalReferer) {
|
|
843
|
+
finalHeaders["Referer"] = finalReferer;
|
|
844
|
+
logger.info(`[smart-fetch] Auto-Referer: ${finalReferer}`);
|
|
845
|
+
}
|
|
846
|
+
if (Object.keys(cookies).length > 0) {
|
|
847
|
+
const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
848
|
+
finalHeaders["Cookie"] = cookieString;
|
|
849
|
+
}
|
|
850
|
+
logger.info(`[smart-fetch] ${method} ${url}`);
|
|
851
|
+
try {
|
|
852
|
+
const response = await fetch(url, {
|
|
853
|
+
method,
|
|
854
|
+
headers: finalHeaders,
|
|
855
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
856
|
+
redirect: followRedirects ? "follow" : "manual",
|
|
857
|
+
signal: AbortSignal.timeout(timeout)
|
|
858
|
+
});
|
|
859
|
+
const contentType = response.headers.get("content-type") || "";
|
|
860
|
+
let data;
|
|
861
|
+
if (contentType.includes("application/json")) {
|
|
862
|
+
data = await response.json();
|
|
863
|
+
} else if (contentType.includes("text")) {
|
|
864
|
+
data = await response.text();
|
|
865
|
+
} else {
|
|
866
|
+
data = await response.arrayBuffer();
|
|
867
|
+
}
|
|
868
|
+
this.lastReferer = url;
|
|
869
|
+
const headersObj = {};
|
|
870
|
+
response.headers.forEach((value, key) => {
|
|
871
|
+
headersObj[key] = value;
|
|
872
|
+
});
|
|
873
|
+
return {
|
|
874
|
+
status: response.status,
|
|
875
|
+
headers: headersObj,
|
|
876
|
+
data
|
|
877
|
+
};
|
|
878
|
+
} catch (err) {
|
|
879
|
+
logger.error(`[smart-fetch] Failed: ${err.message}`);
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async download(url, outputPath, referer) {
|
|
884
|
+
logger.info(`[smart-fetch] Downloading ${url} to ${outputPath}`);
|
|
885
|
+
const response = await this.fetch({
|
|
886
|
+
url,
|
|
887
|
+
referer,
|
|
888
|
+
autoReferer: true
|
|
889
|
+
});
|
|
890
|
+
const dir = import_path2.default.dirname(outputPath);
|
|
891
|
+
if (!import_fs2.default.existsSync(dir)) {
|
|
892
|
+
import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
893
|
+
}
|
|
894
|
+
if (response.data instanceof ArrayBuffer) {
|
|
895
|
+
import_fs2.default.writeFileSync(outputPath, Buffer.from(response.data));
|
|
896
|
+
} else if (typeof response.data === "string") {
|
|
897
|
+
import_fs2.default.writeFileSync(outputPath, response.data);
|
|
898
|
+
} else {
|
|
899
|
+
import_fs2.default.writeFileSync(outputPath, JSON.stringify(response.data));
|
|
900
|
+
}
|
|
901
|
+
logger.info(`[smart-fetch] Downloaded to ${outputPath}`);
|
|
902
|
+
}
|
|
903
|
+
getLastReferer() {
|
|
904
|
+
return this.lastReferer;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/engine/cmd-executor.ts
|
|
911
|
+
var CommandExecutor;
|
|
912
|
+
var init_cmd_executor = __esm({
|
|
913
|
+
"src/engine/cmd-executor.ts"() {
|
|
914
|
+
"use strict";
|
|
915
|
+
init_cjs_shims();
|
|
916
|
+
init_logger();
|
|
917
|
+
init_config();
|
|
918
|
+
init_smart_fetch();
|
|
919
|
+
init_cloudflare();
|
|
920
|
+
CommandExecutor = class {
|
|
921
|
+
page;
|
|
922
|
+
url;
|
|
923
|
+
result = {
|
|
924
|
+
success: false,
|
|
925
|
+
skipped: [],
|
|
926
|
+
extracted: [],
|
|
927
|
+
errors: []
|
|
928
|
+
};
|
|
929
|
+
smartFetch;
|
|
930
|
+
cfManager;
|
|
931
|
+
variables = {};
|
|
932
|
+
constructor(page, url) {
|
|
933
|
+
this.page = page;
|
|
934
|
+
this.url = url;
|
|
935
|
+
this.smartFetch = new SmartFetch();
|
|
936
|
+
this.smartFetch.setPageContext(page);
|
|
937
|
+
this.cfManager = new CloudflareManager();
|
|
938
|
+
}
|
|
939
|
+
async execute(cmd) {
|
|
940
|
+
logger.info(`Executing ${cmd.site}.cmd - ${cmd.steps.length} steps`);
|
|
941
|
+
for (const step2 of cmd.steps) {
|
|
942
|
+
await this.runStep(step2);
|
|
943
|
+
}
|
|
944
|
+
if (this.result.errors.length > 0) {
|
|
945
|
+
logger.warn(`Completed with ${this.result.errors.length} skipped steps`);
|
|
946
|
+
for (const err of this.result.errors) {
|
|
947
|
+
logger.warn(` Line ${err.line} - ${err.action}: ${err.error}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
this.result.success = true;
|
|
951
|
+
return this.result;
|
|
952
|
+
}
|
|
953
|
+
async runStep(cmdStep) {
|
|
954
|
+
const { action, args, line } = cmdStep;
|
|
955
|
+
step(this.url, `${action} ${args.join(" ")}`, { mode: "cmd", step: action });
|
|
956
|
+
try {
|
|
957
|
+
switch (action) {
|
|
958
|
+
case "GOTO":
|
|
959
|
+
await this.goto(args);
|
|
960
|
+
break;
|
|
961
|
+
case "BACK":
|
|
962
|
+
await this.page.goBack();
|
|
963
|
+
break;
|
|
964
|
+
case "FORWARD":
|
|
965
|
+
await this.page.goForward();
|
|
966
|
+
break;
|
|
967
|
+
case "REFRESH":
|
|
968
|
+
await this.page.reload();
|
|
969
|
+
break;
|
|
970
|
+
case "CLICK":
|
|
971
|
+
await this.click(args);
|
|
972
|
+
break;
|
|
973
|
+
case "TYPE":
|
|
974
|
+
await this.type(args);
|
|
975
|
+
break;
|
|
976
|
+
case "PRESS":
|
|
977
|
+
await this.press(args);
|
|
978
|
+
break;
|
|
979
|
+
case "SELECT":
|
|
980
|
+
await this.select(args);
|
|
981
|
+
break;
|
|
982
|
+
case "CHECK":
|
|
983
|
+
await this.check(args);
|
|
984
|
+
break;
|
|
985
|
+
case "UPLOAD":
|
|
986
|
+
await this.upload(args);
|
|
987
|
+
break;
|
|
988
|
+
case "WAIT":
|
|
989
|
+
await this.wait(args);
|
|
990
|
+
break;
|
|
991
|
+
case "WAITLOAD":
|
|
992
|
+
await this.page.waitForLoadState("networkidle");
|
|
993
|
+
break;
|
|
994
|
+
case "SCROLL":
|
|
995
|
+
await this.scroll(args);
|
|
996
|
+
break;
|
|
997
|
+
case "SCROLLDOWN":
|
|
998
|
+
await this.scrollDown(args);
|
|
999
|
+
break;
|
|
1000
|
+
case "SCAN":
|
|
1001
|
+
await this.scan(args);
|
|
1002
|
+
break;
|
|
1003
|
+
case "EXTRACT":
|
|
1004
|
+
await this.extract(args);
|
|
1005
|
+
break;
|
|
1006
|
+
case "SCREENSHOT":
|
|
1007
|
+
await this.screenshot(args);
|
|
1008
|
+
break;
|
|
1009
|
+
case "PAGINATE":
|
|
1010
|
+
await this.paginate(args);
|
|
1011
|
+
break;
|
|
1012
|
+
case "INFINITESCROLL":
|
|
1013
|
+
await this.infiniteScroll();
|
|
1014
|
+
break;
|
|
1015
|
+
case "FETCH":
|
|
1016
|
+
await this.fetch(args);
|
|
1017
|
+
break;
|
|
1018
|
+
case "DOWNLOAD":
|
|
1019
|
+
await this.download(args);
|
|
1020
|
+
break;
|
|
1021
|
+
case "REFERER":
|
|
1022
|
+
await this.setReferer(args);
|
|
1023
|
+
break;
|
|
1024
|
+
case "BYPASS_CLOUDFLARE":
|
|
1025
|
+
await this.bypassCloudflare(args);
|
|
1026
|
+
break;
|
|
1027
|
+
case "REPEAT":
|
|
1028
|
+
await this.repeat(cmdStep);
|
|
1029
|
+
break;
|
|
1030
|
+
case "IF":
|
|
1031
|
+
await this.conditional(cmdStep);
|
|
1032
|
+
break;
|
|
1033
|
+
case "LOOP":
|
|
1034
|
+
await this.loop(cmdStep);
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1039
|
+
logger.warn(`Line ${line} SKIPPED - ${action}: ${msg}`);
|
|
1040
|
+
this.result.errors.push({ line, action, error: msg });
|
|
1041
|
+
this.result.skipped.push(`Line ${line}: ${action} ${args.join(" ")}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async goto(args) {
|
|
1045
|
+
const url = args[0];
|
|
1046
|
+
if (!url) throw new Error("GOTO requires a URL");
|
|
1047
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: config.browser.timeout });
|
|
1048
|
+
step(this.url, `GOTO ${url}`, { mode: "cmd", step: "GOTO", url });
|
|
1049
|
+
}
|
|
1050
|
+
async click(args) {
|
|
1051
|
+
const selector = args[0];
|
|
1052
|
+
if (!selector) throw new Error("CLICK requires a selector");
|
|
1053
|
+
await this.page.waitForSelector(selector, { timeout: 1e4 });
|
|
1054
|
+
await this.page.click(selector);
|
|
1055
|
+
}
|
|
1056
|
+
async type(args) {
|
|
1057
|
+
const selector = args[0];
|
|
1058
|
+
const text2 = args.slice(1).join(" ");
|
|
1059
|
+
if (!selector) throw new Error("TYPE requires a selector");
|
|
1060
|
+
if (!text2) throw new Error("TYPE requires text");
|
|
1061
|
+
await this.page.waitForSelector(selector, { timeout: 1e4 });
|
|
1062
|
+
await this.page.fill(selector, text2);
|
|
1063
|
+
}
|
|
1064
|
+
async press(args) {
|
|
1065
|
+
const key = args[0];
|
|
1066
|
+
if (!key) throw new Error("PRESS requires a key");
|
|
1067
|
+
await this.page.keyboard.press(key);
|
|
1068
|
+
}
|
|
1069
|
+
async select(args) {
|
|
1070
|
+
const selector = args[0];
|
|
1071
|
+
const value = args[1];
|
|
1072
|
+
if (!selector) throw new Error("SELECT requires a selector");
|
|
1073
|
+
if (!value) throw new Error("SELECT requires a value");
|
|
1074
|
+
await this.page.selectOption(selector, value);
|
|
1075
|
+
}
|
|
1076
|
+
async check(args) {
|
|
1077
|
+
const selector = args[0];
|
|
1078
|
+
if (!selector) throw new Error("CHECK requires a selector");
|
|
1079
|
+
await this.page.check(selector);
|
|
1080
|
+
}
|
|
1081
|
+
async upload(args) {
|
|
1082
|
+
const selector = args[0];
|
|
1083
|
+
const filePath = args[1];
|
|
1084
|
+
if (!selector) throw new Error("UPLOAD requires a selector");
|
|
1085
|
+
if (!filePath) throw new Error("UPLOAD requires a file path");
|
|
1086
|
+
await this.page.setInputFiles(selector, filePath);
|
|
1087
|
+
}
|
|
1088
|
+
async wait(args) {
|
|
1089
|
+
const target = args[0];
|
|
1090
|
+
if (!target) throw new Error("WAIT requires a selector or ms value");
|
|
1091
|
+
if (/^\d+$/.test(target)) {
|
|
1092
|
+
await this.page.waitForTimeout(parseInt(target, 10));
|
|
1093
|
+
} else {
|
|
1094
|
+
await this.page.waitForSelector(target, { timeout: config.browser.timeout });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async scroll(args) {
|
|
1098
|
+
const selector = args[0];
|
|
1099
|
+
if (!selector) throw new Error("SCROLL requires a selector");
|
|
1100
|
+
await this.page.locator(selector).scrollIntoViewIfNeeded();
|
|
1101
|
+
}
|
|
1102
|
+
async scrollDown(args) {
|
|
1103
|
+
const pixels = parseInt(args[0] || "500", 10);
|
|
1104
|
+
await this.page.evaluate((px) => window.scrollBy(0, px), pixels);
|
|
1105
|
+
}
|
|
1106
|
+
async scan(args) {
|
|
1107
|
+
const selector = args[0];
|
|
1108
|
+
if (!selector) throw new Error("SCAN requires a selector");
|
|
1109
|
+
const elements = await this.page.$$(selector);
|
|
1110
|
+
const found = [];
|
|
1111
|
+
for (const el of elements) {
|
|
1112
|
+
const tag = await el.evaluate((e) => e.tagName.toLowerCase());
|
|
1113
|
+
const text2 = await el.textContent();
|
|
1114
|
+
const href = await el.getAttribute("href");
|
|
1115
|
+
const src = await el.getAttribute("src");
|
|
1116
|
+
const id = await el.getAttribute("id");
|
|
1117
|
+
const cls = await el.getAttribute("class");
|
|
1118
|
+
found.push({ tag, text: text2?.trim().slice(0, 100), href, src, id, class: cls });
|
|
1119
|
+
}
|
|
1120
|
+
this.result.extracted.push({ type: "scan", selector, count: found.length, found });
|
|
1121
|
+
highlight(this.url, { type: "scan", selector, count: found.length });
|
|
1122
|
+
step(this.url, `SCAN found ${found.length} elements matching "${selector}"`, { mode: "cmd", step: "SCAN" });
|
|
1123
|
+
}
|
|
1124
|
+
async extract(args) {
|
|
1125
|
+
const selector = args[0];
|
|
1126
|
+
const attr = args[1] || "text";
|
|
1127
|
+
if (!selector) throw new Error("EXTRACT requires a selector");
|
|
1128
|
+
const elements = await this.page.$$(selector);
|
|
1129
|
+
const data = [];
|
|
1130
|
+
for (const el of elements) {
|
|
1131
|
+
if (attr === "text") {
|
|
1132
|
+
const text2 = await el.textContent();
|
|
1133
|
+
data.push(text2?.trim());
|
|
1134
|
+
} else {
|
|
1135
|
+
const value = await el.getAttribute(attr);
|
|
1136
|
+
data.push(value);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
this.result.extracted.push({ selector, attr, count: data.length, data });
|
|
1140
|
+
highlight(this.url, { selector, attr, count: data.length });
|
|
1141
|
+
}
|
|
1142
|
+
async screenshot(args) {
|
|
1143
|
+
const path4 = args[0] || `screenshot-${Date.now()}.png`;
|
|
1144
|
+
await this.page.screenshot({ path: path4, fullPage: true });
|
|
1145
|
+
logger.info(`Screenshot saved: ${path4}`);
|
|
1146
|
+
}
|
|
1147
|
+
async paginate(args) {
|
|
1148
|
+
const selector = args[0];
|
|
1149
|
+
if (!selector) throw new Error("PAGINATE requires a selector");
|
|
1150
|
+
let page = 1;
|
|
1151
|
+
while (true) {
|
|
1152
|
+
try {
|
|
1153
|
+
await this.page.waitForSelector(selector, { timeout: 5e3 });
|
|
1154
|
+
logger.info(`Clicking next page (${page})`);
|
|
1155
|
+
await this.page.click(selector);
|
|
1156
|
+
await this.page.waitForLoadState("networkidle");
|
|
1157
|
+
page++;
|
|
1158
|
+
} catch {
|
|
1159
|
+
logger.info(`Pagination complete - ${page} pages`);
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
async infiniteScroll() {
|
|
1165
|
+
let previousHeight = 0;
|
|
1166
|
+
let attempts = 0;
|
|
1167
|
+
const maxAttempts = 50;
|
|
1168
|
+
while (attempts < maxAttempts) {
|
|
1169
|
+
const currentHeight = await this.page.evaluate(() => document.body.scrollHeight);
|
|
1170
|
+
if (currentHeight === previousHeight) {
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
1174
|
+
await this.page.waitForTimeout(1e3);
|
|
1175
|
+
previousHeight = currentHeight;
|
|
1176
|
+
attempts++;
|
|
1177
|
+
}
|
|
1178
|
+
logger.info(`Infinite scroll complete - ${attempts} scrolls`);
|
|
1179
|
+
}
|
|
1180
|
+
async fetch(args) {
|
|
1181
|
+
const url = args[0];
|
|
1182
|
+
const varName = args[1];
|
|
1183
|
+
if (!url) throw new Error("FETCH requires a URL");
|
|
1184
|
+
const response = await this.smartFetch.fetch({
|
|
1185
|
+
url,
|
|
1186
|
+
autoReferer: true
|
|
1187
|
+
});
|
|
1188
|
+
if (varName) {
|
|
1189
|
+
this.variables[varName] = response.data;
|
|
1190
|
+
logger.info(`Saved response to variable: ${varName}`);
|
|
1191
|
+
}
|
|
1192
|
+
this.result.extracted.push({
|
|
1193
|
+
type: "fetch",
|
|
1194
|
+
url,
|
|
1195
|
+
status: response.status,
|
|
1196
|
+
data: response.data
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
async download(args) {
|
|
1200
|
+
const url = args[0];
|
|
1201
|
+
const outputPath = args[1] || `./downloads/${Date.now()}.bin`;
|
|
1202
|
+
const referer = args[2];
|
|
1203
|
+
if (!url) throw new Error("DOWNLOAD requires a URL");
|
|
1204
|
+
logger.info(`Downloading: ${url}`);
|
|
1205
|
+
await this.smartFetch.download(url, outputPath, referer);
|
|
1206
|
+
this.result.extracted.push({
|
|
1207
|
+
type: "download",
|
|
1208
|
+
url,
|
|
1209
|
+
path: outputPath
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
async setReferer(args) {
|
|
1213
|
+
const referer = args[0];
|
|
1214
|
+
if (!referer) throw new Error("REFERER requires a URL");
|
|
1215
|
+
logger.info(`Set manual Referer: ${referer}`);
|
|
1216
|
+
}
|
|
1217
|
+
async bypassCloudflare(args) {
|
|
1218
|
+
const mode = args[0] || "auto";
|
|
1219
|
+
await this.cfManager.handleCloudflare(this.page, this.url);
|
|
1220
|
+
}
|
|
1221
|
+
async repeat(stepCmd) {
|
|
1222
|
+
const selector = stepCmd.args[0];
|
|
1223
|
+
if (!selector) throw new Error("REPEAT requires a selector");
|
|
1224
|
+
if (!stepCmd.children?.length) throw new Error("REPEAT has no child commands");
|
|
1225
|
+
const elements = await this.page.$$(selector);
|
|
1226
|
+
step(this.url, `REPEAT ${elements.length}x over "${selector}"`, { mode: "cmd", step: "REPEAT" });
|
|
1227
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1228
|
+
step(this.url, ` REPEAT iteration ${i + 1}/${elements.length}`, { mode: "cmd" });
|
|
1229
|
+
for (const child of stepCmd.children) {
|
|
1230
|
+
await this.runStep(child);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async conditional(stepCmd) {
|
|
1235
|
+
const selector = stepCmd.args[0];
|
|
1236
|
+
if (!selector) throw new Error("IF requires a selector");
|
|
1237
|
+
if (!stepCmd.children?.length) throw new Error("IF has no child commands");
|
|
1238
|
+
const exists = await this.page.locator(selector).count() > 0;
|
|
1239
|
+
if (exists && stepCmd.children) {
|
|
1240
|
+
for (const child of stepCmd.children) {
|
|
1241
|
+
await this.runStep(child);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async loop(stepCmd) {
|
|
1246
|
+
const count = parseInt(stepCmd.args[0] || "1", 10);
|
|
1247
|
+
if (!stepCmd.children?.length) throw new Error("LOOP has no child commands");
|
|
1248
|
+
for (let i = 0; i < count; i++) {
|
|
1249
|
+
step(this.url, ` LOOP iteration ${i + 1}/${count}`, { mode: "cmd" });
|
|
1250
|
+
for (const child of stepCmd.children) {
|
|
1251
|
+
await this.runStep(child);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// src/modes/downloader.ts
|
|
1260
|
+
var DownloaderMode;
|
|
1261
|
+
var init_downloader = __esm({
|
|
1262
|
+
"src/modes/downloader.ts"() {
|
|
1263
|
+
"use strict";
|
|
1264
|
+
init_cjs_shims();
|
|
1265
|
+
init_logger();
|
|
1266
|
+
init_smart_fetch();
|
|
1267
|
+
DownloaderMode = class {
|
|
1268
|
+
page;
|
|
1269
|
+
smartFetch;
|
|
1270
|
+
constructor(page) {
|
|
1271
|
+
this.page = page;
|
|
1272
|
+
this.smartFetch = new SmartFetch();
|
|
1273
|
+
this.smartFetch.setPageContext(page);
|
|
1274
|
+
}
|
|
1275
|
+
async execute(url) {
|
|
1276
|
+
logger.info("[downloader-mode] Analyzing download flow...");
|
|
1277
|
+
const flow = await this.detectDownloadFlow();
|
|
1278
|
+
if (!flow) {
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
data: {},
|
|
1282
|
+
errors: ["No download flow detected"],
|
|
1283
|
+
timestamp: Date.now()
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
logger.info(`[downloader-mode] Flow type: ${flow.type}`);
|
|
1287
|
+
try {
|
|
1288
|
+
let downloadedFiles = [];
|
|
1289
|
+
if (flow.type === "DIRECT" && flow.links) {
|
|
1290
|
+
downloadedFiles = await this.downloadDirectLinks(flow.links);
|
|
1291
|
+
} else if (flow.type === "BUTTON_CLICK" && flow.button) {
|
|
1292
|
+
downloadedFiles = await this.downloadViaButton(flow.button);
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
success: true,
|
|
1296
|
+
data: {
|
|
1297
|
+
flow: flow.type,
|
|
1298
|
+
files: downloadedFiles
|
|
1299
|
+
},
|
|
1300
|
+
errors: [],
|
|
1301
|
+
timestamp: Date.now()
|
|
1302
|
+
};
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
return {
|
|
1305
|
+
success: false,
|
|
1306
|
+
data: {},
|
|
1307
|
+
errors: [err.message],
|
|
1308
|
+
timestamp: Date.now()
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async detectDownloadFlow() {
|
|
1313
|
+
const directLinks = await this.findDirectLinks();
|
|
1314
|
+
if (directLinks.length > 0) {
|
|
1315
|
+
return {
|
|
1316
|
+
type: "DIRECT",
|
|
1317
|
+
steps: ["Found direct download links"],
|
|
1318
|
+
links: directLinks
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
const downloadButton = await this.findDownloadButton();
|
|
1322
|
+
if (downloadButton) {
|
|
1323
|
+
return {
|
|
1324
|
+
type: "BUTTON_CLICK",
|
|
1325
|
+
steps: ["Click download button", "Wait for file"],
|
|
1326
|
+
button: downloadButton
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
async findDirectLinks() {
|
|
1332
|
+
return await this.page.evaluate(() => {
|
|
1333
|
+
const links = Array.from(document.querySelectorAll("a[href]"));
|
|
1334
|
+
const fileLinks = [];
|
|
1335
|
+
const fileExtensions = /\.(mp4|mp3|pdf|zip|rar|exe|dmg|apk|avi|mkv|mov|wav|flac)$/i;
|
|
1336
|
+
for (const link of links) {
|
|
1337
|
+
const href = link.href;
|
|
1338
|
+
if (href && fileExtensions.test(href)) {
|
|
1339
|
+
const selector = link.id ? `#${link.id}` : `a[href="${href}"]`;
|
|
1340
|
+
const ext = href.match(fileExtensions)?.[1];
|
|
1341
|
+
fileLinks.push({
|
|
1342
|
+
url: href,
|
|
1343
|
+
selector,
|
|
1344
|
+
extension: ext
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return fileLinks;
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
async findDownloadButton() {
|
|
1352
|
+
const buttons = await this.page.evaluate(() => {
|
|
1353
|
+
const candidates = Array.from(document.querySelectorAll('button, a, [class*="download"]'));
|
|
1354
|
+
const scored = [];
|
|
1355
|
+
for (const el of candidates) {
|
|
1356
|
+
const text2 = (el.textContent || "").toLowerCase();
|
|
1357
|
+
const className = (el.className || "").toLowerCase();
|
|
1358
|
+
const href = el.href;
|
|
1359
|
+
let score = 0;
|
|
1360
|
+
if (text2.includes("download")) score += 10;
|
|
1361
|
+
if (className.includes("download")) score += 10;
|
|
1362
|
+
if (el.hasAttribute("download")) score += 20;
|
|
1363
|
+
if (href && href.includes("download")) score += 5;
|
|
1364
|
+
if (text2.includes("get")) score += 3;
|
|
1365
|
+
if (text2.includes("save")) score += 3;
|
|
1366
|
+
if (score > 0) {
|
|
1367
|
+
const selector = el.id ? `#${el.id}` : el.tagName.toLowerCase();
|
|
1368
|
+
scored.push({ selector, score, text: text2, href });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
1372
|
+
});
|
|
1373
|
+
return buttons.length > 0 ? buttons[0] : null;
|
|
1374
|
+
}
|
|
1375
|
+
async downloadDirectLinks(links) {
|
|
1376
|
+
const downloaded = [];
|
|
1377
|
+
for (const link of links) {
|
|
1378
|
+
const filename = `download-${Date.now()}.${link.extension || "bin"}`;
|
|
1379
|
+
const outputPath = `./downloads/${filename}`;
|
|
1380
|
+
logger.info(`[downloader-mode] Downloading ${link.url}`);
|
|
1381
|
+
await this.smartFetch.download(link.url, outputPath);
|
|
1382
|
+
downloaded.push(outputPath);
|
|
1383
|
+
}
|
|
1384
|
+
return downloaded;
|
|
1385
|
+
}
|
|
1386
|
+
async downloadViaButton(button) {
|
|
1387
|
+
logger.info(`[downloader-mode] Clicking download button: ${button.selector}`);
|
|
1388
|
+
const [download] = await Promise.all([
|
|
1389
|
+
this.page.waitForEvent("download"),
|
|
1390
|
+
this.page.click(button.selector)
|
|
1391
|
+
]);
|
|
1392
|
+
const filename = download.suggestedFilename();
|
|
1393
|
+
const outputPath = `./downloads/${filename}`;
|
|
1394
|
+
await download.saveAs(outputPath);
|
|
1395
|
+
logger.info(`[downloader-mode] Saved to ${outputPath}`);
|
|
1396
|
+
return [outputPath];
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// src/modes/scrape.ts
|
|
1403
|
+
var ScrapeMode;
|
|
1404
|
+
var init_scrape = __esm({
|
|
1405
|
+
"src/modes/scrape.ts"() {
|
|
1406
|
+
"use strict";
|
|
1407
|
+
init_cjs_shims();
|
|
1408
|
+
init_logger();
|
|
1409
|
+
ScrapeMode = class {
|
|
1410
|
+
page;
|
|
1411
|
+
constructor(page) {
|
|
1412
|
+
this.page = page;
|
|
1413
|
+
}
|
|
1414
|
+
async execute(url) {
|
|
1415
|
+
logger.info("[scrape-mode] Extracting content...");
|
|
1416
|
+
try {
|
|
1417
|
+
const data = await this.extractContent();
|
|
1418
|
+
return {
|
|
1419
|
+
success: true,
|
|
1420
|
+
data,
|
|
1421
|
+
errors: [],
|
|
1422
|
+
timestamp: Date.now()
|
|
1423
|
+
};
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
return {
|
|
1426
|
+
success: false,
|
|
1427
|
+
data: {},
|
|
1428
|
+
errors: [err.message],
|
|
1429
|
+
timestamp: Date.now()
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async extractContent() {
|
|
1434
|
+
return await this.page.evaluate(() => {
|
|
1435
|
+
const result = {};
|
|
1436
|
+
const title = document.querySelector("h1")?.textContent?.trim();
|
|
1437
|
+
if (title) result.title = title;
|
|
1438
|
+
const description = document.querySelector('meta[name="description"]')?.getAttribute("content");
|
|
1439
|
+
if (description) result.description = description;
|
|
1440
|
+
const images = Array.from(document.querySelectorAll("img[src]")).map((img) => img.src).filter(Boolean);
|
|
1441
|
+
if (images.length > 0) result.images = images;
|
|
1442
|
+
const links = Array.from(document.querySelectorAll("a[href]")).map((a) => ({
|
|
1443
|
+
text: a.textContent?.trim(),
|
|
1444
|
+
href: a.href
|
|
1445
|
+
})).filter((l) => l.text && l.href);
|
|
1446
|
+
if (links.length > 0) result.links = links;
|
|
1447
|
+
const paragraphs = Array.from(document.querySelectorAll("p")).map((p) => p.textContent?.trim()).filter(Boolean);
|
|
1448
|
+
if (paragraphs.length > 0) result.content = paragraphs;
|
|
1449
|
+
return result;
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
// src/modes/navigator.ts
|
|
1457
|
+
var NavigatorMode;
|
|
1458
|
+
var init_navigator2 = __esm({
|
|
1459
|
+
"src/modes/navigator.ts"() {
|
|
1460
|
+
"use strict";
|
|
1461
|
+
init_cjs_shims();
|
|
1462
|
+
init_logger();
|
|
1463
|
+
NavigatorMode = class {
|
|
1464
|
+
page;
|
|
1465
|
+
constructor(page) {
|
|
1466
|
+
this.page = page;
|
|
1467
|
+
}
|
|
1468
|
+
async execute(url) {
|
|
1469
|
+
logger.info("[navigator-mode] Mapping site structure...");
|
|
1470
|
+
try {
|
|
1471
|
+
const siteMap = await this.buildSiteMap();
|
|
1472
|
+
return {
|
|
1473
|
+
success: true,
|
|
1474
|
+
data: siteMap,
|
|
1475
|
+
errors: [],
|
|
1476
|
+
timestamp: Date.now()
|
|
1477
|
+
};
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
return {
|
|
1480
|
+
success: false,
|
|
1481
|
+
data: {},
|
|
1482
|
+
errors: [err.message],
|
|
1483
|
+
timestamp: Date.now()
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async buildSiteMap() {
|
|
1488
|
+
const currentUrl = this.page.url();
|
|
1489
|
+
const structure = await this.page.evaluate(() => {
|
|
1490
|
+
const nav = document.querySelector("nav");
|
|
1491
|
+
const menu = document.querySelector('[class*="menu"]');
|
|
1492
|
+
const header = document.querySelector("header");
|
|
1493
|
+
const navLinks = nav || menu || header;
|
|
1494
|
+
const links = [];
|
|
1495
|
+
if (navLinks) {
|
|
1496
|
+
const anchors = navLinks.querySelectorAll("a[href]");
|
|
1497
|
+
for (const a of anchors) {
|
|
1498
|
+
const text2 = a.textContent?.trim();
|
|
1499
|
+
const href = a.href;
|
|
1500
|
+
if (text2 && href) {
|
|
1501
|
+
links.push({ text: text2, href });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
title: document.title,
|
|
1507
|
+
url: window.location.href,
|
|
1508
|
+
navigation: links,
|
|
1509
|
+
sections: Array.from(document.querySelectorAll("section, article")).length
|
|
1510
|
+
};
|
|
1511
|
+
});
|
|
1512
|
+
return structure;
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// src/modes/auto.ts
|
|
1519
|
+
var auto_exports = {};
|
|
1520
|
+
__export(auto_exports, {
|
|
1521
|
+
AutoMode: () => AutoMode
|
|
1522
|
+
});
|
|
1523
|
+
var AutoMode;
|
|
1524
|
+
var init_auto = __esm({
|
|
1525
|
+
"src/modes/auto.ts"() {
|
|
1526
|
+
"use strict";
|
|
1527
|
+
init_cjs_shims();
|
|
1528
|
+
init_logger();
|
|
1529
|
+
init_downloader();
|
|
1530
|
+
init_scrape();
|
|
1531
|
+
init_navigator2();
|
|
1532
|
+
AutoMode = class {
|
|
1533
|
+
page;
|
|
1534
|
+
constructor(page) {
|
|
1535
|
+
this.page = page;
|
|
1536
|
+
}
|
|
1537
|
+
async execute(url) {
|
|
1538
|
+
logger.info("[auto-mode] Analyzing site...");
|
|
1539
|
+
const mode = await this.detectBestMode();
|
|
1540
|
+
logger.info(`[auto-mode] Selected mode: ${mode}`);
|
|
1541
|
+
let result;
|
|
1542
|
+
switch (mode) {
|
|
1543
|
+
case "downloader":
|
|
1544
|
+
const downloader = new DownloaderMode(this.page);
|
|
1545
|
+
result = await downloader.execute(url);
|
|
1546
|
+
break;
|
|
1547
|
+
case "scrape":
|
|
1548
|
+
const scraper = new ScrapeMode(this.page);
|
|
1549
|
+
result = await scraper.execute(url);
|
|
1550
|
+
break;
|
|
1551
|
+
case "navigator":
|
|
1552
|
+
const navigator2 = new NavigatorMode(this.page);
|
|
1553
|
+
result = await navigator2.execute(url);
|
|
1554
|
+
break;
|
|
1555
|
+
default:
|
|
1556
|
+
result = {
|
|
1557
|
+
success: false,
|
|
1558
|
+
data: {},
|
|
1559
|
+
errors: ["Unknown mode"],
|
|
1560
|
+
timestamp: Date.now()
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return result;
|
|
1564
|
+
}
|
|
1565
|
+
async detectBestMode() {
|
|
1566
|
+
const indicators = await this.page.evaluate(() => {
|
|
1567
|
+
const hasDownloadButton = !!document.querySelector('a[download], button:has-text("Download")');
|
|
1568
|
+
const hasVideoPlayer = !!document.querySelector('video, iframe[src*="youtube"], iframe[src*="vimeo"]');
|
|
1569
|
+
const hasFileLinks = !!document.querySelector('a[href$=".mp4"], a[href$=".pdf"], a[href$=".zip"]');
|
|
1570
|
+
const hasPagination = !!document.querySelector('.pagination, .next, [class*="page-"]');
|
|
1571
|
+
const hasInfiniteScroll = document.body.scrollHeight > window.innerHeight * 3;
|
|
1572
|
+
const hasForm = !!document.querySelector("form");
|
|
1573
|
+
const hasSearch = !!document.querySelector('input[type="search"], input[placeholder*="search"]');
|
|
1574
|
+
return {
|
|
1575
|
+
hasDownloadButton,
|
|
1576
|
+
hasVideoPlayer,
|
|
1577
|
+
hasFileLinks,
|
|
1578
|
+
hasPagination,
|
|
1579
|
+
hasInfiniteScroll,
|
|
1580
|
+
hasForm,
|
|
1581
|
+
hasSearch
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
if (indicators.hasDownloadButton || indicators.hasVideoPlayer || indicators.hasFileLinks) {
|
|
1585
|
+
return "downloader";
|
|
1586
|
+
}
|
|
1587
|
+
if (indicators.hasPagination || indicators.hasInfiniteScroll) {
|
|
1588
|
+
return "scrape";
|
|
1589
|
+
}
|
|
1590
|
+
return "navigator";
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// src/core/scraper.ts
|
|
1597
|
+
var import_playwright, FirekidScraper;
|
|
1598
|
+
var init_scraper = __esm({
|
|
1599
|
+
"src/core/scraper.ts"() {
|
|
1600
|
+
"use strict";
|
|
1601
|
+
init_cjs_shims();
|
|
1602
|
+
import_playwright = require("playwright");
|
|
1603
|
+
init_ghost();
|
|
1604
|
+
init_cloudflare();
|
|
1605
|
+
init_cmd_parser();
|
|
1606
|
+
init_cmd_executor();
|
|
1607
|
+
init_logger();
|
|
1608
|
+
init_config();
|
|
1609
|
+
FirekidScraper = class {
|
|
1610
|
+
config;
|
|
1611
|
+
browser = null;
|
|
1612
|
+
context = null;
|
|
1613
|
+
page = null;
|
|
1614
|
+
cfManager;
|
|
1615
|
+
constructor(userConfig = {}) {
|
|
1616
|
+
this.config = {
|
|
1617
|
+
headless: userConfig.headless ?? config.browser.headless,
|
|
1618
|
+
bypassCloudflare: userConfig.bypassCloudflare ?? true,
|
|
1619
|
+
maxWorkers: userConfig.maxWorkers ?? config.browser.maxWorkers,
|
|
1620
|
+
timeout: userConfig.timeout ?? config.browser.timeout,
|
|
1621
|
+
dataDir: userConfig.dataDir ?? config.storage.dataDir,
|
|
1622
|
+
logLevel: userConfig.logLevel ?? config.logging.level
|
|
1623
|
+
};
|
|
1624
|
+
this.cfManager = new CloudflareManager();
|
|
1625
|
+
}
|
|
1626
|
+
async init() {
|
|
1627
|
+
if (this.browser) return;
|
|
1628
|
+
logger.info("Initializing Firekid Scraper...");
|
|
1629
|
+
this.browser = await import_playwright.chromium.launch({
|
|
1630
|
+
headless: this.config.headless,
|
|
1631
|
+
args: [
|
|
1632
|
+
"--disable-blink-features=AutomationControlled",
|
|
1633
|
+
"--no-sandbox"
|
|
1634
|
+
]
|
|
1635
|
+
});
|
|
1636
|
+
this.context = await this.browser.newContext({
|
|
1637
|
+
viewport: { width: 1920, height: 1080 },
|
|
1638
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1639
|
+
});
|
|
1640
|
+
await applyGhost(this.context);
|
|
1641
|
+
this.page = await this.context.newPage();
|
|
1642
|
+
logger.info("Firekid Scraper initialized");
|
|
1643
|
+
}
|
|
1644
|
+
async goto(url) {
|
|
1645
|
+
await this.init();
|
|
1646
|
+
if (!this.page) throw new Error("Page not initialized");
|
|
1647
|
+
logger.info(`Navigating to ${url}`);
|
|
1648
|
+
await this.page.goto(url, {
|
|
1649
|
+
waitUntil: "domcontentloaded",
|
|
1650
|
+
timeout: this.config.timeout
|
|
1651
|
+
});
|
|
1652
|
+
if (this.config.bypassCloudflare) {
|
|
1653
|
+
await this.cfManager.handleCloudflare(this.page, url);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
async extract(url, selectors) {
|
|
1657
|
+
await this.goto(url);
|
|
1658
|
+
if (!this.page) throw new Error("Page not initialized");
|
|
1659
|
+
const data = {};
|
|
1660
|
+
for (const [key, selector] of Object.entries(selectors)) {
|
|
1661
|
+
try {
|
|
1662
|
+
const element = await this.page.locator(selector).first();
|
|
1663
|
+
const text2 = await element.textContent();
|
|
1664
|
+
data[key] = text2?.trim() || null;
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
logger.warn(`Failed to extract ${key} with selector ${selector}`);
|
|
1667
|
+
data[key] = null;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return data;
|
|
1671
|
+
}
|
|
1672
|
+
async auto(url) {
|
|
1673
|
+
await this.goto(url);
|
|
1674
|
+
if (!this.page) throw new Error("Page not initialized");
|
|
1675
|
+
logger.info("Running auto mode...");
|
|
1676
|
+
const { AutoMode: AutoMode2 } = await Promise.resolve().then(() => (init_auto(), auto_exports));
|
|
1677
|
+
const autoMode = new AutoMode2(this.page);
|
|
1678
|
+
return await autoMode.execute(url);
|
|
1679
|
+
}
|
|
1680
|
+
async runCommandFile(filePath) {
|
|
1681
|
+
await this.init();
|
|
1682
|
+
if (!this.page) throw new Error("Page not initialized");
|
|
1683
|
+
const parser = new CommandParser();
|
|
1684
|
+
const cmdFile = parser.load(filePath);
|
|
1685
|
+
logger.info(`Executing command file: ${cmdFile.site}`);
|
|
1686
|
+
const executor = new CommandExecutor(this.page, cmdFile.steps[0]?.args[0] || "");
|
|
1687
|
+
const result = await executor.execute(cmdFile);
|
|
1688
|
+
return {
|
|
1689
|
+
success: result.success,
|
|
1690
|
+
data: result.extracted,
|
|
1691
|
+
errors: result.errors.map((e) => e.error),
|
|
1692
|
+
timestamp: Date.now()
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
async close() {
|
|
1696
|
+
if (this.page) await this.page.close();
|
|
1697
|
+
if (this.context) await this.context.close();
|
|
1698
|
+
if (this.browser) await this.browser.close();
|
|
1699
|
+
this.page = null;
|
|
1700
|
+
this.context = null;
|
|
1701
|
+
this.browser = null;
|
|
1702
|
+
logger.info("Firekid Scraper closed");
|
|
1703
|
+
}
|
|
1704
|
+
getPage() {
|
|
1705
|
+
return this.page;
|
|
1706
|
+
}
|
|
1707
|
+
getBrowser() {
|
|
1708
|
+
return this.browser;
|
|
1709
|
+
}
|
|
1710
|
+
getContext() {
|
|
1711
|
+
return this.context;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// src/server/app.ts
|
|
1718
|
+
var app_exports = {};
|
|
1719
|
+
__export(app_exports, {
|
|
1720
|
+
startServer: () => startServer
|
|
1721
|
+
});
|
|
1722
|
+
async function startServer(port = 3e3) {
|
|
1723
|
+
app.listen(port, () => {
|
|
1724
|
+
logger.info(`[server] Firekid API server running on port ${port}`);
|
|
1725
|
+
console.log(`
|
|
1726
|
+
Firekid Scraper API Server`);
|
|
1727
|
+
console.log(`Port: ${port}`);
|
|
1728
|
+
console.log(`Health: http://localhost:${port}/health
|
|
1729
|
+
`);
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
var import_express, import_express_rate_limit, app, limiter;
|
|
1733
|
+
var init_app = __esm({
|
|
1734
|
+
"src/server/app.ts"() {
|
|
1735
|
+
"use strict";
|
|
1736
|
+
init_cjs_shims();
|
|
1737
|
+
import_express = __toESM(require("express"));
|
|
1738
|
+
import_express_rate_limit = require("express-rate-limit");
|
|
1739
|
+
init_scraper();
|
|
1740
|
+
init_logger();
|
|
1741
|
+
init_config();
|
|
1742
|
+
app = (0, import_express.default)();
|
|
1743
|
+
app.use(import_express.default.json());
|
|
1744
|
+
limiter = (0, import_express_rate_limit.rateLimit)({
|
|
1745
|
+
windowMs: config.rateLimit.window,
|
|
1746
|
+
max: config.rateLimit.max,
|
|
1747
|
+
message: "Too many requests, please try again later"
|
|
1748
|
+
});
|
|
1749
|
+
if (config.rateLimit.enabled) {
|
|
1750
|
+
app.use(limiter);
|
|
1751
|
+
}
|
|
1752
|
+
app.use((req, res, next) => {
|
|
1753
|
+
const apiKey = req.headers["x-api-key"];
|
|
1754
|
+
if (config.server.apiKey && apiKey !== config.server.apiKey) {
|
|
1755
|
+
return res.status(401).json({ error: "Invalid API key" });
|
|
1756
|
+
}
|
|
1757
|
+
next();
|
|
1758
|
+
});
|
|
1759
|
+
app.post("/scrape", async (req, res) => {
|
|
1760
|
+
const { url, mode = "auto", selectors } = req.body;
|
|
1761
|
+
if (!url) {
|
|
1762
|
+
return res.status(400).json({ error: "URL is required" });
|
|
1763
|
+
}
|
|
1764
|
+
const scraper = new FirekidScraper();
|
|
1765
|
+
try {
|
|
1766
|
+
let result;
|
|
1767
|
+
if (mode === "auto") {
|
|
1768
|
+
result = await scraper.auto(url);
|
|
1769
|
+
} else if (mode === "extract" && selectors) {
|
|
1770
|
+
result = await scraper.extract(url, selectors);
|
|
1771
|
+
} else {
|
|
1772
|
+
return res.status(400).json({ error: "Invalid mode or missing parameters" });
|
|
1773
|
+
}
|
|
1774
|
+
await scraper.close();
|
|
1775
|
+
res.json({ success: true, result });
|
|
1776
|
+
} catch (err) {
|
|
1777
|
+
await scraper.close();
|
|
1778
|
+
logger.error("[api] Scraping error:", err);
|
|
1779
|
+
res.status(500).json({ error: err.message });
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
app.post("/command", async (req, res) => {
|
|
1783
|
+
const { filePath } = req.body;
|
|
1784
|
+
if (!filePath) {
|
|
1785
|
+
return res.status(400).json({ error: "File path is required" });
|
|
1786
|
+
}
|
|
1787
|
+
const scraper = new FirekidScraper();
|
|
1788
|
+
try {
|
|
1789
|
+
const result = await scraper.runCommandFile(filePath);
|
|
1790
|
+
await scraper.close();
|
|
1791
|
+
res.json({ success: true, result });
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
await scraper.close();
|
|
1794
|
+
logger.error("[api] Command execution error:", err);
|
|
1795
|
+
res.status(500).json({ error: err.message });
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
app.get("/health", (req, res) => {
|
|
1799
|
+
res.json({ status: "ok", timestamp: Date.now() });
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// bin/firekid-scraper.ts
|
|
1805
|
+
init_cjs_shims();
|
|
1806
|
+
var import_commander = require("commander");
|
|
1807
|
+
var import_prompts = require("@clack/prompts");
|
|
1808
|
+
init_scraper();
|
|
1809
|
+
|
|
1810
|
+
// src/recorder/recorder.ts
|
|
1811
|
+
init_cjs_shims();
|
|
1812
|
+
var import_playwright2 = require("playwright");
|
|
1813
|
+
|
|
1814
|
+
// src/recorder/selector-generator.ts
|
|
1815
|
+
init_cjs_shims();
|
|
1816
|
+
var SelectorGenerator = class {
|
|
1817
|
+
async generate(page, element) {
|
|
1818
|
+
const selectors = [];
|
|
1819
|
+
if (element.id) {
|
|
1820
|
+
selectors.push(`#${element.id}`);
|
|
1821
|
+
}
|
|
1822
|
+
if (element.className && typeof element.className === "string") {
|
|
1823
|
+
const classes = element.className.split(" ").filter(Boolean);
|
|
1824
|
+
if (classes.length > 0) {
|
|
1825
|
+
selectors.push(`.${classes.join(".")}`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (element.tagName) {
|
|
1829
|
+
const tag = element.tagName.toLowerCase();
|
|
1830
|
+
if (element.type) {
|
|
1831
|
+
selectors.push(`${tag}[type="${element.type}"]`);
|
|
1832
|
+
}
|
|
1833
|
+
if (element.href) {
|
|
1834
|
+
selectors.push(`${tag}[href*="${this.simplifyUrl(element.href)}"]`);
|
|
1835
|
+
}
|
|
1836
|
+
if (element.textContent) {
|
|
1837
|
+
const text2 = element.textContent.trim().slice(0, 30);
|
|
1838
|
+
if (text2) {
|
|
1839
|
+
selectors.push(`${tag}:has-text("${text2}")`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const primary = selectors[0] || "body";
|
|
1844
|
+
const fallbacks = selectors.slice(1);
|
|
1845
|
+
return { primary, fallbacks };
|
|
1846
|
+
}
|
|
1847
|
+
simplifyUrl(url) {
|
|
1848
|
+
try {
|
|
1849
|
+
const parsed = new URL(url);
|
|
1850
|
+
return parsed.pathname;
|
|
1851
|
+
} catch {
|
|
1852
|
+
return url;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async generateCssPath(page, element) {
|
|
1856
|
+
return await page.evaluate((el) => {
|
|
1857
|
+
const path4 = [];
|
|
1858
|
+
let current = el;
|
|
1859
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
1860
|
+
let selector = current.nodeName.toLowerCase();
|
|
1861
|
+
if (current.id) {
|
|
1862
|
+
selector += `#${current.id}`;
|
|
1863
|
+
path4.unshift(selector);
|
|
1864
|
+
break;
|
|
1865
|
+
} else {
|
|
1866
|
+
let sibling = current;
|
|
1867
|
+
let nth = 1;
|
|
1868
|
+
while (sibling.previousElementSibling) {
|
|
1869
|
+
sibling = sibling.previousElementSibling;
|
|
1870
|
+
if (sibling.nodeName === current.nodeName) {
|
|
1871
|
+
nth++;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (nth > 1) {
|
|
1875
|
+
selector += `:nth-of-type(${nth})`;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
path4.unshift(selector);
|
|
1879
|
+
current = current.parentNode;
|
|
1880
|
+
}
|
|
1881
|
+
return path4.join(" > ");
|
|
1882
|
+
}, element);
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
// src/recorder/pattern-detector.ts
|
|
1887
|
+
init_cjs_shims();
|
|
1888
|
+
init_logger();
|
|
1889
|
+
var PatternDetector = class {
|
|
1890
|
+
analyze(actions) {
|
|
1891
|
+
const patterns = {};
|
|
1892
|
+
patterns.hasFormSubmission = this.detectFormSubmission(actions);
|
|
1893
|
+
patterns.hasPagination = this.detectPagination(actions);
|
|
1894
|
+
patterns.hasInfiniteScroll = this.detectInfiniteScroll(actions);
|
|
1895
|
+
patterns.hasDownloadFlow = this.detectDownloadFlow(actions);
|
|
1896
|
+
patterns.hasLogin = this.detectLogin(actions);
|
|
1897
|
+
patterns.hasSearch = this.detectSearch(actions);
|
|
1898
|
+
logger.info("[pattern-detector] Detected patterns:", patterns);
|
|
1899
|
+
return patterns;
|
|
1900
|
+
}
|
|
1901
|
+
detectFormSubmission(actions) {
|
|
1902
|
+
const typeActions = actions.filter((a) => a.type === "type");
|
|
1903
|
+
const clickActions = actions.filter((a) => a.type === "click");
|
|
1904
|
+
if (typeActions.length < 2) return null;
|
|
1905
|
+
const fields = typeActions.map((action) => ({
|
|
1906
|
+
selector: action.selectors.primary,
|
|
1907
|
+
type: action.fieldType || "text",
|
|
1908
|
+
placeholder: action.element?.placeholder || ""
|
|
1909
|
+
}));
|
|
1910
|
+
const submitButton = clickActions.find(
|
|
1911
|
+
(a) => a.element?.textContent?.toLowerCase().includes("submit") || a.element?.textContent?.toLowerCase().includes("login") || a.element?.type === "submit"
|
|
1912
|
+
);
|
|
1913
|
+
if (!submitButton) return null;
|
|
1914
|
+
return {
|
|
1915
|
+
type: "FORM_SUBMISSION",
|
|
1916
|
+
fields,
|
|
1917
|
+
submitButton: submitButton.selectors.primary
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
detectPagination(actions) {
|
|
1921
|
+
const clickActions = actions.filter((a) => a.type === "click");
|
|
1922
|
+
const nextClicks = clickActions.filter(
|
|
1923
|
+
(a) => a.element?.textContent?.toLowerCase().includes("next") || a.element?.className?.toLowerCase().includes("next") || a.element?.href?.includes("page")
|
|
1924
|
+
);
|
|
1925
|
+
if (nextClicks.length < 2) return null;
|
|
1926
|
+
const firstNext = nextClicks[0];
|
|
1927
|
+
const sameSelector = nextClicks.every(
|
|
1928
|
+
(a) => a.selectors.primary === firstNext.selectors.primary
|
|
1929
|
+
);
|
|
1930
|
+
if (sameSelector) {
|
|
1931
|
+
return {
|
|
1932
|
+
type: "PAGINATION",
|
|
1933
|
+
nextButton: firstNext.selectors.primary,
|
|
1934
|
+
timesClicked: nextClicks.length
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
detectInfiniteScroll(actions) {
|
|
1940
|
+
const scrollActions = actions.filter((a) => a.type === "scroll");
|
|
1941
|
+
if (scrollActions.length > 5) {
|
|
1942
|
+
return {
|
|
1943
|
+
type: "INFINITE_SCROLL",
|
|
1944
|
+
totalScrolls: scrollActions.length
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
detectDownloadFlow(actions) {
|
|
1950
|
+
return actions.some(
|
|
1951
|
+
(a) => a.element?.textContent?.toLowerCase().includes("download") || a.element?.href?.includes("download") || a.element?.href?.match(/\.(mp4|mp3|pdf|zip|rar)$/i)
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
detectLogin(actions) {
|
|
1955
|
+
const typeActions = actions.filter((a) => a.type === "type");
|
|
1956
|
+
const hasPassword = typeActions.some(
|
|
1957
|
+
(a) => a.fieldType === "password" || a.selectors.primary.includes("password")
|
|
1958
|
+
);
|
|
1959
|
+
const hasUsername = typeActions.some(
|
|
1960
|
+
(a) => a.fieldType === "email" || a.fieldType === "text" || a.selectors.primary.includes("email") || a.selectors.primary.includes("username")
|
|
1961
|
+
);
|
|
1962
|
+
return hasPassword && hasUsername;
|
|
1963
|
+
}
|
|
1964
|
+
detectSearch(actions) {
|
|
1965
|
+
return actions.some(
|
|
1966
|
+
(a) => a.type === "type" && (a.selectors.primary.includes("search") || a.element?.placeholder?.toLowerCase().includes("search"))
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
// src/recorder/cmd-generator.ts
|
|
1972
|
+
init_cjs_shims();
|
|
1973
|
+
var CmdGenerator = class {
|
|
1974
|
+
generate(url, actions, patterns) {
|
|
1975
|
+
const lines = [];
|
|
1976
|
+
lines.push(`GOTO ${url}`);
|
|
1977
|
+
lines.push(`WAITLOAD`);
|
|
1978
|
+
lines.push("");
|
|
1979
|
+
if (patterns.hasLogin) {
|
|
1980
|
+
lines.push("LOGIN DETECTED");
|
|
1981
|
+
}
|
|
1982
|
+
if (patterns.hasSearch) {
|
|
1983
|
+
lines.push("SEARCH DETECTED");
|
|
1984
|
+
}
|
|
1985
|
+
if (patterns.hasFormSubmission) {
|
|
1986
|
+
const form = patterns.hasFormSubmission;
|
|
1987
|
+
lines.push("");
|
|
1988
|
+
form.fields.forEach((field) => {
|
|
1989
|
+
lines.push(`WAIT ${field.selector}`);
|
|
1990
|
+
lines.push(`TYPE ${field.selector} YOUR_${field.type.toUpperCase()}_HERE`);
|
|
1991
|
+
});
|
|
1992
|
+
lines.push(`CLICK ${form.submitButton}`);
|
|
1993
|
+
lines.push("WAITLOAD");
|
|
1994
|
+
}
|
|
1995
|
+
const uniqueActions = this.deduplicateActions(actions);
|
|
1996
|
+
uniqueActions.forEach((action) => {
|
|
1997
|
+
if (action.type === "click") {
|
|
1998
|
+
lines.push(`CLICK ${action.selectors.primary}`);
|
|
1999
|
+
} else if (action.type === "type" && action.value) {
|
|
2000
|
+
lines.push(`TYPE ${action.selectors.primary} ${action.value}`);
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
if (patterns.hasPagination) {
|
|
2004
|
+
const pagination = patterns.hasPagination;
|
|
2005
|
+
lines.push("");
|
|
2006
|
+
lines.push(`PAGINATE ${pagination.nextButton}`);
|
|
2007
|
+
}
|
|
2008
|
+
if (patterns.hasInfiniteScroll) {
|
|
2009
|
+
lines.push("");
|
|
2010
|
+
lines.push("INFINITESCROLL");
|
|
2011
|
+
}
|
|
2012
|
+
if (patterns.hasDownloadFlow) {
|
|
2013
|
+
lines.push("");
|
|
2014
|
+
lines.push("DOWNLOAD DETECTED");
|
|
2015
|
+
}
|
|
2016
|
+
return lines.join("\n");
|
|
2017
|
+
}
|
|
2018
|
+
deduplicateActions(actions) {
|
|
2019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2020
|
+
const unique = [];
|
|
2021
|
+
for (const action of actions) {
|
|
2022
|
+
const key = `${action.type}:${action.selectors.primary}`;
|
|
2023
|
+
if (!seen.has(key)) {
|
|
2024
|
+
seen.add(key);
|
|
2025
|
+
unique.push(action);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return unique;
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
// src/recorder/recorder.ts
|
|
2033
|
+
init_logger();
|
|
2034
|
+
var import_fs3 = __toESM(require("fs"));
|
|
2035
|
+
var import_path3 = __toESM(require("path"));
|
|
2036
|
+
var ActionRecorder = class {
|
|
2037
|
+
browser = null;
|
|
2038
|
+
page = null;
|
|
2039
|
+
actions = [];
|
|
2040
|
+
isRecording = false;
|
|
2041
|
+
selectorGen;
|
|
2042
|
+
patternDetector;
|
|
2043
|
+
cmdGenerator;
|
|
2044
|
+
startUrl = "";
|
|
2045
|
+
constructor() {
|
|
2046
|
+
this.selectorGen = new SelectorGenerator();
|
|
2047
|
+
this.patternDetector = new PatternDetector();
|
|
2048
|
+
this.cmdGenerator = new CmdGenerator();
|
|
2049
|
+
}
|
|
2050
|
+
async startRecording(url) {
|
|
2051
|
+
this.startUrl = url;
|
|
2052
|
+
this.actions = [];
|
|
2053
|
+
this.isRecording = true;
|
|
2054
|
+
logger.info("[recorder] Starting recording session...");
|
|
2055
|
+
console.log("\n===========================================");
|
|
2056
|
+
console.log(" RECORDING MODE ACTIVATED");
|
|
2057
|
+
console.log(` URL: ${url}`);
|
|
2058
|
+
console.log(" Perform your actions in the browser...");
|
|
2059
|
+
console.log(" Close the browser when done");
|
|
2060
|
+
console.log("===========================================\n");
|
|
2061
|
+
this.browser = await import_playwright2.chromium.launch({ headless: false });
|
|
2062
|
+
const context = await this.browser.newContext();
|
|
2063
|
+
this.page = await context.newPage();
|
|
2064
|
+
await this.attachListeners(this.page);
|
|
2065
|
+
await this.page.goto(url);
|
|
2066
|
+
await this.page.waitForEvent("close");
|
|
2067
|
+
await this.stopRecording();
|
|
2068
|
+
}
|
|
2069
|
+
async attachListeners(page) {
|
|
2070
|
+
await page.exposeFunction("__recordClick", async (x, y) => {
|
|
2071
|
+
if (!this.isRecording) return;
|
|
2072
|
+
const element = await page.evaluate((coords) => {
|
|
2073
|
+
const el = document.elementFromPoint(coords.x, coords.y);
|
|
2074
|
+
if (!el) return null;
|
|
2075
|
+
return {
|
|
2076
|
+
tagName: el.tagName.toLowerCase(),
|
|
2077
|
+
id: el.id,
|
|
2078
|
+
className: el.className,
|
|
2079
|
+
textContent: el.textContent?.slice(0, 50),
|
|
2080
|
+
href: el.href,
|
|
2081
|
+
type: el.type
|
|
2082
|
+
};
|
|
2083
|
+
}, { x, y });
|
|
2084
|
+
if (element) {
|
|
2085
|
+
const selectors = await this.selectorGen.generate(page, element);
|
|
2086
|
+
this.actions.push({
|
|
2087
|
+
type: "click",
|
|
2088
|
+
selectors,
|
|
2089
|
+
timestamp: Date.now(),
|
|
2090
|
+
element
|
|
2091
|
+
});
|
|
2092
|
+
logger.info(`[recorder] Recorded CLICK on ${selectors.primary}`);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
await page.exposeFunction("__recordType", async (selector, value) => {
|
|
2096
|
+
if (!this.isRecording) return;
|
|
2097
|
+
const element = await page.evaluate((sel) => {
|
|
2098
|
+
const el = document.querySelector(sel);
|
|
2099
|
+
if (!el) return null;
|
|
2100
|
+
return {
|
|
2101
|
+
tagName: el.tagName.toLowerCase(),
|
|
2102
|
+
id: el.id,
|
|
2103
|
+
type: el.type,
|
|
2104
|
+
placeholder: el.placeholder
|
|
2105
|
+
};
|
|
2106
|
+
}, selector);
|
|
2107
|
+
if (element) {
|
|
2108
|
+
const selectors = await this.selectorGen.generate(page, element);
|
|
2109
|
+
this.actions.push({
|
|
2110
|
+
type: "type",
|
|
2111
|
+
selectors,
|
|
2112
|
+
value,
|
|
2113
|
+
timestamp: Date.now(),
|
|
2114
|
+
element,
|
|
2115
|
+
fieldType: element.type
|
|
2116
|
+
});
|
|
2117
|
+
logger.info(`[recorder] Recorded TYPE in ${selectors.primary}: "${value.slice(0, 20)}..."`);
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
await page.addInitScript(() => {
|
|
2121
|
+
document.addEventListener("click", (e) => {
|
|
2122
|
+
const target = e.target;
|
|
2123
|
+
if (target) {
|
|
2124
|
+
window.__recordClick(e.clientX, e.clientY);
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
document.addEventListener("input", (e) => {
|
|
2128
|
+
const target = e.target;
|
|
2129
|
+
if (target && target.tagName === "INPUT") {
|
|
2130
|
+
setTimeout(() => {
|
|
2131
|
+
window.__recordType(target.id || target.name, target.value);
|
|
2132
|
+
}, 500);
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
async stopRecording() {
|
|
2138
|
+
this.isRecording = false;
|
|
2139
|
+
logger.info(`[recorder] Recording stopped - ${this.actions.length} actions captured`);
|
|
2140
|
+
const patterns = this.patternDetector.analyze(this.actions);
|
|
2141
|
+
const cmdFile = this.cmdGenerator.generate(this.startUrl, this.actions, patterns);
|
|
2142
|
+
const outputDir = "./commands";
|
|
2143
|
+
if (!import_fs3.default.existsSync(outputDir)) {
|
|
2144
|
+
import_fs3.default.mkdirSync(outputDir, { recursive: true });
|
|
2145
|
+
}
|
|
2146
|
+
const filename = `recorded-${Date.now()}.cmd`;
|
|
2147
|
+
const filepath = import_path3.default.join(outputDir, filename);
|
|
2148
|
+
import_fs3.default.writeFileSync(filepath, cmdFile);
|
|
2149
|
+
console.log("\n===========================================");
|
|
2150
|
+
console.log(" RECORDING COMPLETE");
|
|
2151
|
+
console.log(` Saved to: ${filepath}`);
|
|
2152
|
+
console.log(` Actions: ${this.actions.length}`);
|
|
2153
|
+
console.log("===========================================\n");
|
|
2154
|
+
if (this.browser) {
|
|
2155
|
+
await this.browser.close();
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
getActions() {
|
|
2159
|
+
return this.actions;
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
// bin/firekid-scraper.ts
|
|
2164
|
+
init_logger();
|
|
2165
|
+
var import_fs4 = require("fs");
|
|
2166
|
+
var import_path4 = require("path");
|
|
2167
|
+
var packageJson = JSON.parse(
|
|
2168
|
+
(0, import_fs4.readFileSync)((0, import_path4.join)(__dirname, "../../package.json"), "utf-8")
|
|
2169
|
+
);
|
|
2170
|
+
var program = new import_commander.Command();
|
|
2171
|
+
program.name("firekid-scraper").description("The most advanced web scraping machine ever built").version(packageJson.version);
|
|
2172
|
+
program.option("-u, --url <url>", "URL to scrape").option("-m, --mode <mode>", "Scraping mode (auto, downloader, scrape, navigator)").option("--cmd <file>", "Run command file").option("--record", "Record browser actions").option("--auto", "Use intelligent auto mode").option("--headless", "Run in headless mode").option("--server", "Start API server").option("-p, --port <port>", "API server port", "3000");
|
|
2173
|
+
program.parse();
|
|
2174
|
+
var options = program.opts();
|
|
2175
|
+
async function main() {
|
|
2176
|
+
(0, import_prompts.intro)("Firekid Scraper");
|
|
2177
|
+
if (options.server) {
|
|
2178
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_app(), app_exports));
|
|
2179
|
+
await startServer2(parseInt(options.port, 10));
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const scraper = new FirekidScraper({
|
|
2183
|
+
headless: options.headless !== false
|
|
2184
|
+
});
|
|
2185
|
+
if (options.record) {
|
|
2186
|
+
const url = options.url || await (0, import_prompts.text)({
|
|
2187
|
+
message: "Enter URL to record:",
|
|
2188
|
+
placeholder: "https://example.com"
|
|
2189
|
+
});
|
|
2190
|
+
const s = (0, import_prompts.spinner)();
|
|
2191
|
+
s.start("Starting recorder...");
|
|
2192
|
+
const recorder = new ActionRecorder();
|
|
2193
|
+
await recorder.startRecording(url);
|
|
2194
|
+
s.stop("Recording complete!");
|
|
2195
|
+
(0, import_prompts.outro)("Command file generated");
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (options.cmd) {
|
|
2199
|
+
const s = (0, import_prompts.spinner)();
|
|
2200
|
+
s.start(`Running command file: ${options.cmd}`);
|
|
2201
|
+
await scraper.runCommandFile(options.cmd);
|
|
2202
|
+
s.stop("Execution complete!");
|
|
2203
|
+
(0, import_prompts.outro)("Done");
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (options.auto || options.url) {
|
|
2207
|
+
const url = options.url || await (0, import_prompts.text)({
|
|
2208
|
+
message: "Enter URL to scrape:",
|
|
2209
|
+
placeholder: "https://example.com"
|
|
2210
|
+
});
|
|
2211
|
+
const s = (0, import_prompts.spinner)();
|
|
2212
|
+
s.start("Scraping...");
|
|
2213
|
+
const result = await scraper.auto(url);
|
|
2214
|
+
s.stop("Scraping complete!");
|
|
2215
|
+
console.log("\nResults:", result);
|
|
2216
|
+
(0, import_prompts.outro)("Done");
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const action = await (0, import_prompts.select)({
|
|
2220
|
+
message: "What would you like to do?",
|
|
2221
|
+
options: [
|
|
2222
|
+
{ value: "auto", label: "Auto scrape a URL" },
|
|
2223
|
+
{ value: "record", label: "Record browser actions" },
|
|
2224
|
+
{ value: "command", label: "Run command file" },
|
|
2225
|
+
{ value: "server", label: "Start API server" }
|
|
2226
|
+
]
|
|
2227
|
+
});
|
|
2228
|
+
if (action === "auto") {
|
|
2229
|
+
const url = await (0, import_prompts.text)({
|
|
2230
|
+
message: "Enter URL:",
|
|
2231
|
+
placeholder: "https://example.com"
|
|
2232
|
+
});
|
|
2233
|
+
const s = (0, import_prompts.spinner)();
|
|
2234
|
+
s.start("Scraping...");
|
|
2235
|
+
const result = await scraper.auto(url);
|
|
2236
|
+
s.stop("Complete!");
|
|
2237
|
+
console.log("\nResults:", result);
|
|
2238
|
+
} else if (action === "record") {
|
|
2239
|
+
const url = await (0, import_prompts.text)({
|
|
2240
|
+
message: "Enter URL to record:",
|
|
2241
|
+
placeholder: "https://example.com"
|
|
2242
|
+
});
|
|
2243
|
+
const recorder = new ActionRecorder();
|
|
2244
|
+
await recorder.startRecording(url);
|
|
2245
|
+
} else if (action === "command") {
|
|
2246
|
+
const file = await (0, import_prompts.text)({
|
|
2247
|
+
message: "Enter command file path:",
|
|
2248
|
+
placeholder: "commands/mysite.cmd"
|
|
2249
|
+
});
|
|
2250
|
+
await scraper.runCommandFile(file);
|
|
2251
|
+
} else if (action === "server") {
|
|
2252
|
+
const port = await (0, import_prompts.text)({
|
|
2253
|
+
message: "Enter port:",
|
|
2254
|
+
placeholder: "3000"
|
|
2255
|
+
});
|
|
2256
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_app(), app_exports));
|
|
2257
|
+
await startServer2(parseInt(port, 10));
|
|
2258
|
+
}
|
|
2259
|
+
(0, import_prompts.outro)("Done");
|
|
2260
|
+
}
|
|
2261
|
+
main().catch((err) => {
|
|
2262
|
+
logger.error("CLI error:", err);
|
|
2263
|
+
process.exit(1);
|
|
2264
|
+
});
|