@orcapt/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,911 @@
1
+ /**
2
+ * Orca Storage Commands
3
+ * Manage S3-like storage buckets and files via Orca Deploy API
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const https = require('https');
11
+ const http = require('http');
12
+ const { getCredentials } = require('./login');
13
+ const { API_BASE_URL, API_ENDPOINTS } = require('../config');
14
+
15
+ /**
16
+ * Make API request to Orca Deploy API
17
+ */
18
+ function makeApiRequest(method, endpoint, credentials, body = null) {
19
+ return new Promise((resolve, reject) => {
20
+ const url = new URL(endpoint, API_BASE_URL);
21
+ const isHttps = url.protocol === 'https:';
22
+ const httpModule = isHttps ? https : http;
23
+
24
+ const options = {
25
+ hostname: url.hostname,
26
+ port: url.port || (isHttps ? 443 : 80),
27
+ path: url.pathname,
28
+ method: method,
29
+ headers: {
30
+ 'x-workspace': credentials.workspace,
31
+ 'x-token': credentials.token,
32
+ 'x-mode': credentials.mode || 'dev',
33
+ 'Content-Type': 'application/json'
34
+ }
35
+ };
36
+
37
+ const req = httpModule.request(options, (res) => {
38
+ let data = '';
39
+
40
+ res.on('data', (chunk) => {
41
+ data += chunk;
42
+ });
43
+
44
+ res.on('end', () => {
45
+ try {
46
+ const response = JSON.parse(data);
47
+ if (res.statusCode >= 200 && res.statusCode < 300) {
48
+ resolve(response);
49
+ } else {
50
+ reject({ statusCode: res.statusCode, response });
51
+ }
52
+ } catch (error) {
53
+ reject(new Error(`Invalid response: ${data}`));
54
+ }
55
+ });
56
+ });
57
+
58
+ req.on('error', (error) => {
59
+ reject(error);
60
+ });
61
+
62
+ if (body) {
63
+ req.write(JSON.stringify(body));
64
+ }
65
+
66
+ req.end();
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Upload file using multipart/form-data
72
+ */
73
+ function uploadFileToApi(endpoint, credentials, filePath, bucketName, options = {}) {
74
+ return new Promise((resolve, reject) => {
75
+ const url = new URL(endpoint.replace('{bucketName}', bucketName), API_BASE_URL);
76
+ const isHttps = url.protocol === 'https:';
77
+ const httpModule = isHttps ? https : http;
78
+
79
+ const fileStream = fs.createReadStream(filePath);
80
+ const fileName = path.basename(filePath);
81
+ const boundary = `----FormBoundary${Date.now()}`;
82
+
83
+ const folderPath = options.folder || '';
84
+
85
+ // Build multipart form data
86
+ let formData = '';
87
+
88
+ // Add folder_path field
89
+ if (folderPath) {
90
+ formData += `--${boundary}\r\n`;
91
+ formData += `Content-Disposition: form-data; name="folder_path"\r\n\r\n`;
92
+ formData += `${folderPath}\r\n`;
93
+ }
94
+
95
+ // Add visibility field
96
+ if (options.visibility) {
97
+ formData += `--${boundary}\r\n`;
98
+ formData += `Content-Disposition: form-data; name="visibility"\r\n\r\n`;
99
+ formData += `${options.visibility}\r\n`;
100
+ }
101
+
102
+ // Add generate_url field
103
+ formData += `--${boundary}\r\n`;
104
+ formData += `Content-Disposition: form-data; name="generate_url"\r\n\r\n`;
105
+ formData += `true\r\n`;
106
+
107
+ // Add file field header
108
+ formData += `--${boundary}\r\n`;
109
+ formData += `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`;
110
+ formData += `Content-Type: application/octet-stream\r\n\r\n`;
111
+
112
+ const formDataBuffer = Buffer.from(formData, 'utf8');
113
+ const endBoundary = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
114
+
115
+ const fileStats = fs.statSync(filePath);
116
+ const contentLength = formDataBuffer.length + fileStats.size + endBoundary.length;
117
+
118
+ const requestOptions = {
119
+ hostname: url.hostname,
120
+ port: url.port || (isHttps ? 443 : 80),
121
+ path: url.pathname,
122
+ method: 'POST',
123
+ headers: {
124
+ 'x-workspace': credentials.workspace,
125
+ 'x-token': credentials.token,
126
+ 'x-mode': credentials.mode || 'dev',
127
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
128
+ 'Content-Length': contentLength
129
+ }
130
+ };
131
+
132
+ const req = httpModule.request(requestOptions, (res) => {
133
+ let data = '';
134
+
135
+ res.on('data', (chunk) => {
136
+ data += chunk;
137
+ });
138
+
139
+ res.on('end', () => {
140
+ try {
141
+ const response = JSON.parse(data);
142
+ if (res.statusCode >= 200 && res.statusCode < 300) {
143
+ resolve(response);
144
+ } else {
145
+ reject({ statusCode: res.statusCode, response });
146
+ }
147
+ } catch (error) {
148
+ reject(new Error(`Invalid response: ${data}`));
149
+ }
150
+ });
151
+ });
152
+
153
+ req.on('error', (error) => {
154
+ reject(error);
155
+ });
156
+
157
+ // Write form data header
158
+ req.write(formDataBuffer);
159
+
160
+ // Stream file
161
+ fileStream.on('data', (chunk) => {
162
+ req.write(chunk);
163
+ });
164
+
165
+ fileStream.on('end', () => {
166
+ req.write(endBoundary);
167
+ req.end();
168
+ });
169
+
170
+ fileStream.on('error', (error) => {
171
+ reject(error);
172
+ });
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Format bytes to human readable
178
+ */
179
+ function formatBytes(bytes) {
180
+ if (bytes === 0) return '0 B';
181
+ const k = 1024;
182
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
183
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
184
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
185
+ }
186
+
187
+ /**
188
+ * Check authentication
189
+ */
190
+ function requireAuth() {
191
+ const credentials = getCredentials();
192
+ if (!credentials) {
193
+ console.log(chalk.red('\nāœ— Not authenticated'));
194
+ console.log(chalk.cyan('Please run:'), chalk.yellow('orca login'), chalk.cyan('first\n'));
195
+ process.exit(1);
196
+ }
197
+ return credentials;
198
+ }
199
+
200
+ /**
201
+ * Bucket Create Command
202
+ */
203
+ async function bucketCreate(bucketName, options = {}) {
204
+ console.log(chalk.cyan('\n============================================================'));
205
+ console.log(chalk.cyan('🪣 Creating Storage Bucket'));
206
+ console.log(chalk.cyan('============================================================\n'));
207
+
208
+ const credentials = requireAuth();
209
+
210
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
211
+ console.log(chalk.white('Workspace: '), chalk.yellow(credentials.workspace));
212
+ console.log(chalk.white('Visibility: '), chalk.yellow(options.public ? 'public' : 'private'));
213
+ if (options.description) {
214
+ console.log(chalk.white('Description:'), chalk.yellow(options.description));
215
+ }
216
+
217
+ const spinner = ora('Creating bucket...').start();
218
+
219
+ try {
220
+ const requestBody = {
221
+ bucket_name: bucketName,
222
+ visibility: options.public ? 'public' : 'private',
223
+ versioning_enabled: options.versioning || false,
224
+ encryption_enabled: options.encryption !== false,
225
+ encryption_type: options.encryptionType || 'AES256'
226
+ };
227
+
228
+ if (options.description) {
229
+ requestBody.description = options.description;
230
+ }
231
+
232
+ const response = await makeApiRequest(
233
+ 'POST',
234
+ API_ENDPOINTS.STORAGE_BUCKET_CREATE,
235
+ credentials,
236
+ requestBody
237
+ );
238
+
239
+ spinner.succeed(chalk.green('āœ“ Bucket created successfully!'));
240
+
241
+ console.log(chalk.cyan('\nšŸ“¦ Bucket Details:'));
242
+ console.log(chalk.white(' Name: '), chalk.yellow(response.bucket.bucket_name));
243
+ console.log(chalk.white(' AWS Bucket: '), chalk.gray(response.bucket.aws_bucket_name));
244
+ console.log(chalk.white(' Region: '), chalk.yellow(response.bucket.region));
245
+ console.log(chalk.white(' Status: '), chalk.green(response.bucket.status));
246
+ console.log(chalk.white(' Visibility: '), chalk.yellow(response.bucket.visibility));
247
+ console.log(chalk.white(' Encryption: '), chalk.yellow(response.bucket.encryption_enabled ? 'Enabled' : 'Disabled'));
248
+
249
+ console.log(chalk.cyan('\nšŸ’” Next Steps:'));
250
+ console.log(chalk.white(' Upload file: '), chalk.yellow(`orca storage upload ${bucketName} <file-path>`));
251
+ console.log(chalk.white(' List files: '), chalk.yellow(`orca storage files ${bucketName}`));
252
+ console.log('');
253
+
254
+ } catch (error) {
255
+ spinner.fail(chalk.red('āœ— Failed to create bucket'));
256
+
257
+ if (error.response) {
258
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
259
+ if (error.statusCode === 409) {
260
+ console.log(chalk.yellow(' Bucket name already exists for this workspace'));
261
+ } else if (error.statusCode === 422) {
262
+ console.log(chalk.yellow(' Invalid bucket name or parameters'));
263
+ }
264
+ } else {
265
+ console.log(chalk.red(`\nāœ— ${error.message}`));
266
+ }
267
+ console.log('');
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Bucket List Command
274
+ */
275
+ async function bucketList() {
276
+ console.log(chalk.cyan('\n============================================================'));
277
+ console.log(chalk.cyan('šŸ“‹ Listing Storage Buckets'));
278
+ console.log(chalk.cyan('============================================================\n'));
279
+
280
+ const credentials = requireAuth();
281
+ console.log(chalk.white('Workspace:'), chalk.yellow(credentials.workspace));
282
+
283
+ const spinner = ora('Fetching buckets...').start();
284
+
285
+ try {
286
+ const response = await makeApiRequest(
287
+ 'GET',
288
+ API_ENDPOINTS.STORAGE_BUCKET_LIST,
289
+ credentials
290
+ );
291
+
292
+ spinner.succeed(chalk.green(`āœ“ Found ${response.count} bucket(s)`));
293
+
294
+ if (response.count === 0) {
295
+ console.log(chalk.yellow('\nšŸ“­ No buckets found'));
296
+ console.log(chalk.cyan('\nšŸ’” Create your first bucket:'));
297
+ console.log(chalk.white(' '), chalk.yellow('orca storage bucket create my-bucket'));
298
+ console.log('');
299
+ return;
300
+ }
301
+
302
+ console.log('');
303
+ console.log(chalk.white('─'.repeat(100)));
304
+ console.log(
305
+ chalk.white('NAME').padEnd(25),
306
+ chalk.white('FILES').padEnd(10),
307
+ chalk.white('SIZE').padEnd(15),
308
+ chalk.white('VISIBILITY').padEnd(15),
309
+ chalk.white('STATUS').padEnd(15),
310
+ chalk.white('CREATED')
311
+ );
312
+ console.log(chalk.white('─'.repeat(100)));
313
+
314
+ response.buckets.forEach(bucket => {
315
+ const name = bucket.bucket_name.padEnd(25);
316
+ const files = String(bucket.file_count).padEnd(10);
317
+ const size = bucket.total_size.padEnd(15);
318
+ const visibility = bucket.visibility.padEnd(15);
319
+ const status = bucket.status === 'active'
320
+ ? chalk.green(bucket.status.padEnd(15))
321
+ : chalk.yellow(bucket.status.padEnd(15));
322
+ const created = new Date(bucket.created_at).toLocaleDateString();
323
+
324
+ console.log(
325
+ chalk.yellow(name),
326
+ chalk.white(files),
327
+ chalk.cyan(size),
328
+ visibility === 'public ' ? chalk.green(visibility) : chalk.gray(visibility),
329
+ status,
330
+ chalk.gray(created)
331
+ );
332
+ });
333
+
334
+ console.log(chalk.white('─'.repeat(100)));
335
+ console.log('');
336
+
337
+ } catch (error) {
338
+ spinner.fail(chalk.red('āœ— Failed to list buckets'));
339
+
340
+ if (error.response) {
341
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}\n`));
342
+ } else {
343
+ console.log(chalk.red(`\nāœ— ${error.message}\n`));
344
+ }
345
+ process.exit(1);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * File Upload Command
351
+ */
352
+ async function fileUpload(bucketName, localPath, options = {}) {
353
+ console.log(chalk.cyan('\n============================================================'));
354
+ console.log(chalk.cyan('šŸ“¤ Uploading File'));
355
+ console.log(chalk.cyan('============================================================\n'));
356
+
357
+ const credentials = requireAuth();
358
+
359
+ // Check if file exists
360
+ if (!fs.existsSync(localPath)) {
361
+ console.log(chalk.red(`āœ— File not found: ${localPath}\n`));
362
+ process.exit(1);
363
+ }
364
+
365
+ const fileStats = fs.statSync(localPath);
366
+ const fileSizeMB = (fileStats.size / (1024 * 1024)).toFixed(2);
367
+ const fileName = path.basename(localPath);
368
+
369
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
370
+ console.log(chalk.white('File: '), chalk.yellow(fileName));
371
+ console.log(chalk.white('Local Path: '), chalk.gray(localPath));
372
+ console.log(chalk.white('Size: '), chalk.yellow(`${fileSizeMB} MB`));
373
+
374
+ if (options.folder) {
375
+ console.log(chalk.white('Folder: '), chalk.yellow(options.folder));
376
+ }
377
+
378
+ console.log(chalk.white('Visibility: '), chalk.yellow(options.public ? 'public' : 'private'));
379
+
380
+ const spinner = ora('Uploading file...').start();
381
+
382
+ try {
383
+ const uploadOptions = {
384
+ folder: options.folder || '',
385
+ visibility: options.public ? 'public' : 'private'
386
+ };
387
+
388
+ const response = await uploadFileToApi(
389
+ API_ENDPOINTS.STORAGE_UPLOAD,
390
+ credentials,
391
+ localPath,
392
+ bucketName,
393
+ uploadOptions
394
+ );
395
+
396
+ spinner.succeed(chalk.green('āœ“ File uploaded successfully!'));
397
+
398
+ console.log(chalk.cyan('\nšŸ“„ File Details:'));
399
+ console.log(chalk.white(' Name: '), chalk.yellow(response.file.file_name));
400
+ console.log(chalk.white(' Key: '), chalk.gray(response.file.file_key));
401
+ console.log(chalk.white(' Size: '), chalk.yellow(response.file.file_size));
402
+ console.log(chalk.white(' Type: '), chalk.yellow(response.file.mime_type));
403
+ console.log(chalk.white(' Visibility: '), chalk.yellow(response.file.visibility));
404
+
405
+ if (response.file.download_url) {
406
+ console.log(chalk.cyan('\nšŸ”— Download URL (valid for 60 minutes):'));
407
+ console.log(chalk.gray(response.file.download_url));
408
+ }
409
+
410
+ console.log(chalk.cyan('\nšŸ’” Next Steps:'));
411
+ console.log(chalk.white(' List files: '), chalk.yellow(`orca storage files ${bucketName}`));
412
+ console.log(chalk.white(' Download: '), chalk.yellow(`orca storage download ${bucketName} ${response.file.file_key}`));
413
+ console.log('');
414
+
415
+ } catch (error) {
416
+ spinner.fail(chalk.red('āœ— Upload failed'));
417
+
418
+ if (error.response) {
419
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
420
+ if (error.statusCode === 404) {
421
+ console.log(chalk.yellow(' Bucket not found'));
422
+ } else if (error.statusCode === 413) {
423
+ console.log(chalk.yellow(' File too large (max 100MB)'));
424
+ }
425
+ } else {
426
+ console.log(chalk.red(`\nāœ— ${error.message}`));
427
+ }
428
+ console.log('');
429
+ process.exit(1);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * File Download Command
435
+ */
436
+ async function fileDownload(bucketName, fileKey, localPath) {
437
+ console.log(chalk.cyan('\n============================================================'));
438
+ console.log(chalk.cyan('šŸ“„ Downloading File'));
439
+ console.log(chalk.cyan('============================================================\n'));
440
+
441
+ const credentials = requireAuth();
442
+
443
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
444
+ console.log(chalk.white('File Key: '), chalk.yellow(fileKey));
445
+ console.log(chalk.white('Local Path: '), chalk.yellow(localPath || 'current directory'));
446
+
447
+ const spinner = ora('Getting download URL...').start();
448
+
449
+ try {
450
+ // Get pre-signed download URL from API
451
+ const encodedFileKey = encodeURIComponent(fileKey);
452
+ const response = await makeApiRequest(
453
+ 'GET',
454
+ API_ENDPOINTS.STORAGE_DOWNLOAD.replace('{bucketName}', bucketName).replace('{fileKey}', encodedFileKey),
455
+ credentials
456
+ );
457
+
458
+ spinner.text = 'Downloading file...';
459
+
460
+ // Download file from S3 using pre-signed URL
461
+ const downloadUrl = response.download_url;
462
+ const fileName = response.file.file_name;
463
+ const outputPath = localPath || fileName;
464
+
465
+ // Download file using the pre-signed URL
466
+ await new Promise((resolve, reject) => {
467
+ const urlObj = new URL(downloadUrl);
468
+ const isHttps = urlObj.protocol === 'https:';
469
+ const httpModule = isHttps ? https : http;
470
+
471
+ const fileWriter = fs.createWriteStream(outputPath);
472
+
473
+ httpModule.get(downloadUrl, (res) => {
474
+ if (res.statusCode !== 200) {
475
+ reject(new Error(`Failed to download: ${res.statusCode}`));
476
+ return;
477
+ }
478
+
479
+ res.pipe(fileWriter);
480
+
481
+ fileWriter.on('finish', () => {
482
+ fileWriter.close();
483
+ resolve();
484
+ });
485
+ }).on('error', (err) => {
486
+ fs.unlink(outputPath, () => {});
487
+ reject(err);
488
+ });
489
+
490
+ fileWriter.on('error', (err) => {
491
+ fs.unlink(outputPath, () => {});
492
+ reject(err);
493
+ });
494
+ });
495
+
496
+ spinner.succeed(chalk.green('āœ“ File downloaded successfully!'));
497
+
498
+ console.log(chalk.cyan('\nšŸ“„ File Details:'));
499
+ console.log(chalk.white(' Name: '), chalk.yellow(response.file.file_name));
500
+ console.log(chalk.white(' Size: '), chalk.yellow(formatBytes(response.file.file_size_bytes)));
501
+ console.log(chalk.white(' Type: '), chalk.yellow(response.file.mime_type));
502
+ console.log(chalk.white(' Location: '), chalk.gray(path.resolve(outputPath)));
503
+ console.log('');
504
+
505
+ } catch (error) {
506
+ spinner.fail(chalk.red('āœ— Download failed'));
507
+
508
+ if (error.response) {
509
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
510
+ if (error.statusCode === 404) {
511
+ console.log(chalk.yellow(' File or bucket not found'));
512
+ }
513
+ } else {
514
+ console.log(chalk.red(`\nāœ— ${error.message}`));
515
+ }
516
+ console.log('');
517
+ process.exit(1);
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Bucket Info Command
523
+ */
524
+ async function bucketInfo(bucketName) {
525
+ console.log(chalk.cyan('\n============================================================'));
526
+ console.log(chalk.cyan('ā„¹ļø Bucket Information'));
527
+ console.log(chalk.cyan('============================================================\n'));
528
+
529
+ const credentials = requireAuth();
530
+ const spinner = ora('Fetching bucket info...').start();
531
+
532
+ try {
533
+ const response = await makeApiRequest(
534
+ 'GET',
535
+ API_ENDPOINTS.STORAGE_BUCKET_LIST.replace('/list', `/${bucketName}`),
536
+ credentials
537
+ );
538
+
539
+ spinner.succeed(chalk.green('āœ“ Bucket info retrieved'));
540
+
541
+ const bucket = response.bucket;
542
+
543
+ console.log(chalk.cyan('\nšŸ“¦ Bucket Details:'));
544
+ console.log(chalk.white(' Name: '), chalk.yellow(bucket.bucket_name));
545
+ console.log(chalk.white(' AWS Bucket: '), chalk.gray(bucket.aws_bucket_name));
546
+ console.log(chalk.white(' Region: '), chalk.yellow(bucket.region));
547
+ console.log(chalk.white(' Status: '), bucket.status === 'active' ? chalk.green(bucket.status) : chalk.yellow(bucket.status));
548
+ console.log(chalk.white(' Visibility: '), bucket.visibility === 'public' ? chalk.green(bucket.visibility) : chalk.gray(bucket.visibility));
549
+ console.log(chalk.white(' Files: '), chalk.yellow(bucket.file_count));
550
+ console.log(chalk.white(' Total Size: '), chalk.cyan(bucket.total_size));
551
+ console.log(chalk.white(' Versioning: '), bucket.versioning_enabled ? chalk.green('Enabled') : chalk.gray('Disabled'));
552
+ console.log(chalk.white(' Encryption: '), bucket.encryption_enabled ? chalk.green(`Enabled (${bucket.encryption_type})`) : chalk.gray('Disabled'));
553
+
554
+ if (bucket.description) {
555
+ console.log(chalk.white(' Description: '), chalk.gray(bucket.description));
556
+ }
557
+
558
+ console.log(chalk.white(' Created: '), chalk.gray(new Date(bucket.created_at).toLocaleString()));
559
+ console.log('');
560
+
561
+ } catch (error) {
562
+ spinner.fail(chalk.red('āœ— Failed to get bucket info'));
563
+
564
+ if (error.response) {
565
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
566
+ if (error.statusCode === 404) {
567
+ console.log(chalk.yellow(' Bucket not found'));
568
+ }
569
+ } else {
570
+ console.log(chalk.red(`\nāœ— ${error.message}`));
571
+ }
572
+ console.log('');
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Bucket Delete Command
579
+ */
580
+ async function bucketDelete(bucketName, options = {}) {
581
+ console.log(chalk.cyan('\n============================================================'));
582
+ console.log(chalk.cyan('šŸ—‘ļø Deleting Storage Bucket'));
583
+ console.log(chalk.cyan('============================================================\n'));
584
+
585
+ const credentials = requireAuth();
586
+
587
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
588
+ console.log(chalk.white('Workspace:'), chalk.yellow(credentials.workspace));
589
+
590
+ if (options.force) {
591
+ console.log(chalk.yellow('\nāš ļø Force delete enabled - all files will be deleted'));
592
+ }
593
+
594
+ const spinner = ora('Deleting bucket...').start();
595
+
596
+ try {
597
+ const endpoint = API_ENDPOINTS.STORAGE_BUCKET_LIST.replace('/list', `/${bucketName}`) +
598
+ (options.force ? '?force=true' : '');
599
+
600
+ await makeApiRequest('DELETE', endpoint, credentials);
601
+
602
+ spinner.succeed(chalk.green('āœ“ Bucket deleted successfully!'));
603
+ console.log('');
604
+
605
+ } catch (error) {
606
+ spinner.fail(chalk.red('āœ— Failed to delete bucket'));
607
+
608
+ if (error.response) {
609
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
610
+ if (error.statusCode === 404) {
611
+ console.log(chalk.yellow(' Bucket not found'));
612
+ } else if (error.statusCode === 400 && error.response.file_count) {
613
+ console.log(chalk.yellow(` Bucket contains ${error.response.file_count} file(s)`));
614
+ console.log(chalk.cyan(' Use --force to delete anyway:'));
615
+ console.log(chalk.white(' '), chalk.yellow(`orca storage bucket delete ${bucketName} --force`));
616
+ }
617
+ } else {
618
+ console.log(chalk.red(`\nāœ— ${error.message}`));
619
+ }
620
+ console.log('');
621
+ process.exit(1);
622
+ }
623
+ }
624
+
625
+ /**
626
+ * File List Command
627
+ */
628
+ async function fileList(bucketName, options = {}) {
629
+ console.log(chalk.cyan('\n============================================================'));
630
+ console.log(chalk.cyan('šŸ“ Listing Files'));
631
+ console.log(chalk.cyan('============================================================\n'));
632
+
633
+ const credentials = requireAuth();
634
+
635
+ console.log(chalk.white('Bucket:'), chalk.yellow(bucketName));
636
+ if (options.folder) {
637
+ console.log(chalk.white('Folder:'), chalk.yellow(options.folder));
638
+ }
639
+
640
+ const spinner = ora('Fetching files...').start();
641
+
642
+ try {
643
+ let endpoint = API_ENDPOINTS.STORAGE_FILE_LIST.replace('{bucketName}', bucketName);
644
+ const params = [];
645
+
646
+ if (options.folder) {
647
+ params.push(`folder_path=${encodeURIComponent(options.folder)}`);
648
+ }
649
+ if (options.page) {
650
+ params.push(`page=${options.page}`);
651
+ }
652
+ if (options.perPage) {
653
+ params.push(`per_page=${options.perPage}`);
654
+ }
655
+
656
+ if (params.length > 0) {
657
+ endpoint += '?' + params.join('&');
658
+ }
659
+
660
+ const response = await makeApiRequest('GET', endpoint, credentials);
661
+
662
+ spinner.succeed(chalk.green(`āœ“ Found ${response.pagination.total} file(s)`));
663
+
664
+ if (response.pagination.total === 0) {
665
+ console.log(chalk.yellow('\nšŸ“­ No files found'));
666
+ console.log(chalk.cyan('\nšŸ’” Upload a file:'));
667
+ console.log(chalk.white(' '), chalk.yellow(`orca storage upload ${bucketName} <file-path>`));
668
+ console.log('');
669
+ return;
670
+ }
671
+
672
+ console.log('');
673
+ console.log(chalk.white('─'.repeat(120)));
674
+ console.log(
675
+ chalk.white('NAME').padEnd(40),
676
+ chalk.white('SIZE').padEnd(15),
677
+ chalk.white('TYPE').padEnd(20),
678
+ chalk.white('DOWNLOADS').padEnd(12),
679
+ chalk.white('UPLOADED')
680
+ );
681
+ console.log(chalk.white('─'.repeat(120)));
682
+
683
+ response.files.forEach(file => {
684
+ const name = (file.file_name.length > 38
685
+ ? file.file_name.substring(0, 35) + '...'
686
+ : file.file_name).padEnd(40);
687
+ const size = file.file_size.padEnd(15);
688
+ const type = (file.mime_type.length > 18
689
+ ? file.mime_type.substring(0, 15) + '...'
690
+ : file.mime_type).padEnd(20);
691
+ const downloads = String(file.download_count).padEnd(12);
692
+ const uploaded = new Date(file.uploaded_at).toLocaleDateString();
693
+
694
+ console.log(
695
+ chalk.yellow(name),
696
+ chalk.cyan(size),
697
+ chalk.gray(type),
698
+ chalk.white(downloads),
699
+ chalk.gray(uploaded)
700
+ );
701
+ });
702
+
703
+ console.log(chalk.white('─'.repeat(120)));
704
+
705
+ if (response.pagination.last_page > 1) {
706
+ console.log(chalk.gray(`Page ${response.pagination.current_page} of ${response.pagination.last_page}`));
707
+ if (response.pagination.current_page < response.pagination.last_page) {
708
+ console.log(chalk.cyan('Next page: '), chalk.yellow(`orca storage files ${bucketName} --page ${response.pagination.current_page + 1}`));
709
+ }
710
+ }
711
+
712
+ console.log('');
713
+
714
+ } catch (error) {
715
+ spinner.fail(chalk.red('āœ— Failed to list files'));
716
+
717
+ if (error.response) {
718
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
719
+ if (error.statusCode === 404) {
720
+ console.log(chalk.yellow(' Bucket not found'));
721
+ }
722
+ } else {
723
+ console.log(chalk.red(`\nāœ— ${error.message}`));
724
+ }
725
+ console.log('');
726
+ process.exit(1);
727
+ }
728
+ }
729
+
730
+ /**
731
+ * File Delete Command
732
+ */
733
+ async function fileDelete(bucketName, fileKey) {
734
+ console.log(chalk.cyan('\n============================================================'));
735
+ console.log(chalk.cyan('šŸ—‘ļø Deleting File'));
736
+ console.log(chalk.cyan('============================================================\n'));
737
+
738
+ const credentials = requireAuth();
739
+
740
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
741
+ console.log(chalk.white('File Key:'), chalk.yellow(fileKey));
742
+
743
+ const spinner = ora('Deleting file...').start();
744
+
745
+ try {
746
+ const encodedFileKey = encodeURIComponent(fileKey);
747
+ const endpoint = API_ENDPOINTS.STORAGE_FILE_DELETE
748
+ .replace('{bucketName}', bucketName)
749
+ .replace('{fileKey}', encodedFileKey);
750
+
751
+ await makeApiRequest('DELETE', endpoint, credentials);
752
+
753
+ spinner.succeed(chalk.green('āœ“ File deleted successfully!'));
754
+ console.log('');
755
+
756
+ } catch (error) {
757
+ spinner.fail(chalk.red('āœ— Failed to delete file'));
758
+
759
+ if (error.response) {
760
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
761
+ if (error.statusCode === 404) {
762
+ console.log(chalk.yellow(' File or bucket not found'));
763
+ }
764
+ } else {
765
+ console.log(chalk.red(`\nāœ— ${error.message}`));
766
+ }
767
+ console.log('');
768
+ process.exit(1);
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Permission Add Command
774
+ */
775
+ async function permissionAdd(bucketName, options = {}) {
776
+ console.log(chalk.cyan('\n============================================================'));
777
+ console.log(chalk.cyan('šŸ” Adding Permission'));
778
+ console.log(chalk.cyan('============================================================\n'));
779
+
780
+ const credentials = requireAuth();
781
+
782
+ console.log(chalk.white('Bucket: '), chalk.yellow(bucketName));
783
+ console.log(chalk.white('Target Type: '), chalk.yellow(options.targetType || 'user'));
784
+ console.log(chalk.white('Target ID: '), chalk.yellow(options.targetId || 'N/A'));
785
+ console.log(chalk.white('Resource Type:'), chalk.yellow(options.resourceType || 'bucket'));
786
+
787
+ const spinner = ora('Adding permission...').start();
788
+
789
+ try {
790
+ const requestBody = {
791
+ target_type: options.targetType || 'user',
792
+ target_id: options.targetId,
793
+ resource_type: options.resourceType || 'bucket',
794
+ resource_path: options.resourcePath || null,
795
+ can_read: options.read || false,
796
+ can_write: options.write || false,
797
+ can_delete: options.delete || false,
798
+ can_list: options.list || false,
799
+ valid_until: options.validUntil || null,
800
+ reason: options.reason || null
801
+ };
802
+
803
+ const endpoint = API_ENDPOINTS.STORAGE_PERMISSION_ADD
804
+ .replace('{bucketName}', bucketName);
805
+
806
+ const response = await makeApiRequest('POST', endpoint, credentials, requestBody);
807
+
808
+ spinner.succeed(chalk.green('āœ“ Permission added successfully!'));
809
+
810
+ console.log(chalk.cyan('\nšŸ” Permission Details:'));
811
+ console.log(chalk.white(' ID: '), chalk.yellow(response.permission.id));
812
+ console.log(chalk.white(' Target: '), chalk.yellow(`${response.permission.target_type}:${response.permission.target_id || 'all'}`));
813
+ console.log(chalk.white(' Resource: '), chalk.yellow(`${response.permission.resource_type}:${response.permission.resource_path || 'all'}`));
814
+ console.log(chalk.white(' Permissions: '), chalk.green(response.permission.permissions.join(', ')));
815
+
816
+ if (response.permission.valid_until) {
817
+ console.log(chalk.white(' Valid Until: '), chalk.gray(new Date(response.permission.valid_until).toLocaleString()));
818
+ }
819
+
820
+ console.log('');
821
+
822
+ } catch (error) {
823
+ spinner.fail(chalk.red('āœ— Failed to add permission'));
824
+
825
+ if (error.response) {
826
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}`));
827
+ if (error.statusCode === 404) {
828
+ console.log(chalk.yellow(' Bucket not found'));
829
+ } else if (error.statusCode === 422) {
830
+ console.log(chalk.yellow(' Invalid permission parameters'));
831
+ }
832
+ } else {
833
+ console.log(chalk.red(`\nāœ— ${error.message}`));
834
+ }
835
+ console.log('');
836
+ process.exit(1);
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Permission List Command
842
+ */
843
+ async function permissionList(bucketName) {
844
+ console.log(chalk.cyan('\n============================================================'));
845
+ console.log(chalk.cyan('šŸ” Listing Permissions'));
846
+ console.log(chalk.cyan('============================================================\n'));
847
+
848
+ const credentials = requireAuth();
849
+ console.log(chalk.white('Bucket:'), chalk.yellow(bucketName));
850
+
851
+ const spinner = ora('Fetching permissions...').start();
852
+
853
+ try {
854
+ const endpoint = API_ENDPOINTS.STORAGE_PERMISSION_LIST
855
+ .replace('{bucketName}', bucketName);
856
+
857
+ const response = await makeApiRequest('GET', endpoint, credentials);
858
+
859
+ spinner.succeed(chalk.green(`āœ“ Found ${response.count} permission(s)`));
860
+
861
+ if (response.count === 0) {
862
+ console.log(chalk.yellow('\nšŸ“­ No permissions found'));
863
+ console.log(chalk.cyan('\nšŸ’” Add a permission:'));
864
+ console.log(chalk.white(' '), chalk.yellow(`orca storage permission add ${bucketName} --target-type user --target-id USER_ID --read`));
865
+ console.log('');
866
+ return;
867
+ }
868
+
869
+ console.log('');
870
+ response.permissions.forEach(perm => {
871
+ const statusIcon = perm.is_valid ? 'āœ“' : 'āœ—';
872
+ const statusColor = perm.is_valid ? chalk.green : chalk.red;
873
+
874
+ console.log(statusColor(`${statusIcon} Permission #${perm.id}`));
875
+ console.log(chalk.white(' Target: '), chalk.yellow(`${perm.target_type}:${perm.target_id || 'all'}`));
876
+ console.log(chalk.white(' Resource: '), chalk.yellow(`${perm.resource_type}:${perm.resource_path || 'all'}`));
877
+ console.log(chalk.white(' Actions: '), chalk.green(perm.permissions.join(', ')));
878
+ console.log(chalk.white(' Status: '), statusColor(perm.status));
879
+
880
+ if (perm.valid_until) {
881
+ console.log(chalk.white(' Valid Until:'), chalk.gray(new Date(perm.valid_until).toLocaleString()));
882
+ }
883
+
884
+ console.log('');
885
+ });
886
+
887
+ } catch (error) {
888
+ spinner.fail(chalk.red('āœ— Failed to list permissions'));
889
+
890
+ if (error.response) {
891
+ console.log(chalk.red(`\nāœ— ${error.response.message || 'Unknown error'}\n`));
892
+ } else {
893
+ console.log(chalk.red(`\nāœ— ${error.message}\n`));
894
+ }
895
+ process.exit(1);
896
+ }
897
+ }
898
+
899
+ module.exports = {
900
+ bucketCreate,
901
+ bucketList,
902
+ bucketInfo,
903
+ bucketDelete,
904
+ fileUpload,
905
+ fileDownload,
906
+ fileList,
907
+ fileDelete,
908
+ permissionAdd,
909
+ permissionList
910
+ };
911
+