mbkbucket 3.3.0 → 3.3.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.
Files changed (39) hide show
  1. package/index.js +8 -18
  2. package/lib/cli.js +642 -0
  3. package/{public → lib/public}/bucketadmin.js +1 -1
  4. package/{src → lib/src}/app.js +37 -50
  5. package/{src → lib/src}/cli/auth-helper.js +24 -103
  6. package/lib/src/config/index.js +108 -0
  7. package/{src → lib/src}/config/validation.js +14 -30
  8. package/{src → lib/src}/controllers/api.controller.js +85 -176
  9. package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
  10. package/{src → lib/src}/controllers/info.controller.js +17 -20
  11. package/{src → lib/src}/controllers/view.controller.js +23 -70
  12. package/{src → lib/src}/middleware/async-handler.js +1 -16
  13. package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
  14. package/{src → lib/src}/middleware/error-handler.js +6 -14
  15. package/{src → lib/src}/middleware/rate-limiter.js +2 -10
  16. package/{src → lib/src}/middleware/security.js +1 -17
  17. package/lib/src/routes/api.routes.js +40 -0
  18. package/{src → lib/src}/routes/index.js +1 -9
  19. package/{src → lib/src}/routes/info.routes.js +2 -3
  20. package/{src → lib/src}/routes/view.routes.js +3 -8
  21. package/{src → lib/src}/services/s3.service.js +157 -349
  22. package/lib/src/utils/errors.js +45 -0
  23. package/lib/src/utils/helpers.js +251 -0
  24. package/package.json +18 -3
  25. package/.github/PACKAGE.md +0 -19
  26. package/.github/workflows/publish.yml +0 -88
  27. package/cli.js +0 -977
  28. package/index.d.ts +0 -444
  29. package/lib/index.js +0 -17
  30. package/src/config/index.js +0 -133
  31. package/src/routes/api.routes.js +0 -66
  32. package/src/utils/errors.js +0 -76
  33. package/src/utils/helpers.js +0 -197
  34. package/test/config-validation.test.js +0 -116
  35. /package/{public → lib/public}/bucketadmin.css +0 -0
  36. /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
  37. /package/{public → lib/public}/mbkbucket-helper.css +0 -0
  38. /package/{public → lib/public}/mbkbucket-helper.js +0 -0
  39. /package/{src → lib/src}/utils/logger.js +0 -0
