sfiledl 1.0.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.
@@ -0,0 +1,812 @@
1
+ #!/usr/bin/env node
2
+ import { chromium } from 'playwright';
3
+ import { promises } from 'fs';
4
+ import { join, basename } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { exit } from 'process';
7
+
8
+ const isOk = (result) => result.success;
9
+ const isErr = (result) => !result.success;
10
+ const safeStringify = (value, space) => {
11
+ try {
12
+ return JSON.stringify(value, null, space);
13
+ }
14
+ catch {
15
+ return '[Circular or unserializable]';
16
+ }
17
+ };
18
+ const sanitizeFilename = (name, replacement = '_') => {
19
+ const sanitized = name
20
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, replacement)
21
+ .replace(new RegExp(`${replacement}+`, 'g'), replacement)
22
+ .trim()
23
+ .slice(0, 255);
24
+ return sanitized || 'file.bin';
25
+ };
26
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
+ class AppError extends Error {
28
+ code;
29
+ retryable;
30
+ timestamp;
31
+ context;
32
+ constructor(message, code, retryable = true, context) {
33
+ super(message);
34
+ this.code = code;
35
+ this.retryable = retryable;
36
+ this.name = this.constructor.name;
37
+ this.timestamp = new Date().toISOString();
38
+ this.context = context !== undefined ? Object.freeze({ ...context }) : undefined;
39
+ Error.captureStackTrace?.(this, this.constructor);
40
+ }
41
+ toJSON() {
42
+ return {
43
+ name: this.name,
44
+ message: this.message,
45
+ code: this.code,
46
+ retryable: this.retryable,
47
+ timestamp: this.timestamp,
48
+ context: this.context,
49
+ stack: process.env['NODE_ENV'] === 'development' ? this.stack : undefined,
50
+ };
51
+ }
52
+ }
53
+ class ValidationError extends AppError {
54
+ constructor(message, context) {
55
+ super(message, 'VALIDATION_ERROR', false, context);
56
+ }
57
+ }
58
+ class NetworkError extends AppError {
59
+ constructor(message, context) {
60
+ super(message, 'NETWORK_ERROR', true, context);
61
+ }
62
+ }
63
+ class FileError extends AppError {
64
+ constructor(message, context) {
65
+ super(message, 'FILE_ERROR', false, context);
66
+ }
67
+ }
68
+ class BrowserError extends AppError {
69
+ constructor(message, context) {
70
+ super(message, 'BROWSER_ERROR', true, context);
71
+ }
72
+ }
73
+ class Logger {
74
+ static LEVELS = Object.freeze({
75
+ DEBUG: 0,
76
+ INFO: 1,
77
+ WARN: 2,
78
+ ERROR: 3,
79
+ });
80
+ minLevel;
81
+ entries = [];
82
+ maxEntries = 1000;
83
+ logFile;
84
+ constructor(minLevel = 'INFO') {
85
+ this.minLevel = minLevel;
86
+ }
87
+ setLogLevel(level) {
88
+ this.minLevel = level;
89
+ }
90
+ useLogFile(path) {
91
+ this.logFile = path;
92
+ }
93
+ shouldLog(level) {
94
+ return Logger.LEVELS[level] >= Logger.LEVELS[this.minLevel];
95
+ }
96
+ format(entry) {
97
+ const ctx = entry.context !== undefined ? ` ${safeStringify(entry.context)}` : '';
98
+ return `[${entry.timestamp}] ${entry.level}: ${entry.message}${ctx}`;
99
+ }
100
+ record(entry) {
101
+ this.entries.push(entry);
102
+ if (this.entries.length > this.maxEntries) {
103
+ this.entries.shift();
104
+ }
105
+ }
106
+ output(entry) {
107
+ const formatted = this.format(entry);
108
+ const outputFn = entry.level === 'ERROR' ? console.error : console.log;
109
+ outputFn(formatted);
110
+ if (this.logFile !== undefined) {
111
+ void promises.appendFile(this.logFile, `${formatted}\n`).catch(() => { });
112
+ }
113
+ }
114
+ log(level, message, context) {
115
+ if (!this.shouldLog(level))
116
+ return;
117
+ const entry = {
118
+ timestamp: new Date().toISOString(),
119
+ level,
120
+ message,
121
+ ...(context !== undefined
122
+ ? { context: Object.freeze({ ...context }) }
123
+ : {}),
124
+ };
125
+ this.record(entry);
126
+ this.output(entry);
127
+ }
128
+ debug(message, context) {
129
+ this.log('DEBUG', message, context);
130
+ }
131
+ info(message, context) {
132
+ this.log('INFO', message, context);
133
+ }
134
+ warn(message, context) {
135
+ this.log('WARN', message, context);
136
+ }
137
+ error(message, context) {
138
+ this.log('ERROR', message, context);
139
+ }
140
+ getEntries(level) {
141
+ if (level === undefined)
142
+ return [...this.entries];
143
+ return this.entries.filter((e) => e.level === level);
144
+ }
145
+ }
146
+ class CliFlagsBuilder {
147
+ help = false;
148
+ debug = false;
149
+ headless = true;
150
+ proxy;
151
+ logFile;
152
+ json = false;
153
+ batch;
154
+ concurrency = 1;
155
+ retry = 3;
156
+ timeout = 60000;
157
+ build() {
158
+ const flags = {
159
+ help: this.help,
160
+ debug: this.debug,
161
+ headless: this.headless,
162
+ json: this.json,
163
+ concurrency: this.concurrency,
164
+ retry: this.retry,
165
+ timeout: this.timeout,
166
+ ...(this.proxy !== undefined && { proxy: this.proxy }),
167
+ ...(this.logFile !== undefined && { logFile: this.logFile }),
168
+ ...(this.batch !== undefined && { batch: this.batch }),
169
+ };
170
+ return Object.freeze(flags);
171
+ }
172
+ }
173
+ class CliParser {
174
+ static parse(argv) {
175
+ const args = argv.slice(2);
176
+ const flags = new CliFlagsBuilder();
177
+ const positional = [];
178
+ const errors = [];
179
+ for (let i = 0; i < args.length; i++) {
180
+ const arg = args[i];
181
+ if (arg === undefined || arg === null)
182
+ continue;
183
+ if (arg === '--') {
184
+ positional.push(...args.slice(i + 1).filter((a) => a !== undefined && a !== null));
185
+ break;
186
+ }
187
+ if (arg.startsWith('--')) {
188
+ const [key, ...valueParts] = arg.slice(2).split('=');
189
+ if (key === undefined || key === '')
190
+ continue;
191
+ let value;
192
+ if (valueParts.length > 0) {
193
+ value = valueParts.join('=');
194
+ }
195
+ else if (i + 1 < args.length) {
196
+ const nextArg = args[i + 1];
197
+ if (nextArg !== undefined &&
198
+ typeof nextArg === 'string' &&
199
+ !nextArg.startsWith('-')) {
200
+ value = nextArg;
201
+ i++;
202
+ }
203
+ else {
204
+ value = 'true';
205
+ }
206
+ }
207
+ else {
208
+ value = 'true';
209
+ }
210
+ CliParser.setFlag(flags, key, value ?? 'true', errors);
211
+ }
212
+ else if (typeof arg === 'string' && arg.startsWith('-') && arg.length === 2) {
213
+ const key = arg[1];
214
+ if (key === undefined || key === '')
215
+ continue;
216
+ const next = args[i + 1];
217
+ const value = next !== undefined &&
218
+ typeof next === 'string' &&
219
+ next !== '-' &&
220
+ !next.startsWith('-')
221
+ ? args[++i]
222
+ : 'true';
223
+ CliParser.setFlag(flags, key, value ?? 'true', errors);
224
+ }
225
+ else if (typeof arg === 'string' && arg.startsWith('-') && arg.length > 2) {
226
+ for (let j = 1; j < arg.length; j++) {
227
+ const char = arg[j];
228
+ if (char !== undefined && char !== '') {
229
+ CliParser.setFlag(flags, char, 'true', errors);
230
+ }
231
+ }
232
+ }
233
+ else if (typeof arg === 'string') {
234
+ positional.push(arg);
235
+ }
236
+ }
237
+ const url = positional[0];
238
+ const saveDir = positional[1];
239
+ if (flags.concurrency < 1 || flags.concurrency > 20) {
240
+ errors.push('--concurrency must be 1-20');
241
+ }
242
+ if (flags.retry < 1 || flags.retry > 10) {
243
+ errors.push('--retry must be 1-10');
244
+ }
245
+ if (flags.timeout < 1000) {
246
+ errors.push('--timeout must be >= 1000ms');
247
+ }
248
+ return {
249
+ url,
250
+ saveDir,
251
+ flags: flags.build(),
252
+ errors: Object.freeze(errors),
253
+ };
254
+ }
255
+ static setFlag(flags, key, value, errors) {
256
+ const normalized = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
257
+ switch (normalized) {
258
+ case 'help':
259
+ case 'h':
260
+ flags.help = value === 'true';
261
+ break;
262
+ case 'debug':
263
+ flags.debug = value === 'true';
264
+ break;
265
+ case 'headless':
266
+ flags.headless = value !== 'false';
267
+ break;
268
+ case 'proxy':
269
+ flags.proxy = value;
270
+ break;
271
+ case 'logfile':
272
+ case 'log-file':
273
+ flags.logFile = value;
274
+ break;
275
+ case 'json':
276
+ flags.json = value === 'true';
277
+ break;
278
+ case 'batch':
279
+ flags.batch = value;
280
+ break;
281
+ case 'concurrency': {
282
+ const num = Number(value);
283
+ if (Number.isFinite(num))
284
+ flags.concurrency = num;
285
+ else
286
+ errors.push(`Invalid concurrency: ${value}`);
287
+ break;
288
+ }
289
+ case 'retry': {
290
+ const num = Number(value);
291
+ if (Number.isFinite(num))
292
+ flags.retry = num;
293
+ else
294
+ errors.push(`Invalid retry: ${value}`);
295
+ break;
296
+ }
297
+ case 'timeout': {
298
+ const num = Number(value);
299
+ if (Number.isFinite(num))
300
+ flags.timeout = num;
301
+ else
302
+ errors.push(`Invalid timeout: ${value}`);
303
+ break;
304
+ }
305
+ default:
306
+ errors.push(`Unknown flag: --${key}`);
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ class BrowserManager {
312
+ browser = null;
313
+ context = null;
314
+ page = null;
315
+ logger;
316
+ config;
317
+ constructor(logger, config) {
318
+ this.logger = logger;
319
+ const maybeViewport = config.viewport;
320
+ const maybeProxy = config.proxy;
321
+ const browserConfig = {
322
+ headless: config.headless ?? true,
323
+ userAgent: config.userAgent ??
324
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
325
+ acceptDownloads: true,
326
+ ...(maybeViewport !== undefined && { viewport: maybeViewport }),
327
+ ...(maybeProxy !== undefined && { proxy: maybeProxy }),
328
+ };
329
+ this.config = Object.freeze(browserConfig);
330
+ }
331
+ async launch() {
332
+ try {
333
+ this.logger.debug('Launching browser', { headless: this.config.headless });
334
+ const launchOptions = {
335
+ headless: this.config.headless,
336
+ args: [
337
+ '--no-sandbox',
338
+ '--disable-setuid-sandbox',
339
+ '--disable-dev-shm-usage',
340
+ '--disable-gpu',
341
+ '--disable-blink-features=AutomationControlled',
342
+ ],
343
+ };
344
+ if (this.config.proxy !== undefined) {
345
+ const proxyOpts = {
346
+ server: this.config.proxy.server,
347
+ };
348
+ if (this.config.proxy.username !== undefined) {
349
+ proxyOpts.username = this.config.proxy.username;
350
+ }
351
+ if (this.config.proxy.password !== undefined) {
352
+ proxyOpts.password = this.config.proxy.password;
353
+ }
354
+ launchOptions.proxy = proxyOpts;
355
+ }
356
+ this.browser = await chromium.launch(launchOptions);
357
+ const contextOptions = {
358
+ userAgent: this.config.userAgent,
359
+ acceptDownloads: this.config.acceptDownloads,
360
+ locale: 'en-US',
361
+ timezoneId: 'UTC',
362
+ };
363
+ if (this.config.viewport !== undefined) {
364
+ contextOptions.viewport = this.config.viewport;
365
+ }
366
+ this.context = await this.browser.newContext(contextOptions);
367
+ await this.context.addCookies([
368
+ {
369
+ name: 'safe_link_counter',
370
+ value: '1',
371
+ domain: '.sfile.co',
372
+ path: '/',
373
+ expires: Math.floor(Date.now() / 1000) + 3600,
374
+ },
375
+ ]);
376
+ this.page = await this.context.newPage();
377
+ this.setupListeners();
378
+ this.logger.info('Browser launched successfully');
379
+ return { success: true, value: undefined };
380
+ }
381
+ catch (err) {
382
+ const error = err instanceof Error ? err : new Error(String(err));
383
+ this.logger.error('Browser launch failed', { error: error.message });
384
+ await this.cleanup();
385
+ return {
386
+ success: false,
387
+ error: new BrowserError(`Launch failed: ${error.message}`, {
388
+ original: error.name,
389
+ }),
390
+ };
391
+ }
392
+ }
393
+ setupListeners() {
394
+ if (this.page === null)
395
+ return;
396
+ this.page.on('console', (msg) => {
397
+ const type = msg.type();
398
+ if (type === 'error' || type === 'warning') {
399
+ const level = type === 'error' ? 'ERROR' : 'WARN';
400
+ this.logger.log(level, `[CONSOLE] ${msg.text()}`, { location: msg.location().url });
401
+ }
402
+ });
403
+ this.page.on('pageerror', (err) => {
404
+ this.logger.error('[PAGE ERROR]', { message: err.message });
405
+ });
406
+ }
407
+ async navigate(url, options) {
408
+ if (this.page === null) {
409
+ return { success: false, error: new NetworkError('Page not initialized') };
410
+ }
411
+ try {
412
+ await this.page.goto(url, {
413
+ waitUntil: options?.waitUntil ?? 'networkidle',
414
+ timeout: options?.timeout ?? 60000,
415
+ });
416
+ return { success: true, value: undefined };
417
+ }
418
+ catch (err) {
419
+ const error = err instanceof Error ? err : new Error(String(err));
420
+ return {
421
+ success: false,
422
+ error: new NetworkError(`Navigation failed: ${error.message}`),
423
+ };
424
+ }
425
+ }
426
+ async waitForDownloadButton(timeout = 30000) {
427
+ if (this.page === null) {
428
+ return { success: false, error: new Error('Page not initialized') };
429
+ }
430
+ try {
431
+ const button = this.page.locator('#download');
432
+ await button.waitFor({ state: 'visible', timeout });
433
+ await this.page.waitForFunction(() => {
434
+ const btn = document.querySelector('#download');
435
+ if (btn === null)
436
+ return false;
437
+ const href = btn.getAttribute('href');
438
+ const style = window.getComputedStyle(btn);
439
+ return href !== null && href !== '#' && style.pointerEvents !== 'none';
440
+ }, { timeout });
441
+ return { success: true, value: undefined };
442
+ }
443
+ catch (err) {
444
+ const error = err instanceof Error ? err : new Error(String(err));
445
+ return { success: false, error };
446
+ }
447
+ }
448
+ async extractDownloadUrl() {
449
+ if (this.page === null) {
450
+ return { success: false, error: new Error('Page not initialized') };
451
+ }
452
+ try {
453
+ const href = await this.page.$eval('#download', (el) => {
454
+ const anchor = el;
455
+ return anchor.href;
456
+ });
457
+ if (href === null || href === '' || href === '#') {
458
+ return { success: false, error: new Error('Invalid download href') };
459
+ }
460
+ return { success: true, value: href };
461
+ }
462
+ catch (err) {
463
+ const error = err instanceof Error ? err : new Error(String(err));
464
+ return { success: false, error };
465
+ }
466
+ }
467
+ async waitForDownload(timeout = 60000) {
468
+ if (this.page === null) {
469
+ return { success: false, error: new Error('Page not initialized') };
470
+ }
471
+ try {
472
+ const download = await this.page.waitForEvent('download', { timeout });
473
+ return { success: true, value: download };
474
+ }
475
+ catch (err) {
476
+ const error = err instanceof Error ? err : new Error(String(err));
477
+ return { success: false, error };
478
+ }
479
+ }
480
+ async collectResponses(waitMs = 3000) {
481
+ if (this.page === null)
482
+ return [];
483
+ const responses = [];
484
+ const handler = (res) => {
485
+ responses.push(res);
486
+ };
487
+ this.page.on('response', handler);
488
+ await sleep(waitMs);
489
+ this.page.off('response', handler);
490
+ return Object.freeze(responses);
491
+ }
492
+ findFileResponse(responses) {
493
+ return responses
494
+ .slice()
495
+ .reverse()
496
+ .find((r) => {
497
+ const headers = r.headers();
498
+ const disposition = headers['content-disposition'];
499
+ return ((disposition !== undefined && disposition.includes('attachment')) ||
500
+ r.url().includes('/downloadfile/'));
501
+ });
502
+ }
503
+ async saveDebugArtifacts(errorMessage) {
504
+ if (this.page === null) {
505
+ return { success: false, error: new Error('Page not available') };
506
+ }
507
+ try {
508
+ const debugDir = join(tmpdir(), `sfile_debug_${Date.now()}`);
509
+ await promises.mkdir(debugDir, { recursive: true });
510
+ await Promise.all([
511
+ this.page.screenshot({ path: join(debugDir, 'error.png'), fullPage: true }),
512
+ promises.writeFile(join(debugDir, 'error.html'), await this.page.content()),
513
+ promises.writeFile(join(debugDir, 'error.txt'), errorMessage),
514
+ ]);
515
+ return { success: true, value: debugDir };
516
+ }
517
+ catch (err) {
518
+ const error = err instanceof Error ? err : new Error(String(err));
519
+ return {
520
+ success: false,
521
+ error: new FileError(`Failed to save artifacts: ${error.message}`),
522
+ };
523
+ }
524
+ }
525
+ async cleanup() {
526
+ if (this.page !== null) {
527
+ await this.page.close().catch(() => { });
528
+ this.page = null;
529
+ }
530
+ if (this.context !== null) {
531
+ await this.context.close().catch(() => { });
532
+ this.context = null;
533
+ }
534
+ if (this.browser !== null) {
535
+ await this.browser.close().catch(() => { });
536
+ this.browser = null;
537
+ }
538
+ }
539
+ async [Symbol.asyncDispose]() {
540
+ this.logger.debug('Closing browser resources...');
541
+ await this.cleanup();
542
+ this.logger.info('Browser resources closed');
543
+ }
544
+ [Symbol.dispose]() {
545
+ void this[Symbol.asyncDispose]();
546
+ }
547
+ }
548
+ class Downloader {
549
+ logger;
550
+ config;
551
+ constructor(logger, config) {
552
+ this.logger = logger;
553
+ this.config = Object.freeze(config);
554
+ }
555
+ async download(url) {
556
+ if (!url.includes('sfile.co')) {
557
+ return { success: false, error: new ValidationError('Invalid sfile.co URL', { url }) };
558
+ }
559
+ try {
560
+ await promises.mkdir(this.config.saveDir, { recursive: true });
561
+ }
562
+ catch (err) {
563
+ const error = err instanceof Error ? err : new Error(String(err));
564
+ return {
565
+ success: false,
566
+ error: new FileError(`Failed to create save dir: ${error.message}`),
567
+ };
568
+ }
569
+ let lastError;
570
+ for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
571
+ this.logger.info(`Download attempt ${attempt}/${this.config.retryAttempts}`, { url });
572
+ const result = await this.executeDownload(url);
573
+ if (isOk(result)) {
574
+ return result;
575
+ }
576
+ lastError = result.error;
577
+ if (!lastError.retryable || attempt === this.config.retryAttempts) {
578
+ break;
579
+ }
580
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
581
+ this.logger.info(`Retrying in ${delay}ms...`);
582
+ await sleep(delay);
583
+ }
584
+ if (lastError === undefined) {
585
+ return { success: false, error: new Error('Unknown error') };
586
+ }
587
+ return { success: false, error: lastError };
588
+ }
589
+ async executeDownload(url) {
590
+ const browserMgr = new BrowserManager(this.logger, {
591
+ headless: true,
592
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
593
+ });
594
+ try {
595
+ const launchResult = await browserMgr.launch();
596
+ if (isErr(launchResult)) {
597
+ return { success: false, error: launchResult.error };
598
+ }
599
+ const navResult = await browserMgr.navigate(url);
600
+ if (isErr(navResult)) {
601
+ return { success: false, error: navResult.error };
602
+ }
603
+ const buttonResult = await browserMgr.waitForDownloadButton();
604
+ if (isErr(buttonResult)) {
605
+ return {
606
+ success: false,
607
+ error: new NetworkError(`Button wait failed: ${buttonResult.error.message}`),
608
+ };
609
+ }
610
+ const urlResult = await browserMgr.extractDownloadUrl();
611
+ if (isErr(urlResult)) {
612
+ return {
613
+ success: false,
614
+ error: new NetworkError(`URL extract failed: ${urlResult.error.message}`),
615
+ };
616
+ }
617
+ const intermediateUrl = urlResult.value;
618
+ const autoUrl = intermediateUrl.includes('?')
619
+ ? `${intermediateUrl}&auto=1`
620
+ : `${intermediateUrl}?auto=1`;
621
+ this.logger.debug('Auto URL', { url: autoUrl });
622
+ const downloadPromise = browserMgr.waitForDownload(this.config.timeout);
623
+ const autoNavResult = await browserMgr.navigate(autoUrl, {
624
+ waitUntil: 'commit',
625
+ timeout: this.config.timeout,
626
+ });
627
+ if (isErr(autoNavResult)) {
628
+ return { success: false, error: autoNavResult.error };
629
+ }
630
+ const downloadResult = await downloadPromise;
631
+ if (isOk(downloadResult)) {
632
+ return await this.handleDirectDownload(downloadResult.value, this.config.saveDir);
633
+ }
634
+ else {
635
+ this.logger.warn('Download event failed, trying fallback');
636
+ const responses = await browserMgr.collectResponses(3000);
637
+ const fileResponse = browserMgr.findFileResponse(responses);
638
+ if (fileResponse === undefined) {
639
+ return {
640
+ success: false,
641
+ error: new NetworkError('No file response found in fallback'),
642
+ };
643
+ }
644
+ return await this.handleFallbackDownload(fileResponse, this.config.saveDir);
645
+ }
646
+ }
647
+ finally {
648
+ await browserMgr[Symbol.asyncDispose]();
649
+ }
650
+ }
651
+ async handleDirectDownload(download, saveDir) {
652
+ try {
653
+ const suggested = download.suggestedFilename();
654
+ const filename = sanitizeFilename(suggested !== undefined ? suggested : 'file.bin');
655
+ const savePath = join(saveDir, filename);
656
+ this.logger.info('Saving via direct download', { filename });
657
+ await download.saveAs(savePath);
658
+ const stats = await promises.stat(savePath);
659
+ return {
660
+ success: true,
661
+ value: {
662
+ filename,
663
+ savePath,
664
+ size: stats.size,
665
+ method: 'direct',
666
+ },
667
+ };
668
+ }
669
+ catch (err) {
670
+ const error = err instanceof Error ? err : new Error(String(err));
671
+ return {
672
+ success: false,
673
+ error: new FileError(`Direct download failed: ${error.message}`),
674
+ };
675
+ }
676
+ }
677
+ async handleFallbackDownload(response, saveDir) {
678
+ try {
679
+ const buffer = await response.body();
680
+ const urlPath = response.url().split('?')[0] ?? 'unknown';
681
+ const base = basename(urlPath);
682
+ const filename = sanitizeFilename(base !== '' ? base : 'file.bin');
683
+ const savePath = join(saveDir, filename);
684
+ await promises.writeFile(savePath, buffer);
685
+ this.logger.info('Saved via fallback', { filename, size: buffer.length });
686
+ return {
687
+ success: true,
688
+ value: {
689
+ filename,
690
+ savePath,
691
+ size: buffer.length,
692
+ method: 'fallback',
693
+ },
694
+ };
695
+ }
696
+ catch (err) {
697
+ const error = err instanceof Error ? err : new Error(String(err));
698
+ return {
699
+ success: false,
700
+ error: new FileError(`Fallback download failed: ${error.message}`),
701
+ };
702
+ }
703
+ }
704
+ }
705
+ class App {
706
+ logger;
707
+ constructor() {
708
+ this.logger = new Logger('INFO');
709
+ }
710
+ async run() {
711
+ const args = CliParser.parse(process.argv);
712
+ if (args.errors.length > 0) {
713
+ for (const err of args.errors) {
714
+ this.logger.error(`CLI error: ${err}`);
715
+ }
716
+ this.showHelp();
717
+ return 2;
718
+ }
719
+ if (args.flags.help || args.url === undefined) {
720
+ this.showHelp();
721
+ return 0;
722
+ }
723
+ this.logger.info('Starting Sfile Downloader', {
724
+ url: args.url,
725
+ saveDir: args.saveDir ?? process.cwd(),
726
+ headless: args.flags.headless,
727
+ });
728
+ const downloader = new Downloader(this.logger, {
729
+ saveDir: args.saveDir ?? process.cwd(),
730
+ timeout: args.flags.timeout,
731
+ retryAttempts: args.flags.retry,
732
+ });
733
+ const result = await downloader.download(args.url);
734
+ if (isOk(result)) {
735
+ this.logger.info('✅ Download complete', {
736
+ file: result.value.filename,
737
+ size: `${Math.round(result.value.size / 1024)} KB`,
738
+ method: result.value.method,
739
+ });
740
+ console.log(`Saved: ${result.value.savePath}`);
741
+ return 0;
742
+ }
743
+ else {
744
+ this.logger.error('❌ Download failed', {
745
+ error: result.error.message,
746
+ code: result.error.code,
747
+ retryable: result.error.retryable,
748
+ });
749
+ return 1;
750
+ }
751
+ }
752
+ showHelp() {
753
+ console.log(`
754
+ 🚀 Sfile Downloader - Strict TypeScript CLI
755
+ Usage:
756
+ bun run src/index.ts <url> [saveDir] [options]
757
+ Arguments:
758
+ url sfile.co URL to download (required)
759
+ saveDir Directory to save files (default: current directory)
760
+ Options:
761
+ --help, -h Show this help message
762
+ --debug Enable debug logging
763
+ --headless=BOOL Run browser headless (default: true)
764
+ --proxy=URL Proxy server URL
765
+ --log-file=PATH Write logs to file
766
+ --json Output logs as JSON
767
+ --batch=FILE Download URLs from file (one per line)
768
+ --concurrency=N Parallel downloads (1-20, default: 1)
769
+ --retry=N Max retry attempts (1-10, default: 3)
770
+ --timeout=MS Operation timeout in ms (default: 60000)
771
+ Examples:
772
+ bun run src/index.ts https://sfile.co/xyz ./downloads
773
+ bun run src/index.ts --batch=urls.txt --concurrency=3
774
+ bun run src/index.ts https://sfile.co/abc --debug --headless=false
775
+ Exit Codes:
776
+ 0 Success
777
+ 1 Download/operation error
778
+ 2 CLI/validation error
779
+ `);
780
+ }
781
+ }
782
+ const main = async () => {
783
+ const app = new App();
784
+ const shutdown = async (signal) => {
785
+ console.error(`\nReceived ${signal}, shutting down...`);
786
+ exit(130);
787
+ };
788
+ process.once('SIGINT', () => {
789
+ void shutdown('SIGINT');
790
+ });
791
+ process.once('SIGTERM', () => {
792
+ void shutdown('SIGTERM');
793
+ });
794
+ try {
795
+ const exitCode = await app.run();
796
+ exit(exitCode);
797
+ }
798
+ catch (err) {
799
+ const error = err instanceof Error ? err : new Error(String(err));
800
+ console.error(`[FATAL] Unhandled error: ${error.message}`);
801
+ if (process.env['NODE_ENV'] === 'development' && error.stack !== undefined) {
802
+ console.error(error.stack);
803
+ }
804
+ exit(1);
805
+ }
806
+ };
807
+ if (require.main === module) {
808
+ void main();
809
+ }
810
+
811
+ export { App, BrowserManager, CliParser, Downloader, Logger };
812
+ //# sourceMappingURL=index.mjs.map