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