bytifi 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # Bytifi CLI
2
2
 
3
- Official command-line tool for encrypting and uploading files to [Bytifi](https://bytifi.com).
3
+ Official command-line tool for encrypting, uploading, and decrypting files with [Bytifi](https://bytifi.com).
4
4
 
5
5
  ## Install
6
6
 
7
+ ### npm (all platforms)
8
+
7
9
  ```bash
8
10
  npm install -g bytifi
9
11
  ```
10
12
 
11
13
  Requires **Node.js 18+**.
12
14
 
13
- Or install from source:
15
+ Or from source:
14
16
 
15
17
  ```bash
16
18
  git clone https://github.com/jpwcguy/Bytifi.git
@@ -18,16 +20,37 @@ cd Bytifi
18
20
  npm link
19
21
  ```
20
22
 
21
- ## Setup
23
+ ### Windows (WinGet)
24
+
25
+ ```powershell
26
+ winget install Bytifi.Bytifi
27
+ ```
28
+
29
+ Or download [bytifi.exe from GitHub Releases](https://github.com/jpwcguy/Bytifi/releases/latest).
30
+
31
+ ### Environment variable (all platforms)
22
32
 
23
33
  ```bash
24
34
  export BYTIFI_API_KEY=usk_your_api_key_here
25
35
  ```
26
36
 
37
+ PowerShell:
38
+
39
+ ```powershell
40
+ $env:BYTIFI_API_KEY = "usk_your_api_key_here"
41
+ ```
42
+
27
43
  Create an API key in **Account → API** on bytifi.com.
28
44
 
45
+ ## Setup
46
+
47
+ Set your API key via environment variable (see Install above) or pass `--api-key` per command.
48
+ Prefer the environment variable — keys on the command line can appear in shell history and process lists.
49
+
29
50
  ## Usage
30
51
 
52
+ ### Upload
53
+
31
54
  ```bash
32
55
  bytifi upload ./photo.png
33
56
  bytifi upload ./photo.png --api-key usk_your_api_key_here
@@ -35,6 +58,42 @@ bytifi upload ./photo.png --expires 60 --json
35
58
  bytifi upload ./large.iso -q
36
59
  ```
37
60
 
61
+ ### Decrypt
62
+
63
+ Download and decrypt a shared file locally, or decrypt a file you already downloaded from `/f/...`. No API key required.
64
+
65
+ ```bash
66
+ # From share URL (downloads + decrypts)
67
+ bytifi decrypt 'https://bytifi.com/link?link=TOKEN#token=ENCRYPTION_TOKEN'
68
+ bytifi decrypt TOKEN --token ENCRYPTION_TOKEN -o ./restored-file.pdf
69
+
70
+ # From a downloaded encrypted file
71
+ bytifi decrypt ./downloaded.encrypted --link TOKEN --token ENCRYPTION_TOKEN
72
+ bytifi decrypt ./downloaded.encrypted --meta ./upload-meta.json --token ENCRYPTION_TOKEN
73
+ bytifi decrypt ./parts/ --link TOKEN --token ENCRYPTION_TOKEN
74
+ ```
75
+
76
+ Use the full share URL (with `#token=...`) when possible. For `/f/...` downloads, pass the encryption token with `--token` and either `--link` (fetches metadata from the API) or `--meta` (saved `clientEncryptionMeta` JSON from upload `--json` output).
77
+
78
+ #### Offline decrypt workflow
79
+
80
+ Save metadata when you upload, so you can decrypt a downloaded `/f/...` file even after the link expires:
81
+
82
+ ```bash
83
+ # 1. Upload and save the JSON output
84
+ bytifi upload ./report.pdf --json > upload.json
85
+
86
+ # 2. Download the encrypted file manually (browser, curl, etc.)
87
+ curl -L "$(jq -r .encryptedFile upload.json)" -o report.encrypted
88
+
89
+ # 3. Save metadata and decrypt locally (no API call needed)
90
+ jq -c .clientEncryptionMeta upload.json > report.meta.json
91
+ bytifi decrypt ./report.encrypted \
92
+ --meta ./report.meta.json \
93
+ --token "$(jq -r .encryptionToken upload.json)" \
94
+ -o ./report.pdf
95
+ ```
96
+
38
97
  Without a global install:
39
98
 
40
99
  ```bash
@@ -44,7 +103,7 @@ npm exec bytifi -- upload ./photo.png --api-key usk_your_api_key_here
44
103
 
45
104
  Note: with `npm exec`, put `--` before the file path so npm does not swallow `--api-key`.
46
105
 
47
- ### Options
106
+ ### Upload options
48
107
 
49
108
  | Flag | Description |
50
109
  |------|-------------|
@@ -53,22 +112,50 @@ Note: with `npm exec`, put `--` before the file path so npm does not swallow `--
53
112
  | `--delete-on-download` | Delete after first download |
54
113
  | `--json` | Machine-readable JSON output |
55
114
  | `-q, --quiet` | Print only the share URL |
56
- | `--mime-type` | Override MIME type detection |
115
+ | `--verbose` | Print API error details to stderr |
116
+ | `--mime-type` | Override detected MIME type |
117
+ | `--base-url` | API base URL (default: `https://bytifi.com`) |
118
+
119
+ ### Decrypt options
120
+
121
+ | Flag | Description |
122
+ |------|-------------|
123
+ | `--token` | Encryption token (required if not in URL `#token=...`) |
124
+ | `--link` | Link token for metadata when decrypting a local encrypted file |
125
+ | `--meta` | Saved `clientEncryptionMeta` JSON for offline local decrypt |
126
+ | `--share-url` | Share URL to read token/metadata while decrypting a local file |
127
+ | `-o, --output` | Output file path |
128
+ | `--output-dir` | Output directory when saving under the original filename |
129
+ | `--json` | Machine-readable JSON output |
130
+ | `-q, --quiet` | Print only the output file path |
131
+ | `--verbose` | Print error details to stderr |
132
+ | `--base-url` | API base URL (default: `https://bytifi.com`) |
133
+
134
+ Exit codes: `0` success, `1` usage error, `2` API error, `3` network error.
135
+
136
+ JSON output (`--json`) for upload includes `shareUrl`, `encryptedFile`, `encryptionToken`, `clientEncryptionMeta`, `expiresAt`, and `token`.
137
+
138
+ JSON output for decrypt includes `outputPath`, `originalName`, `size`, `mimeType`, `expiresAt`, `linkToken`, `storageMode`, and `sourcePath` (for local decrypt).
139
+
140
+ Files over ~100 MB encrypted use multipart upload automatically. Progress prints to stderr unless `--json` or `--quiet` is set.
57
141
 
58
142
  ## How it works
59
143
 
144
+ **Upload**
145
+
60
146
  1. Encrypts the file locally with AES-GCM (same format as the website)
61
- 2. Uploads encrypted bytes via the public API
147
+ 2. Uploads encrypted bytes via the public API (parallel part uploads for large files)
62
148
  3. Prints a share URL including `#token=...`
63
149
 
64
- The server never receives plaintext or the decryption token.
150
+ **Decrypt**
151
+
152
+ 1. Reads link metadata from `/api/link/:token`, or from a saved `--meta` JSON file
153
+ 2. Downloads the encrypted file (single blob or per-part for large uploads), unless you pass a local encrypted file
154
+ 3. Decrypts locally and writes the original file to disk
65
155
 
66
156
  ## Development
67
157
 
68
158
  ```bash
69
159
  node bin/bytifi.js upload ./file.png --json
160
+ node bin/bytifi.js decrypt ./file.encrypted --meta ./meta.json --token '...'
70
161
  ```
71
-
72
- ## Status
73
-
74
- WIP — v0.1.0 direct + multipart upload supported.
package/bin/bytifi.js CHANGED
@@ -3,34 +3,73 @@
3
3
  import fs from 'node:fs/promises'
4
4
  import path from 'node:path'
5
5
  import process from 'node:process'
6
- import { fileURLToPath } from 'node:url'
6
+ import { createRequire } from 'node:module'
7
+ import { decryptFile } from '../lib/decrypt.js'
7
8
  import { BytifiApiError, BytifiNetworkError, uploadFile } from '../lib/upload.js'
8
9
 
9
- const __filename = fileURLToPath(import.meta.url)
10
+ const require = createRequire(import.meta.url)
11
+ const { version } = require('../package.json')
10
12
 
11
13
  function printHelp() {
12
- process.stdout.write(`Bytifi CLI — encrypt and upload files
14
+ process.stdout.write(`Bytifi CLI v${version} — encrypt, upload, and decrypt files
13
15
 
14
16
  Usage:
15
17
  bytifi upload <file> [options]
18
+ bytifi decrypt <url-or-token> [options]
19
+ bytifi decrypt <encrypted-file> [options]
16
20
 
17
- Options:
18
- -k, --api-key <key> API key (default: BYTIFI_API_KEY env var)
19
- -e, --expires <minutes> Link lifetime: 5|15|30|60|120 (default: 30)
21
+ Upload options:
22
+ -k, --api-key <key> API key (default: BYTIFI_API_KEY env var)
23
+ -e, --expires <minutes> Link lifetime: 5|15|30|60|120 (default: 30)
20
24
  --delete-on-download Remove file after first download
21
25
  --json Print machine-readable JSON to stdout
22
26
  -q, --quiet Print only the share URL
27
+ --verbose Show API error details on stderr
23
28
  --mime-type <type> Override detected MIME type
24
29
  --base-url <url> API base URL (default: https://bytifi.com)
30
+
31
+ Decrypt options:
32
+ --token <token> Encryption token (if not in URL #token=...)
33
+ --link <token> Link token for metadata (local encrypted files)
34
+ --meta <path> Saved clientEncryptionMeta JSON (offline local decrypt)
35
+ --share-url <url> Share URL for token/metadata when decrypting a local file
36
+ -o, --output <path> Output file path (default: original filename)
37
+ --output-dir <dir> Directory for decrypted file (default: cwd)
38
+ --json Print machine-readable JSON to stdout
39
+ -q, --quiet Print only the output file path
40
+ --verbose Show error details on stderr
41
+ --base-url <url> API base URL (default: https://bytifi.com)
42
+
43
+ Global:
44
+ -V, --version Show version
25
45
  -h, --help Show this help
26
46
 
47
+ Exit codes:
48
+ 0 success
49
+ 1 usage or validation error
50
+ 2 API error (4xx/5xx response)
51
+ 3 network error
52
+
27
53
  Examples:
28
54
  bytifi upload ./photo.png
29
55
  BYTIFI_API_KEY=usk_... bytifi upload report.pdf --expires 60 --json
56
+ bytifi decrypt 'https://bytifi.com/link?link=abc123#token=...'
57
+ bytifi decrypt abc123 --token '...' -o ./report.pdf
58
+ bytifi decrypt ./downloaded.encrypted --link abc123 --token '...'
59
+ bytifi decrypt ./downloaded.encrypted --meta ./upload-meta.json --token '...'
60
+ bytifi decrypt ./parts/ --link abc123 --token '...'
30
61
  `)
31
62
  }
32
63
 
33
- function parseArgs(argv) {
64
+ function readFlagValue(argv, index, flagName) {
65
+ const value = argv[index + 1]
66
+ if (!value || value.startsWith('-')) {
67
+ throw new Error(`Option ${flagName} requires a value.`)
68
+ }
69
+ return value
70
+ }
71
+
72
+ function parseUploadArgs(argv) {
34
73
  const positional = []
35
74
  const options = {
36
75
  apiKey: process.env.BYTIFI_API_KEY || '',
@@ -38,9 +77,11 @@ function parseArgs(argv) {
38
77
  deleteOnDownload: false,
39
78
  json: false,
40
79
  quiet: false,
80
+ verbose: false,
41
81
  mimeType: '',
42
82
  baseUrl: 'https://bytifi.com',
43
83
  help: false,
84
+ version: false,
44
85
  }
45
86
 
46
87
  for (let index = 0; index < argv.length; index += 1) {
@@ -51,6 +92,11 @@ function parseArgs(argv) {
51
92
  continue
52
93
  }
53
94
 
95
+ if (arg === '--version' || arg === '-V') {
96
+ options.version = true
97
+ continue
98
+ }
99
+
54
100
  if (arg === '--json') {
55
101
  options.json = true
56
102
  continue
@@ -61,31 +107,138 @@ function parseArgs(argv) {
61
107
  continue
62
108
  }
63
109
 
110
+ if (arg === '--verbose') {
111
+ options.verbose = true
112
+ continue
113
+ }
114
+
64
115
  if (arg === '--delete-on-download') {
65
116
  options.deleteOnDownload = true
66
117
  continue
67
118
  }
68
119
 
69
120
  if (arg === '--api-key' || arg === '-k') {
70
- options.apiKey = argv[index + 1] || ''
121
+ options.apiKey = readFlagValue(argv, index, arg)
71
122
  index += 1
72
123
  continue
73
124
  }
74
125
 
75
126
  if (arg === '--expires' || arg === '-e') {
76
- options.expiresInMinutes = Number(argv[index + 1])
127
+ const raw = readFlagValue(argv, index, arg)
128
+ const minutes = Number(raw)
129
+ if (!Number.isFinite(minutes)) {
130
+ throw new Error(`Invalid expires value: ${raw}`)
131
+ }
132
+ options.expiresInMinutes = minutes
77
133
  index += 1
78
134
  continue
79
135
  }
80
136
 
81
137
  if (arg === '--mime-type') {
82
- options.mimeType = argv[index + 1] || ''
138
+ options.mimeType = readFlagValue(argv, index, arg)
83
139
  index += 1
84
140
  continue
85
141
  }
86
142
 
87
143
  if (arg === '--base-url') {
88
- options.baseUrl = argv[index + 1] || options.baseUrl
144
+ options.baseUrl = readFlagValue(argv, index, arg)
145
+ index += 1
146
+ continue
147
+ }
148
+
149
+ if (arg.startsWith('-')) {
150
+ throw new Error(`Unknown option: ${arg}`)
151
+ }
152
+
153
+ positional.push(arg)
154
+ }
155
+
156
+ return { positional, options }
157
+ }
158
+
159
+ function parseDecryptArgs(argv) {
160
+ const positional = []
161
+ const options = {
162
+ encryptionToken: '',
163
+ linkToken: '',
164
+ metaPath: '',
165
+ shareUrl: '',
166
+ output: '',
167
+ outputDirectory: '',
168
+ json: false,
169
+ quiet: false,
170
+ verbose: false,
171
+ baseUrl: 'https://bytifi.com',
172
+ help: false,
173
+ version: false,
174
+ }
175
+
176
+ for (let index = 0; index < argv.length; index += 1) {
177
+ const arg = argv[index]
178
+
179
+ if (arg === '--help' || arg === '-h') {
180
+ options.help = true
181
+ continue
182
+ }
183
+
184
+ if (arg === '--version' || arg === '-V') {
185
+ options.version = true
186
+ continue
187
+ }
188
+
189
+ if (arg === '--json') {
190
+ options.json = true
191
+ continue
192
+ }
193
+
194
+ if (arg === '--quiet' || arg === '-q') {
195
+ options.quiet = true
196
+ continue
197
+ }
198
+
199
+ if (arg === '--verbose') {
200
+ options.verbose = true
201
+ continue
202
+ }
203
+
204
+ if (arg === '--token') {
205
+ options.encryptionToken = readFlagValue(argv, index, arg)
206
+ index += 1
207
+ continue
208
+ }
209
+
210
+ if (arg === '--link') {
211
+ options.linkToken = readFlagValue(argv, index, arg)
212
+ index += 1
213
+ continue
214
+ }
215
+
216
+ if (arg === '--meta') {
217
+ options.metaPath = readFlagValue(argv, index, arg)
218
+ index += 1
219
+ continue
220
+ }
221
+
222
+ if (arg === '--share-url') {
223
+ options.shareUrl = readFlagValue(argv, index, arg)
224
+ index += 1
225
+ continue
226
+ }
227
+
228
+ if (arg === '--output' || arg === '-o') {
229
+ options.output = readFlagValue(argv, index, arg)
230
+ index += 1
231
+ continue
232
+ }
233
+
234
+ if (arg === '--output-dir') {
235
+ options.outputDirectory = readFlagValue(argv, index, arg)
236
+ index += 1
237
+ continue
238
+ }
239
+
240
+ if (arg === '--base-url') {
241
+ options.baseUrl = readFlagValue(argv, index, arg)
89
242
  index += 1
90
243
  continue
91
244
  }
@@ -107,6 +260,14 @@ function validateExpires(minutes) {
107
260
  }
108
261
  }
109
262
 
263
+ function writeProgress(label, percent) {
264
+ process.stderr.write(`\r${label}: ${percent}%`)
265
+ }
266
+
267
+ function clearProgressLine() {
268
+ process.stderr.write('\r\x1b[K')
269
+ }
270
+
110
271
  async function runUpload(filePath, options) {
111
272
  validateExpires(options.expiresInMinutes)
112
273
 
@@ -115,34 +276,120 @@ async function runUpload(filePath, options) {
115
276
  }
116
277
 
117
278
  const resolvedPath = path.resolve(filePath)
118
- await fs.access(resolvedPath)
119
-
120
- const result = await uploadFile(resolvedPath, {
121
- apiKey: options.apiKey,
122
- baseUrl: options.baseUrl,
123
- expiresInMinutes: options.expiresInMinutes,
124
- deleteOnDownload: options.deleteOnDownload,
125
- mimeType: options.mimeType || undefined,
126
- onProgress: options.quiet || options.json
127
- ? undefined
128
- : (percent) => {
129
- process.stderr.write(`Encrypting and uploading: ${percent}%\n`)
130
- },
131
- })
132
-
133
- if (options.json) {
134
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
135
- return
279
+
280
+ try {
281
+ await fs.access(resolvedPath)
282
+ } catch {
283
+ throw new Error(`File not found or not readable: ${resolvedPath}`)
136
284
  }
137
285
 
138
- if (options.quiet) {
139
- process.stdout.write(`${result.shareUrl}\n`)
140
- return
286
+ const abortController = new AbortController()
287
+
288
+ const handleSignal = () => {
289
+ abortController.abort()
141
290
  }
142
291
 
143
- process.stdout.write(`Share URL:\n${result.shareUrl}\n`)
144
- process.stdout.write(`Download URL:\n${result.downloadUrl}\n`)
145
- process.stdout.write(`Expires: ${result.expiresAt}\n`)
292
+ process.on('SIGINT', handleSignal)
293
+ process.on('SIGTERM', handleSignal)
294
+
295
+ const showProgress = !options.quiet && !options.json
296
+ let lastPercent = -1
297
+
298
+ try {
299
+ const result = await uploadFile(resolvedPath, {
300
+ apiKey: options.apiKey,
301
+ baseUrl: options.baseUrl,
302
+ expiresInMinutes: options.expiresInMinutes,
303
+ deleteOnDownload: options.deleteOnDownload,
304
+ mimeType: options.mimeType || undefined,
305
+ signal: abortController.signal,
306
+ onProgress: showProgress
307
+ ? (percent) => {
308
+ if (percent !== lastPercent) {
309
+ lastPercent = percent
310
+ writeProgress('Encrypting and uploading', percent)
311
+ }
312
+ }
313
+ : undefined,
314
+ })
315
+
316
+ if (showProgress) {
317
+ clearProgressLine()
318
+ }
319
+
320
+ if (options.json) {
321
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
322
+ return
323
+ }
324
+
325
+ if (options.quiet) {
326
+ process.stdout.write(`${result.shareUrl}\n`)
327
+ return
328
+ }
329
+
330
+ process.stdout.write(`Share URL:\n${result.shareUrl}\n`)
331
+ process.stdout.write(`Encrypted file:\n${result.encryptedFile}\n`)
332
+ process.stdout.write(`Expires: ${result.expiresAt}\n`)
333
+ } finally {
334
+ process.off('SIGINT', handleSignal)
335
+ process.off('SIGTERM', handleSignal)
336
+ }
337
+ }
338
+
339
+ async function runDecrypt(input, options) {
340
+ const abortController = new AbortController()
341
+
342
+ const handleSignal = () => {
343
+ abortController.abort()
344
+ }
345
+
346
+ process.on('SIGINT', handleSignal)
347
+ process.on('SIGTERM', handleSignal)
348
+
349
+ const showProgress = !options.quiet && !options.json
350
+ let lastPercent = -1
351
+
352
+ try {
353
+ const result = await decryptFile(input, {
354
+ encryptionToken: options.encryptionToken,
355
+ linkToken: options.linkToken,
356
+ metaPath: options.metaPath,
357
+ shareUrl: options.shareUrl,
358
+ output: options.output || undefined,
359
+ outputDirectory: options.outputDirectory || undefined,
360
+ baseUrl: options.baseUrl,
361
+ signal: abortController.signal,
362
+ onProgress: showProgress
363
+ ? (percent) => {
364
+ if (percent !== lastPercent) {
365
+ lastPercent = percent
366
+ writeProgress('Downloading and decrypting', percent)
367
+ }
368
+ }
369
+ : undefined,
370
+ })
371
+
372
+ if (showProgress) {
373
+ clearProgressLine()
374
+ }
375
+
376
+ if (options.json) {
377
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
378
+ return
379
+ }
380
+
381
+ if (options.quiet) {
382
+ process.stdout.write(`${result.outputPath}\n`)
383
+ return
384
+ }
385
+
386
+ process.stdout.write(`Saved: ${result.outputPath}\n`)
387
+ process.stdout.write(`Original name: ${result.originalName}\n`)
388
+ process.stdout.write(`Expires: ${result.expiresAt}\n`)
389
+ } finally {
390
+ process.off('SIGINT', handleSignal)
391
+ process.off('SIGTERM', handleSignal)
392
+ }
146
393
  }
147
394
 
148
395
  function exitCodeForError(error) {
@@ -151,6 +398,25 @@ function exitCodeForError(error) {
151
398
  return 1
152
399
  }
153
400
 
401
+ function printError(error, verbose) {
402
+ process.stderr.write(`${error.message || 'Command failed.'}\n`)
403
+
404
+ if (!verbose) return
405
+
406
+ if (error instanceof BytifiApiError) {
407
+ if (error.status) {
408
+ process.stderr.write(`HTTP ${error.status}\n`)
409
+ }
410
+ if (error.body) {
411
+ process.stderr.write(`${JSON.stringify(error.body, null, 2)}\n`)
412
+ }
413
+ }
414
+
415
+ if (error instanceof BytifiNetworkError && error.cause) {
416
+ process.stderr.write(`${error.cause}\n`)
417
+ }
418
+ }
419
+
154
420
  async function main() {
155
421
  const [command, ...rest] = process.argv.slice(2)
156
422
 
@@ -159,31 +425,82 @@ async function main() {
159
425
  process.exit(0)
160
426
  }
161
427
 
162
- if (command !== 'upload') {
163
- if (command === 'help') {
428
+ if (command === '--version' || command === '-V') {
429
+ process.stdout.write(`${version}\n`)
430
+ process.exit(0)
431
+ }
432
+
433
+ if (command === 'help') {
434
+ printHelp()
435
+ process.exit(0)
436
+ }
437
+
438
+ if (command === 'upload') {
439
+ const { positional, options } = parseUploadArgs(rest)
440
+
441
+ if (options.help) {
164
442
  printHelp()
165
443
  process.exit(0)
166
444
  }
167
445
 
168
- throw new Error(`Unknown command: ${command}`)
169
- }
446
+ if (options.version) {
447
+ process.stdout.write(`${version}\n`)
448
+ process.exit(0)
449
+ }
170
450
 
171
- const { positional, options } = parseArgs(rest)
451
+ if (options.json && options.quiet) {
452
+ throw new Error('Use either --json or --quiet, not both.')
453
+ }
172
454
 
173
- if (options.help) {
174
- printHelp()
175
- process.exit(0)
455
+ const filePath = positional[0]
456
+ if (!filePath) {
457
+ throw new Error('Missing file path. Usage: bytifi upload <file>')
458
+ }
459
+
460
+ if (positional.length > 1) {
461
+ process.stderr.write(`Warning: ignoring extra files: ${positional.slice(1).join(', ')}\n`)
462
+ }
463
+
464
+ await runUpload(filePath, options)
465
+ return
176
466
  }
177
467
 
178
- const filePath = positional[0]
179
- if (!filePath) {
180
- throw new Error('Missing file path. Usage: bytifi upload <file>')
468
+ if (command === 'decrypt') {
469
+ const { positional, options } = parseDecryptArgs(rest)
470
+
471
+ if (options.help) {
472
+ printHelp()
473
+ process.exit(0)
474
+ }
475
+
476
+ if (options.version) {
477
+ process.stdout.write(`${version}\n`)
478
+ process.exit(0)
479
+ }
480
+
481
+ if (options.json && options.quiet) {
482
+ throw new Error('Use either --json or --quiet, not both.')
483
+ }
484
+
485
+ const input = positional[0]
486
+ if (!input) {
487
+ throw new Error('Missing input. Usage: bytifi decrypt <url-or-token|encrypted-file>')
488
+ }
489
+
490
+ if (positional.length > 1) {
491
+ process.stderr.write(`Warning: ignoring extra arguments: ${positional.slice(1).join(', ')}\n`)
492
+ }
493
+
494
+ await runDecrypt(input, options)
495
+ return
181
496
  }
182
497
 
183
- await runUpload(filePath, options)
498
+ throw new Error(`Unknown command: ${command}`)
184
499
  }
185
500
 
186
501
  main().catch((error) => {
187
- process.stderr.write(`${error.message || 'Upload failed.'}\n`)
502
+ clearProgressLine()
503
+ const verbose = process.argv.includes('--verbose')
504
+ printError(error, verbose)
188
505
  process.exit(exitCodeForError(error))
189
506
  })