mbkbucket 3.8.1 → 3.8.3

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/README.md CHANGED
@@ -111,65 +111,41 @@ The Express app includes:
111
111
 
112
112
  ## CLI Usage šŸ’»
113
113
 
114
- > **āš ļø Under active development.** The CLI is functional but some features may change.
114
+ mbkbucket includes a CLI for running the dashboard and API as a standalone Node.js server application.
115
115
 
116
- mbkbucket ships with a command-line interface for managing buckets directly from the terminal.
117
-
118
- ### Setup
116
+ ### Run Standalone Server
119
117
 
120
118
  ```bash
121
- # Set your mbkauthe server URL (one-time)
122
- mbkbucket config set serverUrl https://your-server.example.com
123
-
124
- # Set your API token profile key (one-time, optional)
125
- mbkbucket config set profileKey your-profile-key
119
+ # Run using npx
120
+ npx mbkbucket
126
121
 
127
- # Authenticate via device flow
128
- mbkbucket login
122
+ # Or after installing globally / locally
123
+ mbkbucket
129
124
  ```
130
125
 
131
- ### Commands
126
+ ### CLI Options
132
127
 
133
- | Command | Alias | Description |
128
+ | Option | Shorthand | Description |
134
129
  |---|---|---|
135
- | `mbkbucket login` | — | Authenticate via mbkauthe device flow |
136
- | `mbkbucket logout` | — | Clear stored credentials |
137
- | `mbkbucket whoami` | — | Show login status |
138
- | | | |
139
- | `mbkbucket list [prefix]` | `ls` | List files and folders |
140
- | `mbkbucket upload <file> [key]` | `up` | Upload a file or folder |
141
- | `mbkbucket download <key> [dest]` | `dl` | Download a file or folder |
142
- | `mbkbucket delete <key>` | `rm` | Delete a file |
143
- | `mbkbucket delete-folder <prefix>` | `rmdir` | Recursively delete a folder |
144
- | `mbkbucket info <key>` | `stat` | Show file metadata (size, type, etc.) |
145
- | `mbkbucket signed-url <key>` | `sign` | Generate a pre-signed download URL |
146
- | | | |
147
- | `mbkbucket config` | `cfg` | Show all config values |
148
- | `mbkbucket config get <key>` | — | Get a specific config value |
149
- | `mbkbucket config set <k> <v>` | — | Set a config value |
150
- | `mbkbucket config unset <key>` | — | Remove a config value |
151
- | `mbkbucket config path` | — | Show config file location |
152
- | `mbkbucket config reset` | — | Reset to demo values |
153
- | `mbkbucket config edit` | — | Open config file in editor |
154
-
155
- ### Global Options
156
-
157
- | Flag | Description |
158
- |---|---|
159
- | `--app`, `-a <name>` | Override `APP_NAME` for key prefix isolation |
160
- | `--bucket`, `-b <name>` | Override default bucket name |
161
- | `--help`, `-h` | Show help |
162
-
163
- ### Login Options
130
+ | `--port <number>` | `-p` | Port to listen on (default: `3004` or `$PORT`) |
131
+ | `--host <host>` | `-H` | Host address to bind to (default: `0.0.0.0` or `$HOST`) |
132
+ | `--app <name>` | `-a` | Override `APP_NAME` for bucket key prefix isolation |
133
+ | `--bucket <name>` | `-b` | Override default bucket name |
134
+ | `--env <path>` | `-e` | Load environment variables from a custom `.env` file |
135
+ | `--dev` | `-d` | Run in development mode (`NODE_ENV=dev`) |
136
+ | `--open` | `-o` | Automatically open dashboard in default browser |
137
+ | `--version` | `-v` | Show version number |
138
+ | `--help` | `-h` | Show help text |
139
+
140
+ ### Examples
164
141
 
