@lanonasis/cli 3.0.13 → 3.2.14

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.
@@ -15,6 +15,361 @@ const colors = {
15
15
  muted: chalk.gray,
16
16
  highlight: chalk.white.bold
17
17
  };
18
+ // Helper function to handle authentication delays
19
+ async function handleAuthDelay(config) {
20
+ if (config.shouldDelayAuth()) {
21
+ const delayMs = config.getAuthDelayMs();
22
+ const failureCount = config.getFailureCount();
23
+ const lastFailure = config.getLastAuthFailure();
24
+ console.log();
25
+ console.log(chalk.yellow(`⚠️ Multiple authentication failures detected (${failureCount} attempts)`));
26
+ if (lastFailure) {
27
+ const lastFailureDate = new Date(lastFailure);
28
+ console.log(chalk.gray(`Last failure: ${lastFailureDate.toLocaleString()}`));
29
+ }
30
+ console.log(chalk.yellow(`Waiting ${Math.round(delayMs / 1000)} seconds before retry...`));
31
+ console.log(chalk.gray('This delay helps prevent account lockouts and reduces server load.'));
32
+ // Show countdown
33
+ const spinner = ora(`Waiting ${Math.round(delayMs / 1000)} seconds...`).start();
34
+ await new Promise(resolve => setTimeout(resolve, delayMs));
35
+ spinner.succeed('Ready to retry authentication');
36
+ console.log();
37
+ }
38
+ }
39
+ // Enhanced authentication failure handler
40
+ async function handleAuthenticationFailure(error, config, authMethod = 'jwt') {
41
+ // Increment failure count
42
+ await config.incrementFailureCount();
43
+ const failureCount = config.getFailureCount();
44
+ // Determine error type and provide specific guidance
45
+ const errorType = categorizeAuthError(error);
46
+ console.log();
47
+ console.log(chalk.red('✖ Authentication failed'));
48
+ switch (errorType) {
49
+ case 'invalid_credentials':
50
+ console.log(chalk.red('Invalid credentials provided'));
51
+ if (authMethod === 'vendor_key') {
52
+ console.log(chalk.gray('• Check your vendor key format: pk_xxx.sk_xxx'));
53
+ console.log(chalk.gray('• Verify the key is active in your account dashboard'));
54
+ console.log(chalk.gray('• Ensure you copied the complete key including both parts'));
55
+ }
56
+ else {
57
+ console.log(chalk.gray('• Double-check your email and password'));
58
+ console.log(chalk.gray('• Passwords are case-sensitive'));
59
+ console.log(chalk.gray('• Consider resetting your password if needed'));
60
+ }
61
+ break;
62
+ case 'network_error':
63
+ console.log(chalk.red('Network connection failed'));
64
+ console.log(chalk.gray('• Check your internet connection'));
65
+ console.log(chalk.gray('• Verify you can access https://api.lanonasis.com'));
66
+ console.log(chalk.gray('• Try again in a few moments'));
67
+ if (failureCount >= 2) {
68
+ console.log(chalk.gray('• Consider using a different network if issues persist'));
69
+ }
70
+ break;
71
+ case 'server_error':
72
+ console.log(chalk.red('Server temporarily unavailable'));
73
+ console.log(chalk.gray('• The authentication service may be experiencing issues'));
74
+ console.log(chalk.gray('• Please try again in a few minutes'));
75
+ console.log(chalk.gray('• Check https://status.lanonasis.com for service status'));
76
+ break;
77
+ case 'rate_limited':
78
+ console.log(chalk.red('Too many authentication attempts'));
79
+ console.log(chalk.gray('• Please wait before trying again'));
80
+ console.log(chalk.gray('• Rate limiting helps protect your account'));
81
+ console.log(chalk.gray('• Consider using a vendor key for automated access'));
82
+ break;
83
+ case 'expired_token':
84
+ console.log(chalk.red('Authentication token has expired'));
85
+ console.log(chalk.gray('• Please log in again to refresh your session'));
86
+ console.log(chalk.gray('• Consider using a vendor key for longer-term access'));
87
+ await config.clearInvalidCredentials();
88
+ break;
89
+ default:
90
+ console.log(chalk.red(`Unexpected error: ${error.message || 'Unknown error'}`));
91
+ console.log(chalk.gray('• Please try again'));
92
+ console.log(chalk.gray('• If the problem persists, contact support'));
93
+ }
94
+ // Progressive guidance for repeated failures
95
+ if (failureCount >= 3) {
96
+ console.log();
97
+ console.log(chalk.yellow('💡 Multiple failures detected. Recovery options:'));
98
+ if (authMethod === 'vendor_key') {
99
+ console.log(chalk.cyan('• Generate a new vendor key from your dashboard'));
100
+ console.log(chalk.cyan('• Try: lanonasis auth logout && lanonasis auth login'));
101
+ console.log(chalk.cyan('• Switch to browser login: lanonasis auth login --use-web-auth'));
102
+ }
103
+ else {
104
+ console.log(chalk.cyan('• Reset your password if you\'re unsure'));
105
+ console.log(chalk.cyan('• Try vendor key authentication instead'));
106
+ console.log(chalk.cyan('• Clear stored config: lanonasis auth logout'));
107
+ }
108
+ if (failureCount >= 5) {
109
+ console.log(chalk.yellow('• Consider contacting support if issues persist'));
110
+ console.log(chalk.gray('• Include error details and your email address'));
111
+ }
112
+ }
113
+ }
114
+ // Categorize authentication errors for specific handling
115
+ function categorizeAuthError(error) {
116
+ if (!error)
117
+ return 'unknown';
118
+ // Check HTTP status codes
119
+ if (error.response?.status) {
120
+ const status = error.response.status;
121
+ switch (status) {
122
+ case 401:
123
+ // Check if it's specifically an expired token
124
+ if (error.response.data?.error?.includes('expired') || error.response.data?.message?.includes('expired')) {
125
+ return 'expired_token';
126
+ }
127
+ return 'invalid_credentials';
128
+ case 403:
129
+ return 'invalid_credentials';
130
+ case 429:
131
+ return 'rate_limited';
132
+ case 500:
133
+ case 502:
134
+ case 503:
135
+ case 504:
136
+ return 'server_error';
137
+ }
138
+ }
139
+ // Check error codes for network issues
140
+ if (error.code) {
141
+ switch (error.code) {
142
+ case 'ECONNREFUSED':
143
+ case 'ENOTFOUND':
144
+ case 'ECONNRESET':
145
+ case 'ETIMEDOUT':
146
+ case 'ENETUNREACH':
147
+ return 'network_error';
148
+ }
149
+ }
150
+ // Check error messages
151
+ const message = error.message?.toLowerCase() || '';
152
+ if (message.includes('network') || message.includes('connection') || message.includes('timeout')) {
153
+ return 'network_error';
154
+ }
155
+ if (message.includes('invalid') || message.includes('unauthorized') || message.includes('forbidden')) {
156
+ return 'invalid_credentials';
157
+ }
158
+ if (message.includes('expired')) {
159
+ return 'expired_token';
160
+ }
161
+ if (message.includes('rate limit') || message.includes('too many')) {
162
+ return 'rate_limited';
163
+ }
164
+ return 'unknown';
165
+ }
166
+ export async function diagnoseCommand() {
167
+ const config = new CLIConfig();
168
+ await config.init();
169
+ console.log(chalk.blue.bold('🔍 Authentication Diagnostic'));
170
+ console.log(colors.info('━'.repeat(50)));
171
+ console.log();
172
+ const diagnostics = {
173
+ configExists: false,
174
+ hasCredentials: false,
175
+ credentialType: 'none',
176
+ credentialsValid: false,
177
+ tokenExpired: false,
178
+ authFailures: 0,
179
+ lastFailure: null,
180
+ endpointsReachable: false,
181
+ serviceDiscovery: false,
182
+ deviceId: null
183
+ };
184
+ // Step 1: Check if config exists
185
+ console.log(chalk.cyan('1. Configuration File'));
186
+ try {
187
+ const configExists = await config.exists();
188
+ diagnostics.configExists = configExists;
189
+ if (configExists) {
190
+ console.log(chalk.green(' ✓ Config file exists at'), config.getConfigPath());
191
+ }
192
+ else {
193
+ console.log(chalk.red(' ✖ Config file not found at'), config.getConfigPath());
194
+ console.log(chalk.gray(' → Run: lanonasis auth login'));
195
+ }
196
+ }
197
+ catch (error) {
198
+ console.log(chalk.red(' ✖ Error checking config:'), error instanceof Error ? error.message : 'Unknown error');
199
+ }
200
+ // Step 2: Check stored credentials
201
+ console.log(chalk.cyan('\n2. Stored Credentials'));
202
+ const token = config.getToken();
203
+ const vendorKey = config.getVendorKey();
204
+ const authMethod = config.get('authMethod');
205
+ if (vendorKey) {
206
+ diagnostics.hasCredentials = true;
207
+ diagnostics.credentialType = 'vendor_key';
208
+ console.log(chalk.green(' ✓ Vendor key found'));
209
+ // Validate vendor key format
210
+ const formatValidation = config.validateVendorKeyFormat(vendorKey);
211
+ if (formatValidation === true) {
212
+ console.log(chalk.green(' ✓ Vendor key format is valid'));
213
+ }
214
+ else {
215
+ console.log(chalk.red(' ✖ Vendor key format is invalid:'));
216
+ console.log(chalk.gray(` ${formatValidation}`));
217
+ }
218
+ }
219
+ else if (token) {
220
+ diagnostics.hasCredentials = true;
221
+ diagnostics.credentialType = authMethod === 'oauth' ? 'oauth' : 'jwt';
222
+ console.log(chalk.green(` ✓ ${diagnostics.credentialType.toUpperCase()} token found`));
223
+ // Check token expiry
224
+ try {
225
+ const isAuth = await config.isAuthenticated();
226
+ if (!isAuth) {
227
+ diagnostics.tokenExpired = true;
228
+ console.log(chalk.red(' ✖ Token is expired'));
229
+ }
230
+ else {
231
+ console.log(chalk.green(' ✓ Token is not expired'));
232
+ }
233
+ }
234
+ catch (error) {
235
+ console.log(chalk.yellow(' ⚠ Could not validate token expiry'));
236
+ }
237
+ }
238
+ else {
239
+ console.log(chalk.red(' ✖ No credentials found'));
240
+ console.log(chalk.gray(' → Run: lanonasis auth login'));
241
+ }
242
+ // Step 3: Check authentication failures
243
+ console.log(chalk.cyan('\n3. Authentication History'));
244
+ diagnostics.authFailures = config.getFailureCount();
245
+ diagnostics.lastFailure = config.getLastAuthFailure() ?? null;
246
+ if (diagnostics.authFailures === 0) {
247
+ console.log(chalk.green(' ✓ No recent authentication failures'));
248
+ }
249
+ else {
250
+ console.log(chalk.yellow(` ⚠ ${diagnostics.authFailures} recent authentication failures`));
251
+ if (diagnostics.lastFailure) {
252
+ const lastFailureDate = new Date(diagnostics.lastFailure);
253
+ console.log(chalk.gray(` Last failure: ${lastFailureDate.toLocaleString()}`));
254
+ }
255
+ if (config.shouldDelayAuth()) {
256
+ const delayMs = config.getAuthDelayMs();
257
+ console.log(chalk.yellow(` ⚠ Authentication delay active: ${Math.round(delayMs / 1000)}s`));
258
+ }
259
+ }
260
+ // Step 4: Test credential validation against server
261
+ console.log(chalk.cyan('\n4. Server Validation'));
262
+ if (diagnostics.hasCredentials) {
263
+ const spinner = ora('Testing credentials against server...').start();
264
+ try {
265
+ const isValid = await config.validateStoredCredentials();
266
+ diagnostics.credentialsValid = isValid;
267
+ if (isValid) {
268
+ spinner.succeed('Credentials are valid');
269
+ console.log(chalk.green(' ✓ Server authentication successful'));
270
+ }
271
+ else {
272
+ spinner.fail('Credentials are invalid');
273
+ console.log(chalk.red(' ✖ Server rejected credentials'));
274
+ console.log(chalk.gray(' → Try: lanonasis auth login'));
275
+ }
276
+ }
277
+ catch (error) {
278
+ spinner.fail('Server validation failed');
279
+ console.log(chalk.red(' ✖ Could not validate with server:'));
280
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
281
+ }
282
+ }
283
+ else {
284
+ console.log(chalk.gray(' - Skipped (no credentials to validate)'));
285
+ }
286
+ // Step 5: Test endpoint connectivity
287
+ console.log(chalk.cyan('\n5. Endpoint Connectivity'));
288
+ const spinner2 = ora('Testing authentication endpoints...').start();
289
+ try {
290
+ await config.discoverServices();
291
+ diagnostics.serviceDiscovery = true;
292
+ const services = config.get('discoveredServices');
293
+ if (services) {
294
+ spinner2.succeed('Authentication endpoints reachable');
295
+ console.log(chalk.green(' ✓ Service discovery successful'));
296
+ console.log(chalk.gray(` Auth endpoint: ${services.auth_base}`));
297
+ diagnostics.endpointsReachable = true;
298
+ }
299
+ else {
300
+ spinner2.warn('Using fallback endpoints');
301
+ console.log(chalk.yellow(' ⚠ Service discovery failed, using fallbacks'));
302
+ diagnostics.endpointsReachable = true; // Fallbacks still work
303
+ }
304
+ }
305
+ catch (error) {
306
+ spinner2.fail('Endpoint connectivity failed');
307
+ console.log(chalk.red(' ✖ Cannot reach authentication endpoints'));
308
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
309
+ console.log(chalk.gray(' → Check internet connection'));
310
+ }
311
+ // Step 6: Device identification
312
+ console.log(chalk.cyan('\n6. Device Information'));
313
+ try {
314
+ const deviceId = await config.getDeviceId();
315
+ diagnostics.deviceId = deviceId;
316
+ console.log(chalk.green(' ✓ Device ID:'), chalk.gray(deviceId));
317
+ }
318
+ catch (error) {
319
+ console.log(chalk.yellow(' ⚠ Could not get device ID'));
320
+ }
321
+ // Summary and recommendations
322
+ console.log(chalk.blue.bold('\n📋 Diagnostic Summary'));
323
+ console.log(colors.info('━'.repeat(50)));
324
+ const issues = [];
325
+ const recommendations = [];
326
+ if (!diagnostics.configExists) {
327
+ issues.push('No configuration file found');
328
+ recommendations.push('Run: lanonasis auth login');
329
+ }
330
+ if (!diagnostics.hasCredentials) {
331
+ issues.push('No authentication credentials stored');
332
+ recommendations.push('Run: lanonasis auth login --vendor-key pk_xxx.sk_xxx');
333
+ }
334
+ if (diagnostics.hasCredentials && !diagnostics.credentialsValid) {
335
+ issues.push('Stored credentials are invalid');
336
+ recommendations.push('Run: lanonasis auth logout && lanonasis auth login');
337
+ }
338
+ if (diagnostics.tokenExpired) {
339
+ issues.push('Authentication token has expired');
340
+ recommendations.push('Run: lanonasis auth login');
341
+ }
342
+ if (diagnostics.authFailures >= 3) {
343
+ issues.push(`Multiple authentication failures (${diagnostics.authFailures})`);
344
+ recommendations.push('Wait for delay period, then try: lanonasis auth login');
345
+ }
346
+ if (!diagnostics.endpointsReachable) {
347
+ issues.push('Cannot reach authentication endpoints');
348
+ recommendations.push('Check internet connection and firewall settings');
349
+ }
350
+ if (issues.length === 0) {
351
+ console.log(chalk.green('✅ All authentication checks passed!'));
352
+ console.log(chalk.cyan(' Your authentication is working correctly.'));
353
+ }
354
+ else {
355
+ console.log(chalk.red(`❌ Found ${issues.length} issue(s):`));
356
+ issues.forEach(issue => {
357
+ console.log(chalk.red(` • ${issue}`));
358
+ });
359
+ console.log(chalk.yellow('\n💡 Recommended actions:'));
360
+ recommendations.forEach(rec => {
361
+ console.log(chalk.cyan(` • ${rec}`));
362
+ });
363
+ }
364
+ // Additional troubleshooting info
365
+ if (diagnostics.authFailures > 0 || !diagnostics.credentialsValid) {
366
+ console.log(chalk.gray('\n🔧 Additional troubleshooting:'));
367
+ console.log(chalk.gray(' • Verify your vendor key format: pk_xxx.sk_xxx'));
368
+ console.log(chalk.gray(' • Check if your key is active in the dashboard'));
369
+ console.log(chalk.gray(' • Try browser authentication: lanonasis auth login --use-web-auth'));
370
+ console.log(chalk.gray(' • Contact support if issues persist'));
371
+ }
372
+ }
18
373
  export async function loginCommand(options) {
19
374
  const config = new CLIConfig();
20
375
  await config.init();
@@ -74,6 +429,8 @@ export async function loginCommand(options) {
74
429
  }
75
430
  }
