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