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/index.js CHANGED
@@ -1,34 +1,24 @@
1
1
  import dotenv from "dotenv";
2
- import { createApp } from "./src/app.js";
3
- import { checkVersion } from "./src/config/index.js";
4
- import { runHealthCheck } from "./src/services/s3.service.js";
5
- import { createLogger } from "./src/utils/logger.js";
2
+ import { createApp } from "./lib/src/app.js";
3
+ import { checkVersion } from "./lib/src/config/index.js";
4
+ import { runHealthCheck } from "./lib/src/services/s3.service.js";
5
+ import { createLogger } from "#logger";
6
6
 
7
7
  dotenv.config();
8
8
 
9
9
  const debugServer = createLogger('server');
10
10
  const port = process.env.PORT || 3004;
11
- const isDevMode = process.env.NODE_ENV === "dev";
12
-
13
- // Create and start the Express application
14
11
  const server = createApp();
15
12
 
16
- if (isDevMode) {
17
-
18
- // Check for package updates and S3 connectivity on startup
13
+ if (process.env.NODE_ENV === "dev") {
19
14
  await checkVersion();
20
15
  await runHealthCheck();
21
-
22
16
  server.listen(port, () => {
23
17
  debugServer("Server running on http://localhost:%s", port);
24
18
  });
25
-
26
19
  }
27
20
 
28
- // Re-export the public API for consumers of this package (named exports only)
29
- export * from "./src/services/s3.service.js";
30
- export * from "./src/config/index.js";
31
- export { default as bucket } from "./src/routes/index.js";
32
-
33
- // Default export: the Express application instance
21
+ export * from "./lib/src/services/s3.service.js";
22
+ export * from "./lib/src/config/index.js";
23
+ export { default as bucket } from "./lib/src/routes/index.js";
34
24
  export default server;
package/lib/cli.js ADDED
@@ -0,0 +1,642 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mbkbucket CLI — Manage S3/R2 buckets from the command line.
5
+ */
6
+
7
+ import dotenv from 'dotenv';
8
+ import path from 'node:path';
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';
12
+
13
+ dotenv.config();
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Parse CLI arguments
17
+ // ---------------------------------------------------------------------------
18
+ const args = process.argv.slice(2);
19
+ const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
20
+
21
+ function findFlag(name) {
22
+ const long = `--${name}`;
23
+ const short = `-${name[0]}`;
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (args[i] === long || args[i] === short) {
26
+ return args[i + 1] !== undefined && !args[i + 1].startsWith('-') ? args[i + 1] : true;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ const hasFlag = name => args.includes(`--${name}`) || args.includes(`-${name[0]}`);
33
+
34
+ function getPositionalArgs() {
35
+ const positional = [];
36
+ for (let i = 0; i < args.length; i++) {
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++;
41
+ }
42
+ } else {
43
+ positional.push(args[i]);
44
+ }
45
+ }
46
+ return positional;
47
+ }
48
+
49
+ const command = getPositionalArgs()[0] || 'help';
50
+ const cmdArgs = getPositionalArgs().slice(1);
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Help text & S3 helper
54
+ // ---------------------------------------------------------------------------
55
+ function showHelp() {
56
+ console.log(`
57
+ mbkbucket — S3/R2 bucket CLI
58
+
59
+ USAGE:
60
+ mbkbucket <command> [options] [arguments]
61
+
62
+ COMMANDS:
63
+ login Authenticate with mbkauthe device flow
64
+ logout Clear stored credentials
65
+ whoami Show current login status
66
+
67
+ config Show all config values
68
+ config get <key> Get a specific config value
69
+ config set <k> <v> Set a config value
70
+ config unset <key> Remove a config value
71
+ config path Show config file path
72
+ config reset Reset to demo values
73
+ config edit Open config file in editor
74
+
75
+ list [prefix] List files and folders
76
+ upload <file> [key] Upload a file or folder
77
+ download <key> [dest] Download a file or folder
78
+ delete <key> Delete a file
79
+ delete-folder <prefix> Recursively delete a folder
80
+ info <key> Show file metadata
81
+ signed-url <key> Generate a pre-signed download URL
82
+
83
+ GLOBAL OPTIONS:
84
+ --app, -a <name> Override APP_NAME for key prefixing
85
+ --bucket, -b <name> Override default bucket name
86
+ --help, -h Show this help
87
+
88
+ LOGIN OPTIONS:
89
+ --server <url> mbkauthe server URL (required for login)
90
+ --profile-key <k> API token profile key
91
+
92
+ SIGNED-URL OPTIONS:
93
+ --expires <sec> Expiration in seconds (default: 3600)
94
+
95
+ ENVIRONMENT:
96
+ BucketConnection JSON mapping of bucket name → S3 credentials
97
+ APP_NAME Application name for key prefix isolation
98
+ PORT Server port (default: 3004)
99
+ `);
100
+ }
101
+
102
+ let _s3Module = null;
103
+ async function getS3() {
104
+ if (_s3Module) return _s3Module;
105
+ const appFlag = findFlag('app');
106
+ if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
107
+ _s3Module = await import('./src/services/s3.service.js');
108
+ return _s3Module;
109
+ }
110
+
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
+ });
116
+ }
117
+
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
+ });
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Command Implementations
132
+ // ---------------------------------------------------------------------------
133
+
134
+ async function cmdLogin() {
135
+ const storedCfg = readConfig();
136
+ let serverUrl = findFlag('server') || storedCfg.serverUrl;
137
+
138
+ if (!serverUrl || typeof serverUrl !== '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 serverUrl https://your-server.com\n Then just run: mbkbucket login');
140
+ process.exit(1);
141
+ }
142
+
143
+ const profileKey = findFlag('profile-key') || storedCfg.profileKey;
144
+ updateConfig({ serverUrl });
145
+
146
+ try {
147
+ const result = await deviceFlowLogin({
148
+ serverUrl,
149
+ profileKey: typeof profileKey === 'string' ? profileKey : undefined,
150
+ });
151
+
152
+ updateConfig({
153
+ token: result.token,
154
+ tokenPrefix: result.tokenPrefix,
155
+ username: result.username,
156
+ serverUrl,
157
+ profile: result.profile || null,
158
+ loggedInAt: nowIso(),
159
+ });
160
+
161
+ console.log('\n✅ Credentials saved. You are now logged in.\n Run "mbkbucket whoami" to verify.');
162
+ } catch (err) {
163
+ const msg = err.message || '';
164
+ console.error(`❌ Login failed: ${msg}`);
165
+ if (msg.includes('profileKey') || msg.includes('profileId')) {
166
+ console.error('\n💡 An API token profile is required. Create one in mbkauthe admin panel and pass --profile-key.');
167
+ } else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
168
+ console.error(`\n💡 Could not reach server "${serverUrl}". Verify it is running and CLI_AUTH_ENABLED=true.`);
169
+ }
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ function cmdLogout() {
175
+ if (!isLoggedIn()) return console.log('ℹ️ Not logged in.');
176
+ const { username = 'user' } = readConfig();
177
+ clearConfig();
178
+ console.log(`👋 Logged out. Goodbye, ${username}!`);
179
+ }
180
+
181
+ function cmdWhoami() {
182
+ if (!isLoggedIn()) return console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
183
+ const cfg = readConfig();
184
+ console.log(`✅ Logged in as: ${cfg.username}`);
185
+ console.log(` Server: ${cfg.serverUrl}`);
186
+ console.log(` Token: ${cfg.tokenPrefix}...`);
187
+ if (cfg.profile) console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
188
+ console.log(` Since: ${formatDate(cfg.loggedInAt)}`);
189
+ }
190
+
191
+ async function cmdList() {
192
+ const prefix = cmdArgs[0] || '';
193
+ const bucketName = findFlag('bucket');
194
+
195
+ try {
196
+ const s3 = await getS3();
197
+ const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
198
+
199
+ if (!result.Contents?.length) {
200
+ return console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
201
+ }
202
+
203
+ console.log(`\n📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):\n` + '─'.repeat(80));
204
+
205
+ if (result.CommonPrefixes?.length) {
206
+ for (const cp of result.CommonPrefixes) {
207
+ const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
208
+ console.log(` 📂 ${folderName}/`);
209
+ }
210
+ if (result.Contents.length > 0) console.log('');
211
+ }
212
+
213
+ for (const obj of result.Contents) {
214
+ const name = obj.Key || '';
215
+ const displayName = name.length > 60 ? `...${name.slice(-57)}` : name;
216
+ console.log(` ${displayName.padEnd(62)} ${formatBytes(obj.Size).padStart(10)} ${formatDate(obj.LastModified)}`);
217
+ }
218
+
219
+ console.log('─'.repeat(80));
220
+ if (result.hasMore) console.log(' ... more files available (use --prefix to narrow down)');
221
+ console.log('');
222
+ } catch (err) {
223
+ console.error(`❌ List failed: ${err.message}`);
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ async function cmdUpload() {
229
+ const filePath = cmdArgs[0];
230
+ if (!filePath) {
231
+ console.error('❌ Usage: mbkbucket upload <file> [key]\n Example: mbkbucket upload ./photo.jpg images/photo.jpg');
232
+ process.exit(1);
233
+ }
234
+
235
+ const resolved = path.resolve(filePath);
236
+ if (!fs.existsSync(resolved)) {
237
+ console.error(`❌ File not found: ${filePath}`);
238
+ process.exit(1);
239
+ }
240
+
241
+ if (fs.statSync(resolved).isDirectory()) {
242
+ await uploadFolder(resolved);
243
+ } else {
244
+ await uploadSingleFile(resolved);
245
+ }
246
+ }
247
+
248
+ async function uploadSingleFile(filePath) {
249
+ const bucketName = findFlag('bucket');
250
+ const stat = fs.statSync(filePath);
251
+ const fileName = path.basename(filePath);
252
+ const key = cmdArgs[1] || fileName;
253
+ const contentType = getMimeType(fileName);
254
+
255
+ console.log(`⬆️ Uploading: ${fileName} (${formatBytes(stat.size)}) → ${key}`);
256
+ try {
257
+ const s3 = await getS3();
258
+ const result = await s3.uploadFile(key, fs.readFileSync(filePath), contentType, { bucketName });
259
+ console.log(`✅ Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
260
+ } catch (err) {
261
+ console.error(`❌ Upload failed: ${err.message}`);
262
+ process.exit(1);
263
+ }
264
+ }
265
+
266
+ async function uploadFolder(folderPath) {
267
+ const bucketName = findFlag('bucket');
268
+ const prefix = cmdArgs[1] || path.basename(folderPath);
269
+ const files = walkDir(folderPath);
270
+
271
+ if (!files.length) return console.log('📭 No files found in folder.');
272
+
273
+ console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/\n`);
274
+ let uploaded = 0;
275
+ let failed = 0;
276
+
277
+ for (const file of files) {
278
+ const rel = path.relative(folderPath, file).replace(/\\/g, '/');
279
+ const key = `${prefix}/${rel}`;
280
+ try {
281
+ const s3 = await getS3();
282
+ await s3.uploadFile(key, fs.readFileSync(file), getMimeType(file), { bucketName });
283
+ console.log(` ✅ ${rel}`);
284
+ uploaded++;
285
+ } catch (err) {
286
+ console.error(` ❌ ${rel}: ${err.message}`);
287
+ failed++;
288
+ }
289
+ }
290
+
291
+ console.log(`\nDone: ${uploaded} uploaded, ${failed} failed.`);
292
+ if (failed > 0) process.exit(1);
293
+ }
294
+
295
+ async function cmdDownload() {
296
+ const key = cmdArgs[0];
297
+ if (!key) {
298
+ console.error('❌ Usage: mbkbucket download <key> [destination]');
299
+ process.exit(1);
300
+ }
301
+
302
+ const dest = cmdArgs[1] || path.basename(key);
303
+ const bucketName = findFlag('bucket');
304
+
305
+ try {
306
+ const s3 = await getS3();
307
+ if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
308
+
309
+ console.log(`⬇️ Downloading: ${key} → ${dest}`);
310
+ const result = await s3.downloadFile(key, { bucketName });
311
+ if (result.notModified) return console.log('ℹ️ File not modified.');
312
+
313
+ const body = await streamToBuffer(result.Body);
314
+ const destDir = path.dirname(path.resolve(dest));
315
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
316
+ fs.writeFileSync(path.resolve(dest), body);
317
+
318
+ console.log(`✅ Downloaded: ${dest} (${formatBytes(body.length)})`);
319
+ } catch (err) {
320
+ console.error(`❌ Download failed: ${err.message}`);
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ async function downloadFolder(s3, prefix, destDir, bucketName) {
326
+ const allFiles = [];
327
+ let token = null;
328
+ const resolvedDest = path.resolve(destDir);
329
+
330
+ do {
331
+ const result = await s3.listfiles(prefix, { bucketName, continuationToken: token });
332
+ (result.Contents || []).forEach(obj => {
333
+ if (obj.Key && obj.Key !== prefix) allFiles.push(obj.Key);
334
+ });
335
+ token = result.IsTruncated ? result.NextContinuationToken : null;
336
+ } while (token);
337
+
338
+ if (!allFiles.length) return console.log(`📭 No files found with prefix "${prefix}".`);
339
+
340
+ console.log(`📁 Downloading ${allFiles.length} files to ${resolvedDest}/\n`);
341
+ let downloaded = 0;
342
+ let failed = 0;
343
+
344
+ for (const fileKey of allFiles) {
345
+ const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
346
+ const filePath = path.join(resolvedDest, relativePath);
347
+ try {
348
+ const result = await s3.downloadFile(fileKey, { bucketName });
349
+ if (!result.notModified) {
350
+ const body = await streamToBuffer(result.Body);
351
+ const fileDir = path.dirname(filePath);
352
+ if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
353
+ fs.writeFileSync(filePath, body);
354
+ }
355
+ console.log(` ✅ ${relativePath}`);
356
+ downloaded++;
357
+ } catch (err) {
358
+ console.error(` ❌ ${relativePath}: ${err.message}`);
359
+ failed++;
360
+ }
361
+ }
362
+
363
+ console.log(`\nDone: ${downloaded} downloaded, ${failed} failed.`);
364
+ if (failed > 0) process.exit(1);
365
+ }
366
+
367
+ async function cmdDelete() {
368
+ const key = cmdArgs[0];
369
+ if (!key) {
370
+ console.error('❌ Usage: mbkbucket delete <key>');
371
+ process.exit(1);
372
+ }
373
+
374
+ try {
375
+ const s3 = await getS3();
376
+ console.log(`🗑️ Deleting: ${key}`);
377
+ const result = await s3.deleteFile(key, findFlag('bucket'));
378
+ console.log(`✅ Deleted: ${result.key}`);
379
+ } catch (err) {
380
+ console.error(`❌ Delete failed: ${err.message}`);
381
+ process.exit(1);
382
+ }
383
+ }
384
+
385
+ async function cmdDeleteFolder() {
386
+ const prefix = cmdArgs[0];
387
+ if (!prefix) {
388
+ console.error('❌ Usage: mbkbucket delete-folder <prefix>');
389
+ process.exit(1);
390
+ }
391
+
392
+ const cleaned = trimSlashes(prefix);
393
+ console.log(`⚠️ This will delete ALL files under "${cleaned}/".\n Are you sure? Type "yes" to confirm:`);
394
+
395
+ const confirmed = await promptLine();
396
+ if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
397
+
398
+ try {
399
+ const s3 = await getS3();
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}/".`);
403
+ } catch (err) {
404
+ console.error(`❌ Delete folder failed: ${err.message}`);
405
+ process.exit(1);
406
+ }
407
+ }
408
+
409
+ async function cmdInfo() {
410
+ const key = cmdArgs[0];
411
+ if (!key) {
412
+ console.error('❌ Usage: mbkbucket info <key>');
413
+ process.exit(1);
414
+ }
415
+
416
+ try {
417
+ const s3 = await getS3();
418
+ const meta = await s3.getFileMetadata(key, findFlag('bucket'));
419
+ if (!meta.exists) {
420
+ console.log(`❌ File not found: ${key}`);
421
+ process.exit(1);
422
+ }
423
+
424
+ console.log(`\n📄 ${meta.key}\n` + '─'.repeat(60));
425
+ console.log(` Size: ${formatBytes(meta.ContentLength)}`);
426
+ console.log(` Type: ${meta.ContentType || 'unknown'}`);
427
+ console.log(` Last Modified: ${formatDate(meta.LastModified)}`);
428
+ console.log(` ETag: ${meta.ETag || '—'}`);
429
+ console.log(` Cache-Control: ${meta.CacheControl || '—'}`);
430
+ if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
431
+ console.log(' Metadata:');
432
+ for (const [k, v] of Object.entries(meta.Metadata)) console.log(` ${k}: ${v}`);
433
+ }
434
+ console.log('');
435
+ } catch (err) {
436
+ console.error(`❌ Info failed: ${err.message}`);
437
+ process.exit(1);
438
+ }
439
+ }
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
+
460
+ // ---------------------------------------------------------------------------
461
+ // Config Commands
462
+ // ---------------------------------------------------------------------------
463
+ const DEMO_CONFIG = {
464
+ token: 'mbk_demo_token_replace_with_real_one_from_login',
465
+ tokenPrefix: 'mbk_demo',
466
+ username: 'demo-user',
467
+ serverUrl: 'https://your-app.example.com',
468
+ profile: { name: 'Default CLI Profile', scope: 'read-write', allowedApps: ['mbkbucket'] },
469
+ loggedInAt: nowIso(),
470
+ };
471
+
472
+ const VALID_CONFIG_KEYS = ['token', 'tokenPrefix', 'username', 'serverUrl', 'profile', 'profileKey', 'loggedInAt'];
473
+
474
+ async function cmdConfig() {
475
+ const [sub, key, value] = cmdArgs;
476
+ switch (sub) {
477
+ case 'get': return cmdConfigGet(key);
478
+ case 'set': return cmdConfigSet(key, value);
479
+ case 'unset':
480
+ case 'del':
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();
486
+ }
487
+ }
488
+
489
+ function cmdConfigShow() {
490
+ const cfg = readConfig();
491
+ const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
492
+
493
+ if (!entries.length) {
494
+ console.log(`📭 No config values set.\n File: ${CONFIG_FILE}\n Run "mbkbucket config reset" to populate demo values.`);
495
+ return;
496
+ }
497
+
498
+ console.log(`\n📋 Config (${CONFIG_FILE}):\n` + '─'.repeat(60));
499
+ for (const k of entries.sort()) {
500
+ const v = cfg[k];
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}`);
506
+ }
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');
508
+ }
509
+
510
+ function cmdConfigGet(key) {
511
+ if (!key || !VALID_CONFIG_KEYS.includes(key)) {
512
+ console.error(`❌ Usage: mbkbucket config get <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
513
+ process.exit(1);
514
+ }
515
+ const cfg = readConfig();
516
+ if (!(key in cfg)) {
517
+ console.error(`❌ Key not found: ${key}\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
518
+ process.exit(1);
519
+ }
520
+ const v = cfg[key];
521
+ console.log(typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v));
522
+ }
523
+
524
+ function cmdConfigSet(key, value) {
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(', ')}`);
527
+ process.exit(1);
528
+ }
529
+
530
+ let parsed = value;
531
+ if (/^[{\[].*[}\]]$/.test(value)) {
532
+ try { parsed = JSON.parse(value); } catch {}
533
+ }
534
+
535
+ updateConfig({ [key]: parsed });
536
+ console.log(`✅ Set ${key} = ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}`);
537
+ }
538
+
539
+ function cmdConfigUnset(key) {
540
+ if (!key) {
541
+ console.error(`❌ Usage: mbkbucket config unset <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
542
+ process.exit(1);
543
+ }
544
+ const cfg = readConfig();
545
+ if (!(key in cfg)) {
546
+ console.error(`❌ Key not found: ${key}`);
547
+ process.exit(1);
548
+ }
549
+ delete cfg[key];
550
+ writeConfig(cfg);
551
+ console.log(`🗑️ Removed: ${key}`);
552
+ }
553
+
554
+ function cmdConfigPath() {
555
+ console.log(`📁 Config directory: ${CONFIG_DIR}\n📄 Config file: ${CONFIG_FILE}`);
556
+ if (fs.existsSync(CONFIG_FILE)) {
557
+ const stat = fs.statSync(CONFIG_FILE);
558
+ console.log(` Size: ${formatBytes(stat.size)}\n Modified: ${formatDate(stat.mtime.toISOString())}`);
559
+ }
560
+ }
561
+
562
+ async function cmdConfigReset() {
563
+ console.log('⚠️ This will overwrite your current config with demo values.\n Are you sure? Type "yes" to confirm:');
564
+ const confirmed = await promptLine();
565
+ if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
566
+ writeConfig({ ...DEMO_CONFIG });
567
+ console.log('✅ Config reset to demo values.\n Run "mbkbucket config" to view.');
568
+ }
569
+
570
+ async function cmdConfigEdit() {
571
+ const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
572
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
573
+ if (!fs.existsSync(CONFIG_FILE)) writeConfig({ ...DEMO_CONFIG });
574
+
575
+ console.log(`✏️ Opening ${CONFIG_FILE} with ${editor}...`);
576
+ const { spawn } = await import('node:child_process');
577
+ const child = spawn(editor, [CONFIG_FILE], { stdio: 'inherit', shell: true });
578
+
579
+ return new Promise((resolve) => {
580
+ child.on('exit', (code) => {
581
+ if (code === 0) console.log('✅ Editor closed.');
582
+ else console.error(`⚠️ Editor exited with code ${code}.`);
583
+ resolve();
584
+ });
585
+ child.on('error', (err) => {
586
+ console.error(`❌ Could not open editor (${editor}): ${err.message}`);
587
+ resolve();
588
+ });
589
+ });
590
+ }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Main Router
594
+ // ---------------------------------------------------------------------------
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
+ };
619
+
620
+ async function main() {
621
+ if (hasFlag('help')) return showHelp();
622
+
623
+ const appFlag = findFlag('app');
624
+ if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
625
+
626
+ const bucketFlag = findFlag('bucket');
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);
633
+ }
634
+
635
+ await handler();
636
+ }
637
+
638
+ main().catch((err) => {
639
+ console.error(`❌ Unexpected error: ${err.message}`);
640
+ if (process.env.DEBUG) console.error(err);
641
+ process.exit(1);
642
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MBKBucket File Manager - Gentelella v4 UI & Client Logic
2
+ * MBKBucket File Manager - UI & Client Logic
3
3
  */
4
4
 
5
5
  (function () {