76
431
  async function handleVendorKeyAuth(vendorKey, config) {
432
+ // Check for authentication delay before attempting
433
+ await handleAuthDelay(config);
77
434
  const spinner = ora('Validating vendor key...').start();
78
435
  try {
79
436
  await config.setVendorKey(vendorKey);
@@ -86,8 +443,8 @@ async function handleVendorKeyAuth(vendorKey, config) {
86
443
  }
87
444
  catch (error) {
88
445
  spinner.fail('Vendor key validation failed');
89
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
90
- console.error(chalk.red('✖ Invalid vendor key:'), errorMessage);
446
+ // Use enhanced error handling
447
+ await handleAuthenticationFailure(error, config, 'vendor_key');
91
448
  process.exit(1);
92
449
  }
93
450
  }
@@ -96,6 +453,13 @@ async function handleVendorKeyFlow(config) {
96
453
  console.log(chalk.yellow('🔑 Vendor Key Authentication'));
97
454
  console.log(chalk.gray('Vendor keys provide secure API access with format: pk_xxx.sk_xxx'));
98
455
  console.log();
456
+ // Enhanced guidance for obtaining vendor keys
457
+ console.log(chalk.cyan('📋 How to get your vendor key:'));
458
+ console.log(chalk.gray('1. Visit your Lanonasis dashboard at https://app.lanonasis.com'));
459
+ console.log(chalk.gray('2. Navigate to Settings → API Keys'));
460
+ console.log(chalk.gray('3. Click "Generate New Key" and copy the full key'));
461
+ console.log(chalk.gray('4. The key format should be: pk_[letters/numbers].sk_[letters/numbers]'));
462
+ console.log();
99
463
  const { vendorKey } = await inquirer.prompt([
100
464
  {
101
465
  type: 'password',
@@ -103,32 +467,73 @@ async function handleVendorKeyFlow(config) {
103
467
  message: 'Enter your vendor key (pk_xxx.sk_xxx):',
104
468
  mask: '*',
105
469
  validate: (input) => {
106
- if (!input)
107
- return 'Vendor key is required';
108
- if (!input.match(/^pk_[a-zA-Z0-9]+\.sk_[a-zA-Z0-9]+$/)) {
109
- return 'Invalid format. Expected: pk_xxx.sk_xxx';
110
- }
111
- return true;
470
+ return validateVendorKeyFormat(input);
112
471
  }
113
472
  }
114
473
  ]);
115
474
  await handleVendorKeyAuth(vendorKey, config);
116
475
  }
476
+ // Enhanced vendor key format validation with detailed error messages
477
+ function validateVendorKeyFormat(input) {
478
+ if (!input || input.trim().length === 0) {
479
+ return 'Vendor key is required';
480
+ }
481
+ const trimmed = input.trim();
482
+ // Check basic format
483
+ if (!trimmed.includes('.')) {
484
+ return 'Invalid format: Vendor key must contain a dot (.) separator\nExpected format: pk_xxx.sk_xxx';
485
+ }
486
+ const parts = trimmed.split('.');
487
+ if (parts.length !== 2) {
488
+ return 'Invalid format: Vendor key must have exactly two parts separated by a dot\nExpected format: pk_xxx.sk_xxx';
489
+ }
490
+ const [publicPart, secretPart] = parts;
491
+ // Validate public key part
492
+ if (!publicPart.startsWith('pk_')) {
493
+ return 'Invalid format: First part must start with "pk_"\nExpected format: pk_xxx.sk_xxx';
494
+ }
495
+ if (publicPart.length < 4) {
496
+ return 'Invalid format: Public key part is too short\nExpected format: pk_xxx.sk_xxx (where xxx is alphanumeric)';
497
+ }
498
+ const publicKeyContent = publicPart.substring(3); // Remove 'pk_'
499
+ if (!/^[a-zA-Z0-9]+$/.test(publicKeyContent)) {
500
+ return 'Invalid format: Public key part contains invalid characters\nOnly letters and numbers are allowed after "pk_"';
501
+ }
502
+ // Validate secret key part
503
+ if (!secretPart.startsWith('sk_')) {
504
+ return 'Invalid format: Second part must start with "sk_"\nExpected format: pk_xxx.sk_xxx';
505
+ }
506
+ if (secretPart.length < 4) {
507
+ return 'Invalid format: Secret key part is too short\nExpected format: pk_xxx.sk_xxx (where xxx is alphanumeric)';
508
+ }
509
+ const secretKeyContent = secretPart.substring(3); // Remove 'sk_'
510
+ if (!/^[a-zA-Z0-9]+$/.test(secretKeyContent)) {
511
+ return 'Invalid format: Secret key part contains invalid characters\nOnly letters and numbers are allowed after "sk_"';
512
+ }
513
+ // Check minimum length requirements
514
+ if (publicKeyContent.length < 8) {
515
+ return 'Invalid format: Public key part is too short (minimum 8 characters after "pk_")';
516
+ }
517
+ if (secretKeyContent.length < 16) {
518
+ return 'Invalid format: Secret key part is too short (minimum 16 characters after "sk_")';
519
+ }
520
+ return true;
521
+ }
117
522
  async function handleOAuthFlow(config) {
118
523
  console.log();
119
524
  console.log(chalk.yellow('🌐 Browser-Based Authentication'));
120
525
  console.log(chalk.gray('This will open your browser for secure authentication'));
121
526
  console.log();
122
- const { proceed } = await inquirer.prompt([
527
+ const { openBrowser } = await inquirer.prompt([
123
528
  {
124
529
  type: 'confirm',
125
- name: 'proceed',
530
+ name: 'openBrowser',
126
531
  message: 'Open browser for authentication?',
127
532
  default: true
128
533
  }
129
534
  ]);
130
- if (!proceed) {
131
- console.log(chalk.yellow('Authentication cancelled'));
535
+ if (!openBrowser) {
536
+ console.log(chalk.yellow('⚠️ Authentication cancelled'));
132
537
  return;
133
538
  }
134
539
  // Use the browser-based CLI login endpoint from MCP service
@@ -148,10 +553,30 @@ async function handleOAuthFlow(config) {
148
553
  type: 'input',
149
554
  name: 'token',
150
555
  message: 'Paste the authentication token from browser:',
151
- validate: (input) => {
556
+ validate: async (input) => {
152
557
  if (!input || input.trim().length === 0) {
153
558
  return 'Token is required';
154
559
  }
560
+ const trimmed = input.trim();
561
+ // Reject if user pasted a URL instead of token
562
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
563
+ return 'Please paste the TOKEN from the page, not the URL';
564
+ }
565
+ // Check token format - should start with 'cli_' or be a JWT
566
+ if (!trimmed.startsWith('cli_') && !trimmed.match(/^[\w-]+\.[\w-]+\.[\w-]+$/)) {
567
+ return 'Invalid token format. Expected format: cli_xxx or JWT token';
568
+ }
569
+ // Verify token with server
570
+ try {
571
+ const response = await apiClient.post('/auth/verify', { token: trimmed });
572
+ if (!response.valid) {
573
+ return 'Token verification failed. Please try again.';
574
+ }
575
+ }
576
+ catch (error) {
577
+ const errorMessage = error instanceof Error ? error.message : 'Server verification failed';
578
+ return `Token verification error: ${errorMessage}`;
579
+ }
155
580
  return true;
156
581
  }
157
582
  }
@@ -174,6 +599,8 @@ async function handleCredentialsFlow(options, config) {
174
599
  console.log();
175
600
  console.log(chalk.yellow('⚙️ Username/Password Authentication'));
176
601
  console.log();
602
+ // Check for authentication delay before attempting
603
+ await handleAuthDelay(config);
177
604
  let { email, password } = options;
178
605
  // Get credentials if not provided
179
606
  if (!email || !password) {
@@ -215,11 +642,12 @@ async function handleCredentialsFlow(options, config) {
215
642
  }
216
643
  catch (error) {
217
644
  spinner.fail('Login failed');
218
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
645
+ // Use enhanced error handling
646
+ await handleAuthenticationFailure(error, config, 'jwt');
647
+ // For 401 errors, offer registration option
219
648
  const errorResponse = error && typeof error === 'object' && 'response' in error ? error.response : null;
220
649
  if (errorResponse && typeof errorResponse === 'object' && 'status' in errorResponse && errorResponse.status === 401) {
221
- console.error(chalk.red('✖ Invalid email or password'));
222
- // Ask if they want to register
650
+ console.log();
223
651
  const answer = await inquirer.prompt([
224
652
  {
225
653
  type: 'confirm',
@@ -230,11 +658,9 @@ async function handleCredentialsFlow(options, config) {
230
658
  ]);
231
659
  if (answer.register) {
232
660
  await registerFlow(email);
661
+ return; // Don't exit if registration succeeds
233
662
  }
234
663
  }
235
- else {
236
- console.error(chalk.red('✖ Login failed:'), errorMessage);
237
- }
238
664
  process.exit(1);
239
665
  }
240
666
  }