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