165
- | Flag | Description |
166
- |---|---|
167
- | `--server <url>` | mbkauthe server URL |
168
- | `--profile-key <k>` | API token profile key |
169
-
170
- ### Config File
142
+ ```bash
143
+ # Start server on custom port and open browser
144
+ mbkbucket -p 8080 --open
171
145
 
172
- CLI configuration is stored at `~/.mbkbucket/config.json`. A demo config is created automatically on first use.
146
+ # Start with a specific .env file and application prefix
147
+ mbkbucket --env ./configs/.env.prod -a myapp
148
+ ```
173
149
 
174
150
  ---
175
151
 
package/lib/cli.js CHANGED
@@ -8,643 +8,232 @@
8
8
  */
9
9
 
10
10
  /**
11
- * mbkbucket CLI — Manage S3/R2 buckets from the command line.
11
+ * mbkbucket CLI — Standalone server runner for MBKBucket web application.
12
12
  */
13
13
 
14
14
  import dotenv from 'dotenv';
15
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';
16
+ import os from 'node:os';
17
+ import { createApp } from './src/app.js';
18
+ import { packageJson, checkVersion } from './src/config/index.js';
19
+ import { checkHealth, getAvailableBucketNames } from './src/services/s3.service.js';
19
20
 
20
- dotenv.config();
21
+ // ---------------------------------------------------------------------------
22
+ // ANSI Color helpers
23
+ // ---------------------------------------------------------------------------
24
+ const c = {
25
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
26
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
27
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
28
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
29
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
30
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
31
+ };
21
32
 
22
33
  // ---------------------------------------------------------------------------
23
34
  // Parse CLI arguments
24
35
  // ---------------------------------------------------------------------------
25
36
  const args = process.argv.slice(2);
26
- const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
27
37
 
28
- function findFlag(name) {
38
+ function findFlag(name, shortChar) {
29
39
  const long = `--${name}`;
30
- const short = `-${name[0]}`;
40
+ const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
31
41
  for (let i = 0; i < args.length; i++) {
32
42
  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++;
43
+ if (args[i + 1] !== undefined && !args[i + 1].startsWith('-')) {
44
+ return args[i + 1];
48
45
  }
49
- } else {
50
- positional.push(args[i]);
46
+ return true;
51
47
  }
52
48
  }
53
- return positional;
49
+ return undefined;
54
50
  }
55
51
 
56
- const positionalArgs = getPositionalArgs();
57
- const command = positionalArgs[0] || 'help';
58
- const cmdArgs = positionalArgs.slice(1);
52
+ const hasFlag = (name, shortChar) => {
53
+ const long = `--${name}`;
54
+ const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
55
+ return args.includes(long) || args.includes(short);
56
+ };
59
57
 
60
58
  // ---------------------------------------------------------------------------
61
- // Help text & S3 helper
59
+ // Help & Version
62
60
  // ---------------------------------------------------------------------------
63
61
  function showHelp() {
64
62
  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)
63
+ ${c.cyan(c.bold('⚔ mbkbucket'))} ${c.dim(`v${packageJson.version}`)}
64
+ Standalone S3/R2 Bucket Management Server
65
+
66
+ ${c.bold('USAGE:')}
67
+ mbkbucket [options]
68
+ npx mbkbucket [options]
69
+
70
+ ${c.bold('OPTIONS:')}
71
+ -p, --port <number> Port to listen on (default: 3004 or $PORT)
72
+ -H, --host <host> Host address to bind (default: 0.0.0.0 or $HOST)
73
+ -a, --app <name> Override APP_NAME for bucket key prefix isolation
74
+ -b, --bucket <name> Override default bucket name (MBKAUTHE_BUCKET)
75
+ -e, --env <path> Load environment variables from a custom .env file
76
+ -d, --dev Run in development mode (NODE_ENV=dev)
77
+ -o, --open Automatically open the dashboard in default browser
78
+ -v, --version Show version number
79
+ -h, --help Show this help message
80
+
81
+ ${c.bold('ENVIRONMENT VARIABLES:')}
82
+ PORT Server port (default: 3004)
83
+ HOST Server host (default: 0.0.0.0)
84
+ NODE_ENV Environment mode ('production' | 'dev')
85
+ BucketConnection JSON mapping of bucket name -> S3 credentials
86
+ mbkautheVar mbkauthe configuration JSON
87
+ mbkbucketVar mbkbucket configuration JSON
88
+
89
+ ${c.bold('EXAMPLES:')}
90
+ $ mbkbucket
91
+ $ mbkbucket -p 8080 --open
92
+ $ mbkbucket --env ./configs/.env.prod -a myapp
107
93
  `);
108
94
  }
109
95
 
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
- });
96
+ function showVersion() {
97
+ console.log(`mbkbucket v${packageJson.version}`);
136
98
  }
137
99
 
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
-
100
+ function getNetworkIp() {
154
101
  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}/`);
