sfiledl 1.0.0 → 1.0.2

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 CHANGED
@@ -1,756 +1,731 @@
1
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';
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
7
 
8
- const isOk = (result) => result.success;
9
- const isErr = (result) => !result.success;
8
+ const isOk = (result) => result.success
9
+ const isErr = (result) => !result.success
10
10
  const safeStringify = (value, space) => {
11
- try {
12
- return JSON.stringify(value, null, space);
13
- }
14
- catch {
15
- return '[Circular or unserializable]';
16
- }
17
- };
11
+ try {
12
+ return JSON.stringify(value, null, space)
13
+ } catch {
14
+ return '[Circular or unserializable]'
15
+ }
16
+ }
18
17
  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));
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))
27
26
  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
- }
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
+ }
52
51
  }
53
52
  class ValidationError extends AppError {
54
- constructor(message, context) {
55
- super(message, 'VALIDATION_ERROR', false, context);
56
- }
53
+ constructor(message, context) {
54
+ super(message, 'VALIDATION_ERROR', false, context)
55
+ }
57
56
  }
58
57
  class NetworkError extends AppError {
59
- constructor(message, context) {
60
- super(message, 'NETWORK_ERROR', true, context);
61
- }
58
+ constructor(message, context) {
59
+ super(message, 'NETWORK_ERROR', true, context)
60
+ }
62
61
  }
63
62
  class FileError extends AppError {
64
- constructor(message, context) {
65
- super(message, 'FILE_ERROR', false, context);
66
- }
63
+ constructor(message, context) {
64
+ super(message, 'FILE_ERROR', false, context)
65
+ }
67
66
  }
68
67
  class BrowserError extends AppError {
69
- constructor(message, context) {
70
- super(message, 'BROWSER_ERROR', true, context);
71
- }
68
+ constructor(message, context) {
69
+ super(message, 'BROWSER_ERROR', true, context)
70
+ }
72
71
  }
73
72
  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
- }
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
+ }
145
140
  }
146
141
  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
- }
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
+ }
172
167
  }
173
168
  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
- }
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
+ }
310
293
  }
311
294
  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
- }
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
+ }
547
527
  }
548
528
  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
- }
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
+ }
704
680
  }
705
681
  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(`
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(`
754
729
  🚀 Sfile Downloader - Strict TypeScript CLI
755
730
  Usage:
756
731
  bun run src/index.ts <url> [saveDir] [options]
@@ -776,37 +751,36 @@ Exit Codes:
776
751
  0 Success
777
752
  1 Download/operation error
778
753
  2 CLI/validation error
779
- `);
780
- }
754
+ `)
755
+ }
781
756
  }
782
757
  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
- };
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
+ }
807
781
  if (require.main === module) {
808
- void main();
782
+ void main()
809
783
  }
810
784
 
811
- export { App, BrowserManager, CliParser, Downloader, Logger };
785
+ export { App, BrowserManager, CliParser, Downloader, Logger }
812
786
  //# sourceMappingURL=index.mjs.map