package/cli.js DELETED
@@ -1,977 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * mbkbucket CLI — Manage S3/R2 buckets from the command line.
5
- *
6
- * mbkbucket login --server <url> Authenticate via device flow
7
- * mbkbucket logout Clear stored credentials
8
- * mbkbucket whoami Show login status
9
- * mbkbucket list [prefix] List files and folders
10
- * mbkbucket upload <file> [key] Upload a file
11
- * mbkbucket download <key> [dest] Download a file
12
- * mbkbucket delete <key> Delete a file
13
- * mbkbucket delete-folder <prefix> Recursively delete a folder
14
- * mbkbucket info <key> Show file metadata
15
- * mbkbucket signed-url <key> Generate a pre-signed URL
16
- *
17
- * Before running, set these env vars (or use a .env file in cwd):
18
- * BucketConnection={"bucketname":{...}}
19
- * APP_NAME=your-app-name
20
- */
21
-
22
- import dotenv from 'dotenv';
23
- import path from 'node:path';
24
- import fs from 'node:fs';
25
- import os from 'node:os';
26
-
27
- // Load .env from cwd first so S3 service can pick up BucketConnection etc.
28
- dotenv.config();
29
-
30
- // ---------------------------------------------------------------------------
31
- // Parse CLI arguments
32
- // ---------------------------------------------------------------------------
33
- const args = process.argv.slice(2);
34
-
35
- function findFlag(name) {
36
- const long = `--${name}`;
37
- const short = `-${name[0]}`;
38
- for (let i = 0; i < args.length; i++) {
39
- if (args[i] === long || args[i] === short) {
40
- return args[i + 1] !== undefined && !args[i + 1].startsWith('-') ? args[i + 1] : true;
41
- }
42
- }
43
- return undefined;
44
- }
45
-
46
- function hasFlag(name) {
47
- return args.includes(`--${name}`) || args.includes(`-${name[0]}`);
48
- }
49
-
50
- function getPositionalArgs() {
51
- const positional = [];
52
- for (let i = 0; i < args.length; i++) {
53
- if (args[i].startsWith('--') || args[i].startsWith('-')) {
54
- // Skip flag and its value
55
- if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
56
- const long = args[i].replace(/^-+/, '');
57
- // Only skip value for flags that take one (not boolean flags like --help)
58
- if (['server', 'app', 'bucket', 'profile-key', 'expires'].includes(long)) {
59
- i++;
60
- }
61
- }
62
- } else {
63
- positional.push(args[i]);
64
- }
65
- }
66
- return positional;
67
- }
68
-
69
- const command = getPositionalArgs()[0] || 'help';
70
- const cmdArgs = getPositionalArgs().slice(1);
71
-
72
- // ---------------------------------------------------------------------------
73
- // CLI Auth helpers (lazy-loaded because they don't import S3)
74
- // ---------------------------------------------------------------------------
75
- import {
76
- readConfig,
77
- clearConfig,
78
- updateConfig,
79
- writeConfig,
80
- isLoggedIn,
81
- deviceFlowLogin,
82
- CONFIG_FILE,
83
- CONFIG_DIR,
84
- } from './src/cli/auth-helper.js';
85
-
86
- // ---------------------------------------------------------------------------
87
- // Help text
88
- // ---------------------------------------------------------------------------
89
- function showHelp() {
90
- console.log(`
91
- mbkbucket — S3/R2 bucket CLI
92
-
93
- USAGE:
94
- mbkbucket <command> [options] [arguments]
95
-
96
- COMMANDS:
97
- login Authenticate with mbkauthe device flow
98
- logout Clear stored credentials
99
- whoami Show current login status
100
-
101
- config Show all config values
102
- config get <key> Get a specific config value
103
- config set <k> <v> Set a config value
104
- config unset <key> Remove a config value
105
- config path Show config file path
106
- config reset Reset to demo values
107
- config edit Open config file in editor
108
-
109
- list [prefix] List files and folders
110
- upload <file> [key] Upload a file or folder
111
- download <key> [dest] Download a file or folder
112
- delete <key> Delete a file
113
- delete-folder <prefix> Recursively delete a folder
114
- info <key> Show file metadata
115
- signed-url <key> Generate a pre-signed download URL
116
-
117
- GLOBAL OPTIONS:
118
- --app, -a <name> Override APP_NAME for key prefixing
119
- --bucket, -b <name> Override default bucket name
120
- --help, -h Show this help
121
-
122
- LOGIN OPTIONS:
123
- --server <url> mbkauthe server URL (required for login)
124
- --profile-key <k> API token profile key
125
-
126
- SIGNED-URL OPTIONS:
127
- --expires <sec> Expiration in seconds (default: 3600)
128
-
129
- ENVIRONMENT:
130
- BucketConnection JSON mapping of bucket name → S3 credentials
131
- APP_NAME Application name for key prefix isolation
132
- PORT Server port (default: 3004)
133
- `);
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // S3 helper — conditionally imports S3 service after env is set up
138
- // ---------------------------------------------------------------------------
139
- let _s3Module = null;
140
- async function getS3() {
141
- if (_s3Module) return _s3Module;
142
- // Set APP_NAME from CLI flag before importing
143
- const appFlag = findFlag('app');
144
- if (appFlag && typeof appFlag === 'string') {
145
- process.env.APP_NAME = appFlag;
146
- }
147
- _s3Module = await import('./src/services/s3.service.js');
148
- return _s3Module;
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // Formatting helpers
153
- // ---------------------------------------------------------------------------
154
- function formatBytes(bytes) {
155
- if (!bytes || bytes === 0) return '0 B';
156
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
157
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
158
- return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
159
- }
160
-
161
- function formatDate(iso) {
162
- try {
163
- return new Date(iso).toLocaleString();
164
- } catch {
165
- return iso || '—';
166
- }
167
- }
168
-
169
- // ---------------------------------------------------------------------------
170
- // Command implementations
171
- // ---------------------------------------------------------------------------
172
-
173
- async function cmdLogin() {
174
- // Use --server flag, or fall back to stored config
175
- let serverUrl = findFlag('server');
176
- const storedCfg = readConfig();
177
-
178
- if (!serverUrl || typeof serverUrl !== 'string') {
179
- if (storedCfg.serverUrl) {
180
- serverUrl = storedCfg.serverUrl;
181
- console.log(`ℹ️ Using stored server: ${serverUrl}`);
182
- } else {
183
- console.error('❌ --server <url> is required for login.');
184
- console.error(' Example: mbkbucket login --server https://myapp.example.com');
185
- console.error('');
186
- console.error('💡 Tip: Set it once with:');
187
- console.error(' mbkbucket config set serverUrl https://your-server.com');
188
- console.error(' Then just run: mbkbucket login');
189
- process.exit(1);
190
- }
191
- }
192
-
193
- // Use --profile-key flag, or fall back to stored config
194
- let profileKey = findFlag('profile-key');
195
- if (!profileKey && storedCfg.profileKey) {
196
- profileKey = storedCfg.profileKey;
197
- }
198
-
199
- // Always save the server URL so the user doesn't have to retype it
200
- updateConfig({ serverUrl });
201
-
202
- try {
203
- const result = await deviceFlowLogin({
204
- serverUrl,
205
- profileKey: typeof profileKey === 'string' ? profileKey : undefined,
206
- });
207
-
208
- updateConfig({
209
- token: result.token,
210
- tokenPrefix: result.tokenPrefix,
211
- username: result.username,
212
- serverUrl,
213
- profile: result.profile || null,
214
- loggedInAt: new Date().toISOString(),
215
- });
216
-
217
- console.log('');
218
- console.log('✅ Credentials saved. You are now logged in.');
219
- console.log(` Run "mbkbucket whoami" to verify.`);
220
- } catch (err) {
221
- const msg = err.message || '';
222
- console.error(`❌ Login failed: ${msg}`);
223
-
224
- // Give actionable guidance based on the error
225
- if (msg.includes('profileKey') || msg.includes('profileId')) {
226
- console.error('');
227
- console.error('💡 An API token profile is required. You need to:');
228
- console.error(' 1. Create an API token profile in the mbkauthe admin panel');
229
- console.error(' 2. Run: mbkbucket login --profile-key <your-profile-key>');
230
- console.error('');
231
- console.error(' Or save it for future use:');
232
- console.error(' mbkbucket config set profileKey <your-profile-key>');
233
- } else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
234
- console.error('');
235
- console.error('💡 Could not reach the server. Check:');
236
- console.error(' 1. Is the server running?');
237
- console.error(` 2. Is "${serverUrl}" the correct URL?`);
238
- console.error(' 3. Is CLI_AUTH_ENABLED=true on the server?');
239
- }
240
-
241
- console.error('');
242
- console.error(` Server URL saved. Run "mbkbucket login" to retry.`);
243
- process.exit(1);
244
- }
245
- }
246
-
247
- function cmdLogout() {
248
- if (!isLoggedIn()) {
249
- console.log('ℹ️ Not logged in.');
250
- return;
251
- }
252
- const cfg = readConfig();
253
- clearConfig();
254
- console.log(`👋 Logged out. Goodbye, ${cfg.username || 'user'}!`);
255
- }
256
-
257
- function cmdWhoami() {
258
- if (!isLoggedIn()) {
259
- console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
260
- return;
261
- }
262
- const cfg = readConfig();
263
- console.log(`✅ Logged in as: ${cfg.username}`);
264
- console.log(` Server: ${cfg.serverUrl}`);
265
- console.log(` Token: ${cfg.tokenPrefix}...`);
266
- if (cfg.profile) {
267
- console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
268
- }
269
- console.log(` Since: ${formatDate(cfg.loggedInAt)}`);
270
- }
271
-
272
- async function cmdList() {
273
- const prefix = cmdArgs[0] || '';
274
- const bucketName = findFlag('bucket');
275
-
276
- try {
277
- const s3 = await getS3();
278
- const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
279
-
280
- if (!result.Contents || result.Contents.length === 0) {
281
- console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
282
- return;
283
- }
284
-
285
- console.log('');
286
- console.log(`📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):`);
287
- console.log('─'.repeat(80));
288
-
289
- // Show CommonPrefixes (folders) first
290
- if (result.CommonPrefixes && result.CommonPrefixes.length > 0) {
291
- for (const cp of result.CommonPrefixes) {
292
- const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
293
- console.log(` 📂 ${folderName}/`);
294
- }
295
- if (result.Contents.length > 0) console.log('');
296
- }
297
-
298
- for (const obj of result.Contents) {
299
- const name = obj.Key || '';
300
- const size = formatBytes(obj.Size);
301
- const modified = formatDate(obj.LastModified);
302
- const displayName = name.length > 60 ? '...' + name.slice(-57) : name;
303
- console.log(` ${displayName.padEnd(62)} ${size.padStart(10)} ${modified}`);
304
- }
305
-
306
- console.log('─'.repeat(80));
307
- if (result.hasMore) {
308
- console.log(` ... more files available (use --prefix to narrow down)`);
309
- }
310
- console.log('');
311
- } catch (err) {
312
- console.error(`❌ List failed: ${err.message}`);
313
- process.exit(1);
314
- }
315
- }
316
-
317
- async function cmdUpload() {
318
- const filePath = cmdArgs[0];
319
- if (!filePath) {
320
- console.error('❌ Usage: mbkbucket upload <file> [key]');
321
- console.error(' Example: mbkbucket upload ./photo.jpg');
322
- console.error(' Example: mbkbucket upload ./photo.jpg images/photo.jpg');
323
- process.exit(1);
324
- }
325
-
326
- const resolvedPath = path.resolve(filePath);
327
- const isDir = fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory();
328
-
329
- if (isDir) {
330
- await uploadFolder(resolvedPath);
331
- } else {
332
- await uploadSingleFile(resolvedPath);
333
- }
334
- }
335
-
336
- async function uploadSingleFile(filePath) {
337
- const bucketName = findFlag('bucket');
338
- const customKey = cmdArgs[1] || null;
339
-
340
- if (!fs.existsSync(filePath)) {
341
- console.error(`❌ File not found: ${filePath}`);
342
- process.exit(1);
343
- }
344
-
345
- const stat = fs.statSync(filePath);
346
- const fileName = path.basename(filePath);
347
- const key = customKey || fileName;
348
-
349
- // Determine MIME type from extension
350
- const ext = path.extname(fileName).slice(1).toLowerCase();
351
- const mimeMap = {
352
- 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
353
- 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
354
- 'mp4': 'video/mp4', 'webm': 'video/webm', 'mp3': 'audio/mpeg',
355
- 'wav': 'audio/wav', 'pdf': 'application/pdf', 'json': 'application/json',
356
- 'js': 'application/javascript', 'css': 'text/css', 'html': 'text/html',
357
- 'txt': 'text/plain', 'md': 'text/markdown', 'csv': 'text/csv',
358
- 'zip': 'application/zip', 'gz': 'application/gzip',
359
- };
360
- const contentType = mimeMap[ext] || 'application/octet-stream';
361
-
362
- console.log(`⬆️ Uploading: ${fileName} (${formatBytes(stat.size)}) → ${key}`);
363
-
364
- try {
365
- const s3 = await getS3();
366
- const buffer = fs.readFileSync(filePath);
367
- const result = await s3.uploadFile(key, buffer, contentType, { bucketName });
368
- console.log(`✅ Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
369
- } catch (err) {
370
- console.error(`❌ Upload failed: ${err.message}`);
371
- process.exit(1);
372
- }
373
- }
374
-
375
- async function uploadFolder(folderPath) {
376
- const bucketName = findFlag('bucket');
377
- const prefix = cmdArgs[1] || path.basename(folderPath);
378
-
379
- // Recursively collect all files
380
- const files = walkDir(folderPath);
381
-
382
- if (files.length === 0) {
383
- console.log('📭 No files found in folder.');
384
- return;
385
- }
386
-
387
- console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/`);
388
- console.log('');
389
-
390
- let uploaded = 0;
391
- let failed = 0;
392
-
393
- for (const file of files) {
394
- const relativePath = path.relative(folderPath, file).replace(/\\/g, '/');
395
- const key = `${prefix}/${relativePath}`;
396
- const stat = fs.statSync(file);
397
- const ext = path.extname(file).slice(1).toLowerCase();
398
- const mimeMap = {
399
- 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
400
- 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
401
- };
402
- const contentType = mimeMap[ext] || 'application/octet-stream';
403
-
404
- try {
405
- const s3 = await getS3();
406
- const buffer = fs.readFileSync(file);
407
- await s3.uploadFile(key, buffer, contentType, { bucketName });
408
- console.log(` ✅ ${relativePath}`);
409
- uploaded++;
410
- } catch (err) {
411
- console.error(` ❌ ${relativePath}: ${err.message}`);
412
- failed++;
413
- }
414
- }
415
-
416
- console.log('');
417
- console.log(`Done: ${uploaded} uploaded, ${failed} failed.`);
418
- if (failed > 0) process.exit(1);
419
- }
420
-
421
- function walkDir(dir) {
422
- const results = [];
423
- const list = fs.readdirSync(dir);
424
- for (const item of list) {
425
- const fullPath = path.join(dir, item);
426
- if (fs.statSync(fullPath).isDirectory()) {
427
- results.push(...walkDir(fullPath));
428
- } else {
429
- results.push(fullPath);
430
- }
431
- }
432
- return results;
433
- }
434
-
435
- async function cmdDownload() {
436
- const key = cmdArgs[0];
437
- if (!key) {
438
- console.error('❌ Usage: mbkbucket download <key> [destination]');
439
- process.exit(1);
440
- }
441
-
442
- const dest = cmdArgs[1] || path.basename(key);
443
- const bucketName = findFlag('bucket');
444
-
445
- try {
446
- const s3 = await getS3();
447
-
448
- // Check if it's a folder (key ends with / or has children)
449
- const isFolderLike = key.endsWith('/');
450
- if (isFolderLike) {
451
- await downloadFolder(s3, key, dest, bucketName);
452
- return;
453
- }
454
-
455
- // Single file download
456
- console.log(`⬇️ Downloading: ${key} → ${dest}`);
457
- const result = await s3.downloadFile(key, { bucketName });
458
-
459
- if (result.notModified) {
460
- console.log('ℹ️ File not modified.');
461
- return;
462
- }
463
-
464
- // Read the body stream
465
- const body = await streamToBuffer(result.Body);
466
- const destDir = path.dirname(path.resolve(dest));
467
- if (!fs.existsSync(destDir)) {
468
- fs.mkdirSync(destDir, { recursive: true });
469
- }
470
- fs.writeFileSync(path.resolve(dest), body);
471
-
472
- console.log(`✅ Downloaded: ${dest} (${formatBytes(body.length)})`);
473
- } catch (err) {
474
- console.error(`❌ Download failed: ${err.message}`);
475
- process.exit(1);
476
- }
477
- }
478
-
479
- async function downloadFolder(s3, prefix, destDir, bucketName) {
480
- // List all files under the prefix
481
- const allFiles = [];
482
- let token = null;
483
- const resolvedDest = path.resolve(destDir);
484
-
485
- do {
486
- const result = await s3.listfiles(prefix, { bucketName, continuationToken: token });
487
- if (result.Contents) {
488
- for (const obj of result.Contents) {
489
- if (obj.Key && obj.Key !== prefix) {
490
- allFiles.push(obj.Key);
491
- }
492
- }
493
- }
494
- token = result.IsTruncated ? result.NextContinuationToken : null;
495
- } while (token);
496
-
497
- if (allFiles.length === 0) {
498
- console.log(`📭 No files found with prefix "${prefix}".`);
499
- return;
500
- }
501
-
502
- console.log(`📁 Downloading ${allFiles.length} files to ${resolvedDest}/`);
503
- console.log('');
504
-
505
- let downloaded = 0;
506
- let failed = 0;
507
-
508
- for (const fileKey of allFiles) {
509
- const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
510
- const filePath = path.join(resolvedDest, relativePath);
511
-
512
- try {
513
- const result = await s3.downloadFile(fileKey, { bucketName });
514
- if (!result.notModified) {
515
- const body = await streamToBuffer(result.Body);
516
- const fileDir = path.dirname(filePath);
517
- if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
518
- fs.writeFileSync(filePath, body);
519
- }
520
- console.log(` ✅ ${relativePath}`);
521
- downloaded++;
522
- } catch (err) {
523
- console.error(` ❌ ${relativePath}: ${err.message}`);
524
- failed++;
525
- }
526
- }
527
-
528
- console.log('');
529
- console.log(`Done: ${downloaded} downloaded, ${failed} failed.`);
530
- if (failed > 0) process.exit(1);
531
- }
532
-
533
- async function streamToBuffer(stream) {
534
- if (Buffer.isBuffer(stream)) return stream;
535
- if (stream?.transformToWeb) {
536
- // AWS SDK v3 sometimes returns Node.js Readable
537
- const chunks = [];
538
- for await (const chunk of stream) {
539
- chunks.push(chunk);
540
- }
541
- return Buffer.concat(chunks);
542
- }
543
- // Web ReadableStream
544
- const reader = stream.getReader();
545
- const chunks = [];
546
- while (true) {
547
- const { done, value } = await reader.read();
548
- if (done) break;
549
- chunks.push(value);
550
- }
551
- return Buffer.concat(chunks);
552
- }
553
-
554
- async function cmdDelete() {
555
- const key = cmdArgs[0];
556
- if (!key) {
557
- console.error('❌ Usage: mbkbucket delete <key>');
558
- process.exit(1);
559
- }
560
-
561
- const bucketName = findFlag('bucket');
562
-
563
- try {
564
- const s3 = await getS3();
565
- console.log(`🗑️ Deleting: ${key}`);
566
- const result = await s3.deleteFile(key, bucketName);
567
- console.log(`✅ Deleted: ${result.key}`);
568
- } catch (err) {
569
- console.error(`❌ Delete failed: ${err.message}`);
570
- process.exit(1);
571
- }
572
- }
573
-
574
- async function cmdDeleteFolder() {
575
- const prefix = cmdArgs[0];
576
- if (!prefix) {
577
- console.error('❌ Usage: mbkbucket delete-folder <prefix>');
578
- process.exit(1);
579
- }
580
-
581
- const bucketName = findFlag('bucket');
582
- const cleanedPrefix = String(prefix).replace(/^\/+/, '').replace(/\/+$/, '');
583
-
584
- console.log(`⚠️ This will delete ALL files under "${cleanedPrefix}/".`);
585
- console.log(` Are you sure? Type "yes" to confirm:`);
586
-
587
- // Simple confirmation
588
- const confirmed = await promptLine();
589
- if (confirmed.trim().toLowerCase() !== 'yes') {
590
- console.log('Cancelled.');
591
- return;
592
- }
593
-
594
- try {
595
- const s3 = await getS3();
596
- console.log(`🗑️ Deleting folder: ${cleanedPrefix}/`);
597
- const result = await s3.deleteFolder(cleanedPrefix, bucketName);
598
- console.log(`✅ Deleted ${result.deletedCount} file(s) under "${cleanedPrefix}/".`);
599
- } catch (err) {
600
- console.error(`❌ Delete folder failed: ${err.message}`);
601
- process.exit(1);
602
- }
603
- }
604
-
605
- async function cmdInfo() {
606
- const key = cmdArgs[0];
607
- if (!key) {
608
- console.error('❌ Usage: mbkbucket info <key>');
609
- process.exit(1);
610
- }
611
-
612
- const bucketName = findFlag('bucket');
613
-
614
- try {
615
- const s3 = await getS3();
616
- const meta = await s3.getFileMetadata(key, bucketName);
617
-
618
- if (!meta.exists) {
619
- console.log(`❌ File not found: ${key}`);
620
- process.exit(1);
621
- }
622
-
623
- console.log('');
624
- console.log(`📄 ${meta.key}`);
625
- console.log('─'.repeat(60));
626
- console.log(` Size: ${formatBytes(meta.ContentLength)}`);
627
- console.log(` Type: ${meta.ContentType || 'unknown'}`);
628
- console.log(` Last Modified: ${formatDate(meta.LastModified)}`);
629
- console.log(` ETag: ${meta.ETag || '—'}`);
630
- console.log(` Cache-Control: ${meta.CacheControl || '—'}`);
631
- if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
632
- console.log(' Metadata:');
633
- for (const [k, v] of Object.entries(meta.Metadata)) {
634
- console.log(` ${k}: ${v}`);
635
- }
636
- }
637
- console.log('');
638
- } catch (err) {
639
- console.error(`❌ Info failed: ${err.message}`);
640
- process.exit(1);
641
- }
642
- }
643
-
644
- // ---------------------------------------------------------------------------
645
- // Config command
646
- // ---------------------------------------------------------------------------
647
-
648
- const DEMO_CONFIG = {
649
- token: 'mbk_demo_token_replace_with_real_one_from_login',
650
- tokenPrefix: 'mbk_demo',
651
- username: 'demo-user',
652
- serverUrl: 'https://your-app.example.com',
653
- profile: {
654
- name: 'Default CLI Profile',
655
- scope: 'read-write',
656
- allowedApps: ['mbkbucket'],
657
- },
658
- loggedInAt: new Date().toISOString(),
659
- };
660
-
661
- const VALID_CONFIG_KEYS = ['token', 'tokenPrefix', 'username', 'serverUrl', 'profile', 'profileKey', 'loggedInAt'];
662
-
663
- async function cmdConfig() {
664
- const sub = cmdArgs[0];
665
- const key = cmdArgs[1];
666
- const value = cmdArgs[2];
667
-
668
- switch (sub) {
669
- case 'get':
670
- return cmdConfigGet(key);
671
- case 'set':
672
- return cmdConfigSet(key, value);
673
- case 'unset':
674
- case 'del':
675
- case 'delete':
676
- return cmdConfigUnset(key);
677
- case 'path':
678
- return cmdConfigPath();
679
- case 'reset':
680
- return cmdConfigReset();
681
- case 'edit':
682
- await cmdConfigEdit();
683
- break;
684
- default:
685
- cmdConfigShow();
686
- break;
687
- }
688
- }
689
-
690
- function cmdConfigShow() {
691
- const cfg = readConfig();
692
- const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
693
-
694
- if (entries.length === 0) {
695
- console.log('📭 No config values set.');
696
- console.log(` File: ${CONFIG_FILE}`);
697
- console.log(' Run "mbkbucket config reset" to populate demo values.');
698
- return;
699
- }
700
-
701
- console.log('');
702
- console.log(`📋 Config (${CONFIG_FILE}):`);
703
- console.log('─'.repeat(60));
704
-
705
- for (const k of entries.sort()) {
706
- const v = cfg[k];
707
- const display = typeof v === 'object' && v !== null
708
- ? JSON.stringify(v)
709
- : String(v);
710
-
711
- // Mask the token for security
712
- if (k === 'token' && display.length > 12) {
713
- console.log(` ${k.padEnd(14)} ${display.slice(0, 8)}...${display.slice(-4)}`);
714
- } else {
715
- const truncated = display.length > 55 ? display.slice(0, 52) + '...' : display;
716
- console.log(` ${k.padEnd(14)} ${truncated}`);
717
- }
718
- }
719
-
720
- console.log('─'.repeat(60));
721
- console.log(' Run "mbkbucket config set <key> <value>" to change a value.');
722
- console.log(' Run "mbkbucket config edit" to open in your editor.');
723
- console.log('');
724
- }
725
-
726
- function cmdConfigGet(key) {
727
- if (!key) {
728
- console.error('❌ Usage: mbkbucket config get <key>');
729
- console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
730
- process.exit(1);
731
- }
732
-
733
- const cfg = readConfig();
734
- if (!(key in cfg)) {
735
- console.error(`❌ Key not found: ${key}`);
736
- console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
737
- process.exit(1);
738
- }
739
-
740
- const v = cfg[key];
741
- if (typeof v === 'object' && v !== null) {
742
- console.log(JSON.stringify(v, null, 2));
743
- } else {
744
- console.log(String(v));
745
- }
746
- }
747
-
748
- function cmdConfigSet(key, value) {
749
- if (!key || value === undefined) {
750
- console.error('❌ Usage: mbkbucket config set <key> <value>');
751
- console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
752
- console.error(' Example: mbkbucket config set serverUrl https://myapp.example.com');
753
- process.exit(1);
754
- }
755
-
756
- if (!VALID_CONFIG_KEYS.includes(key)) {
757
- console.error(`❌ Invalid key: ${key}`);
758
- console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
759
- process.exit(1);
760
- }
761
-
762
- // Parse JSON if it looks like an object/array
763
- let parsed = value;
764
- if ((value.startsWith('{') || value.startsWith('[')) &&
765
- (value.endsWith('}') || value.endsWith(']'))) {
766
- try {
767
- parsed = JSON.parse(value);
768
- } catch {
769
- // Keep as string if parse fails
770
- }
771
- }
772
-
773
- updateConfig({ [key]: parsed });
774
- console.log(`✅ Set ${key} = ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}`);
775
- }
776
-
777
- function cmdConfigUnset(key) {
778
- if (!key) {
779
- console.error('❌ Usage: mbkbucket config unset <key>');
780
- console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
781
- process.exit(1);
782
- }
783
-
784
- const cfg = readConfig();
785
- if (!(key in cfg)) {
786
- console.error(`❌ Key not found: ${key}`);
787
- process.exit(1);
788
- }
789
-
790
- delete cfg[key];
791
- writeConfig(cfg);
792
- console.log(`🗑️ Removed: ${key}`);
793
- }
794
-
795
- function cmdConfigPath() {
796
- console.log(`📁 Config directory: ${CONFIG_DIR}`);
797
- console.log(`📄 Config file: ${CONFIG_FILE}`);
798
- if (fs.existsSync(CONFIG_FILE)) {
799
- const stat = fs.statSync(CONFIG_FILE);
800
- console.log(` Size: ${formatBytes(stat.size)}`);
801
- console.log(` Modified: ${formatDate(stat.mtime.toISOString())}`);
802
- }
803
- if (!isLoggedIn()) {
804
- console.log('');
805
- console.log('💡 Tip: Run "mbkbucket login --server <url>" to authenticate.');
806
- }
807
- }
808
-
809
- async function cmdConfigReset() {
810
- console.log('⚠️ This will overwrite your current config with demo values.');
811
- console.log(' Are you sure? Type "yes" to confirm:');
812
-
813
- const confirmed = await promptLine();
814
- if (confirmed.trim().toLowerCase() !== 'yes') {
815
- console.log('Cancelled.');
816
- return;
817
- }
818
-
819
- writeConfig({ ...DEMO_CONFIG });
820
- console.log('✅ Config reset to demo values.');
821
- console.log(' Run "mbkbucket config" to view.');
822
- }
823
-
824
- async function cmdConfigEdit() {
825
- const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
826
-
827
- if (!fs.existsSync(CONFIG_DIR)) {
828
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
829
- }
830
- if (!fs.existsSync(CONFIG_FILE)) {
831
- writeConfig({ ...DEMO_CONFIG });
832
- }
833
-
834
- console.log(`✏️ Opening ${CONFIG_FILE} with ${editor}...`);
835
-
836
- const { spawn } = await import('node:child_process');
837
- const child = spawn(editor, [CONFIG_FILE], {
838
- stdio: 'inherit',
839
- shell: true,
840
- });
841
-
842
- return new Promise((resolve) => {
843
- child.on('exit', (code) => {
844
- if (code === 0) {
845
- console.log('✅ Editor closed.');
846
- } else {
847
- console.error(`⚠️ Editor exited with code ${code}.`);
848
- }
849
- resolve();
850
- });
851
-
852
- child.on('error', (err) => {
853
- console.error(`❌ Could not open editor (${editor}): ${err.message}`);
854
- console.error(' Set the EDITOR environment variable to your preferred editor.');
855
- console.error(` Config file location: ${CONFIG_FILE}`);
856
- resolve();
857
- });
858
- });
859
- }
860
-
861
- async function cmdSignedUrl() {
862
- const key = cmdArgs[0];
863
- if (!key) {
864
- console.error('❌ Usage: mbkbucket signed-url <key> [--expires <seconds>]');
865
- process.exit(1);
866
- }
867
-
868
- const bucketName = findFlag('bucket');
869
- const expires = parseInt(findFlag('expires'), 10) || 3600;
870
-
871
- try {
872
- const s3 = await getS3();
873
- console.log(`🔗 Generating signed URL (expires in ${expires}s)...`);
874
- const result = await s3.generateSignedUrl(key, 'getObject', expires, bucketName);
875
- console.log('');
876
- console.log(result.url || result);
877
- } catch (err) {
878
- console.error(`❌ Signed URL failed: ${err.message}`);
879
- process.exit(1);
880
- }
881
- }
882
-
883
- // ---------------------------------------------------------------------------
884
- // Prompt helper
885
- // ---------------------------------------------------------------------------
886
- function promptLine() {
887
- return new Promise((resolve) => {
888
- const { stdin, stdout } = process;
889
- stdin.resume();
890
- stdout.write('> ');
891
- stdin.once('data', (data) => {
892
- stdin.pause();
893
- resolve(data.toString().trim());
894
- });
895
- });
896
- }
897
-
898
- // ---------------------------------------------------------------------------
899
- // Command router
900
- // ---------------------------------------------------------------------------
901
- async function main() {
902
- // Global flags
903
- if (hasFlag('help')) {
904
- showHelp();
905
- return;
906
- }
907
-
908
- // Set APP_NAME from flag before any S3 import
909
- const appFlag = findFlag('app');
910
- if (appFlag && typeof appFlag === 'string') {
911
- process.env.APP_NAME = appFlag;
912
- }
913
-
914
- const bucketFlag = findFlag('bucket');
915
- if (bucketFlag && typeof bucketFlag === 'string') {
916
- process.env.MBKAUTHE_BUCKET = bucketFlag;
917
- }
918
-
919
- switch (command) {
920
- case 'login':
921
- await cmdLogin();
922
- break;
923
- case 'logout':
924
- cmdLogout();
925
- break;
926
- case 'whoami':
927
- cmdWhoami();
928
- break;
929
- case 'list':
930
- case 'ls':
931
- await cmdList();
932
- break;
933
- case 'upload':
934
- case 'up':
935
- await cmdUpload();
936
- break;
937
- case 'download':
938
- case 'dl':
939
- await cmdDownload();
940
- break;
941
- case 'delete':
942
- case 'rm':
943
- await cmdDelete();
944
- break;
945
- case 'delete-folder':
946
- case 'rmdir':
947
- await cmdDeleteFolder();
948
- break;
949
- case 'info':
950
- case 'stat':
951
- await cmdInfo();
952
- break;
953
- case 'signed-url':
954
- case 'sign':
955
- await cmdSignedUrl();
956
- break;
957
- case 'config':
958
- case 'cfg':
959
- await cmdConfig();
960
- break;
961
- case 'help':
962
- case '--help':
963
- case '-h':
964
- showHelp();
965
- break;
966
- default:
967
- console.error(`❌ Unknown command: ${command}`);
968
- console.error(' Run "mbkbucket --help" for usage information.');
969
- process.exit(1);
970
- }
971
- }
972
-
973
- main().catch((err) => {
974
- console.error(`❌ Unexpected error: ${err.message}`);
975
- if (process.env.DEBUG) console.error(err);
976
- process.exit(1);
977
- });