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