102
+ const nets = os.networkInterfaces();
103
+ for (const name of Object.keys(nets)) {
104
+ for (const net of nets[name] ?? []) {
105
+ if (net.family === 'IPv4' && !net.internal) return net.address;
217
106
  }
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
107
  }
108
+ } catch {}
109
+ return undefined;
110
+ }
226
111
 
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
- }
112
+ function openBrowser(url) {
113
+ const platform = process.platform;
114
+ let cmd = '';
115
+ if (platform === 'darwin') cmd = `open "${url}"`;
116
+ else if (platform === 'win32') cmd = `start "" "${url}"`;
117
+ else cmd = `xdg-open "${url}"`;
118
+ import('node:child_process').then(({ exec }) => {
119
+ exec(cmd, () => {});
120
+ }).catch(() => {});
234
121
  }
235
122
 
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);
123
+ // ---------------------------------------------------------------------------
124
+ // Main Runner
125
+ // ---------------------------------------------------------------------------
126
+ async function main() {
127
+ if (hasFlag('help', 'h')) {
128
+ showHelp();
129
+ process.exit(0);
241
130
  }
242
131
 
243
- const resolved = path.resolve(filePath);
244
- if (!fs.existsSync(resolved)) {
245
- console.error(`āŒ File not found: ${filePath}`);
246
- process.exit(1);
132
+ if (hasFlag('version', 'v')) {
133
+ showVersion();
134
+ process.exit(0);
247
135
  }
248
136
 
249
- if (fs.statSync(resolved).isDirectory()) {
250
- await uploadFolder(resolved);
137
+ // Load custom or default .env
138
+ const envPath = findFlag('env', 'e');
139
+ if (typeof envPath === 'string') {
140
+ dotenv.config({ path: path.resolve(process.cwd(), envPath) });
251
141
  } else {
252
- await uploadSingleFile(resolved);
142
+ dotenv.config();
253
143
  }
254
- }
255
144
 
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);
145
+ // Handle environment overrides from flags
146
+ const appFlag = findFlag('app', 'a');
147
+ if (typeof appFlag === 'string') {
148
+ process.env.APP_NAME = appFlag;
271
149
  }
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
150
 
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
- }
151
+ const bucketFlag = findFlag('bucket', 'b');
152
+ if (typeof bucketFlag === 'string') {
153
+ process.env.MBKAUTHE_BUCKET = bucketFlag;
297
154
  }
298
155
 
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);
156
+ if (hasFlag('dev', 'd')) {
157
+ process.env.NODE_ENV = 'dev';
308
158
  }
309
159
 
310
- const dest = cmdArgs[1] || path.basename(key);
311
- const bucketName = findFlag('bucket');
160
+ const port = parseInt(findFlag('port', 'p') || process.env.PORT || '3004', 10);
161
+ const host = findFlag('host', 'H') || process.env.HOST || '0.0.0.0';
162
+ const shouldOpen = hasFlag('open', 'o');
312
163
 
313
- try {
314
- const s3 = await getS3();
315
- if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
164
+ // Check version in background
165
+ await checkVersion();
316
166
 
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)})`);
167
+ // Test S3 connectivity
168
+ let healthResult = null;
169
+ try {
170
+ healthResult = await checkHealth();
327
171
  } catch (err) {
328
- console.error(`āŒ Download failed: ${err.message}`);
329
- process.exit(1);
172
+ healthResult = { status: 'unhealthy', error: err.message };
330
173
  }
331
- }
332
174
 
333
- async function downloadFolder(s3, prefix, destDir, bucketName) {
334
- const allFiles = [];
335
- let token = null;
336
- const resolvedDest = path.resolve(destDir);
175
+ // Create express application
176
+ const app = createApp();
337
177
 
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);
178
+ const server = app.listen(port, host, () => {
179
+ const isAnyHost = host === '0.0.0.0' || host === '::' || host === '';
180
+ const displayHost = isAnyHost ? 'localhost' : host;
181
+ const localUrl = `http://${displayHost}:${port}/mbkbucket`;
345
182
 
346
- if (!allFiles.length) return console.log(`šŸ“­ No files found with prefix "${prefix}".`);
183
+ console.log();
184
+ console.log(` ${c.cyan(c.bold('⚔ MBKBucket'))} ${c.dim(`v${packageJson.version}`)}`);
185
+ console.log();
186
+ console.log(` ${c.green('āžœ')} ${c.bold('Local:')} ${c.cyan(localUrl)}`);
347
187
 
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);
188
+ if (isAnyHost) {
189
+ const netIp = getNetworkIp();
190
+ if (netIp) {
191
+ console.log(` ${c.green('āžœ')} ${c.bold('Network:')} ${c.cyan(`http://${netIp}:${port}/mbkbucket`)}`);
362
192
  }
363
- console.log(` āœ… ${relativePath}`);
364
- downloaded++;
365
- } catch (err) {
366
- console.error(` āŒ ${relativePath}: ${err.message}`);
367
- failed++;
368
193
  }
369
- }
370
194
 
371
- console.log(`\nDone: ${downloaded} downloaded, ${failed} failed.`);
372
- if (failed > 0) process.exit(1);
373
- }
195
+ const appName = process.env.APP_NAME || 'root';
196
+ console.log(` ${c.green('āžœ')} ${c.bold('App Name:')} ${appName}`);
374
197
 
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
- }
198
+ let bucketNames = [];
199
+ try {
200
+ bucketNames = getAvailableBucketNames();
201
+ } catch {}
423
202
 
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);
203
+ if (bucketNames.length > 0) {
204
+ console.log(` ${c.green('āžœ')} ${c.bold('Buckets:')} ${bucketNames.join(', ')}`);
430
205
  }
431
206
 
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}`);
207
+ if (healthResult?.status === 'healthy') {
208
+ console.log(` ${c.green('āžœ')} ${c.bold('Storage:')} ${c.green('āœ“ Connected')} (${healthResult.bucket} in ${healthResult.responseTime}ms)`);
209
+ } else if (healthResult?.error) {
210
+ console.log(` ${c.yellow('āžœ')} ${c.bold('Storage:')} ${c.yellow('⚠ Connection warning')} (${healthResult.error})`);
441
211
  }
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
212
 
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 });
213
+ console.log();
214
+ console.log(` ${c.dim('Press Ctrl+C to stop the server')}`);
215
+ console.log();
586
216
 
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
- });
217
+ if (shouldOpen) {
218
+ openBrowser(localUrl);
219
+ }
597
220
  });
598
- }
599
221
 
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
- }
222
+ const shutdown = (signal) => {
223
+ console.log(`\n ${c.dim(`Received ${signal}, shutting down...`)}`);
224
+ server.close(() => {
225
+ console.log(` ${c.green('āœ“')} ${c.dim('MBKBucket server stopped.')}\n`);
226
+ process.exit(0);
227
+ });
228
+ setTimeout(() => process.exit(0), 2000).unref();
229
+ };
642
230
 
643
- await handler();
231
+ process.on('SIGINT', () => shutdown('SIGINT'));
232
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
644
233
  }
645
234
 
646
235
  main().catch((err) => {
647
- console.error(`āŒ Unexpected error: ${err.message}`);
236
+ console.error(`\n${c.red(c.bold('Error starting mbkbucket:'))} ${err.message}`);
648
237
  if (process.env.DEBUG) console.error(err);
649
238
  process.exit(1);
650
239
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "3.8.1",
4
- "description": "Flexible S3/R2 bucket management — library, Express router, and CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a command-line interface with built-in configuration validation and health checks",
3
+ "version": "3.8.3",
4
+ "description": "Flexible S3/R2 bucket management — library, Express router, and standalone server CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a standalone server runner with built-in configuration validation and health checks",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
7
7
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  "mbkbucket": "./lib/cli.js"
13
13
  },
14
14
  "scripts": {
15
+ "start": "node lib/cli.js",
15
16
  "dev": "cross-env NODE_ENV=dev nodemon index.js",
16
17
  "publish": "node scripts/publish.js",
17
18
  "test": "vitest run"
@@ -23,22 +24,22 @@
23
24
  "dependencies": {
24
25
  "@aws-sdk/client-s3": "^3.1127.0",
25
26
  "@aws-sdk/s3-request-presigner": "^3.1127.0",
26
- "compression": "^1.8.0",
27
+ "compression": "^1.8.2",
27
28
  "debug": "^4.4.3",
28
- "dotenv": "^16.6.1",
29
- "express": "^5.1.0",
29
+ "dotenv": "^18.0.5",
30
+ "express": "^5.2.1",
30
31
  "express-handlebars": "^9.0.1",
31
32
  "express-rate-limit": "^7.5.1",
32
- "mbkauthe": "6.2.0",
33
- "multer": "^2.0.2",
34
- "pg": "^8.16.3"
33
+ "mbkauthe": "^6.2.3",
34
+ "multer": "^2.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/supertest": "^7.2.1",
38
38
  "cross-env": "^7.0.3",
39
39
  "nodemon": "^3.1.14",
40
- "supertest": "^7.2.2",
41
- "vitest": "^4.1.11"
40
+ "supertest": "^7.3.1",
41
+ "vite": "^8.3.2",
42
+ "vitest": "5.0.3"
42
43
  },
43
44
  "overrides": {
44
45
  "csurf": {
@@ -56,8 +57,6 @@
56
57
  "lib",
57
58
  "views",
58
59
  ".env.example",
59
- ".mbkbucket.config.example.json",
60
- "cli.js",
61
60
  "index.js",
62
61
  "LICENSE",
63
62
  "package.json",
@@ -1,14 +0,0 @@
1
- {
2
- "_schema": "mbkbucket CLI config — stored at ~/.mbkbucket/config.json",
3
-
4
- "token": "mbk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
5
- "tokenPrefix": "mbk_xxxx",
6
- "username": "your-username",
7
- "serverUrl": "https://your-app.example.com",
8
- "profile": {
9
- "name": "Read-Write CLI",
10
- "scope": "read-write",
11
- "allowedApps": ["mbkbucket"]
12
- },
13
- "loggedInAt": "2026-01-01T00:00:00.000Z"
14
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * CLI Authentication Helper
3
- * Handles mbkauthe device-flow login for the mbkbucket CLI.
4
- * Stores credentials in ~/.mbkbucket/config.json
5
- */
6
-
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import os from 'node:os';
10
- import { createLogger } from '#logger';
11
-
12
- const debugCli = createLogger('cli:auth');
13
-
14
- export const CONFIG_DIR = path.join(os.homedir(), '.mbkbucket');
15
- export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
16
- const MAX_POLL_TIME_MS = 15 * 60 * 1000; // 15 minutes
17
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
18
-
19
- export function readConfig() {
20
- try {
21
- return fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) : {};
22
- } catch (err) {
23
- debugCli('Failed to read config: %s', err.message);
24
- return {};
25
- }
26
- }
27
-
28
- export function writeConfig(config) {
29
- try {
30
- if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
32
- } catch (err) {
33
- console.error(`āŒ Failed to write config: ${err.message}`);
34
- throw err;
35
- }
36
- }
37
-
38
- export function clearConfig() {
39
- try {
40
- if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
41
- return true;
42
- } catch (err) {
43
- debugCli('Failed to clear config: %s', err.message);
44
- return false;
45
- }
46
- }
47
-
48
- export function updateConfig(partial) {
49
- const updated = { ...readConfig(), ...partial };
50
- writeConfig(updated);
51
- return updated;
52
- }
53
-
54
- export function isLoggedIn() {
55
- const { token, server_url } = readConfig();
56
- return Boolean(token && server_url);
57
- }
58
-
59
- export const getStoredToken = () => readConfig().token || null;
60
- export const getStoredServerUrl = () => readConfig().server_url || null;
61
-
62
- export async function deviceFlowLogin({ server_url, profile_key }) {
63
- const baseUrl = String(server_url).replace(/\/+$/, '');
64
- console.log(`šŸ”‘ Requesting device login from ${baseUrl} ...`);
65
-
66
- let deviceResp;
67
- try {
68
- const res = await fetch(`${baseUrl}/api/cli/device`, {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({
72
- client_name: `mbkbucket-cli-${os.hostname()}`,
73
- ...(profile_key && { profile_key }),
74
- }),
75
- });
76
- deviceResp = await res.json();
77
- if (!deviceResp.success) throw new Error(deviceResp.message || 'Failed to initiate device login');
78
- } catch (err) {
79
- if (err.message.includes('Failed to initiate') || err.message.includes('fetch')) {
80
- throw new Error(`Could not reach mbkauthe server at ${baseUrl}/api/cli/device.\nVerify the server URL and that CLI_AUTH_ENABLED=true on the server.`);
81
- }
82
- throw err;
83
- }
84
-
85
- const { verification_url = '', user_code = '', device_code, expires_in = 900, interval = 5, profile } = deviceResp;
86
-
87
- console.log('\nā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
88
- console.log('│ │');
89
- console.log('│ 🌐 Open this URL in your browser to approve: │');
90
- console.log(`│ ${verification_url.padEnd(53)}│`);
91
- console.log('│ │');
92
- console.log(`│ šŸ”¢ Or enter this code: ${user_code.padEnd(30)}│`);
93
- console.log('│ │');
94
- console.log('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n');
95
-
96
- if (profile) console.log(` Profile: ${profile.name} (scope: ${profile.scope || 'full'})`);
97
- console.log(` Waiting for approval (expires in ${Math.floor(expires_in / 60)} minutes)...\n`);
98
-
99
- const pollInterval = interval * 1000;
100
- const deadline = Date.now() + MAX_POLL_TIME_MS;
101
- let attempts = 0;
102
-
103
- while (Date.now() < deadline) {
104
- await sleep(pollInterval);
105
- attempts++;
106
-
107
- try {
108
- const pollRes = await fetch(`${baseUrl}/api/cli/device/token`, {
109
- method: 'POST',
110
- headers: { 'Content-Type': 'application/json' },
111
- body: JSON.stringify({ device_code }),
112
- });
113
-
114
- const pollBody = await pollRes.json();
115
-
116
- if (pollBody.status === 'approved' && pollBody.success) {
117
- const token_prefix = pollBody.token_prefix || '';
118
- console.log(`āœ… Login approved! Welcome, ${pollBody.username}.`);
119
- console.log(` Token prefix: ${token_prefix}...`);
120
- return {
121
- token: pollBody.token,
122
- token_prefix,
123
- username: pollBody.username,
124
- profile: profile || null,
125
- };
126
- }
127
-
128
- if (pollBody.status === 'denied') throw new Error('Login request was denied.');
129
- if (pollBody.status === 'expired') throw new Error('Login request expired. Please try again.');
130
- if (pollBody.status === 'completed') throw new Error('This login session has already been completed.');
131
-
132
- if (attempts % 5 === 0) process.stdout.write('.');
133
- } catch (err) {
134
- if (['denied', 'expired', 'completed'].some(k => err.message.includes(k))) throw err;
135
- debugCli('Poll error (will retry): %s', err.message);
136
- }
137
- }
138
-
139
- throw new Error('Login timed out. Please try again.');
140
- }
141
-