@startanaicompany/cli 1.4.15 → 1.4.17
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/CLAUDE.md +221 -10
- package/bin/saac.js +19 -1
- package/package.json +1 -1
- package/src/commands/create.js +27 -6
- package/src/commands/deploy.js +41 -8
- package/src/commands/keys.js +143 -0
- package/src/commands/login.js +143 -42
- package/src/lib/api.js +68 -4
- package/src/lib/errorDisplay.js +170 -0
package/CLAUDE.md
CHANGED
|
@@ -275,6 +275,71 @@ saac create web -s web -r git@git... -t abc123 \
|
|
|
275
275
|
- Saves project config to `.saac/config.json` after successful creation
|
|
276
276
|
- Displays next steps and useful commands
|
|
277
277
|
|
|
278
|
+
**Deployment Behavior (NEW):**
|
|
279
|
+
|
|
280
|
+
The `create` command now **waits for the initial deployment to complete** (up to 5 minutes) before returning.
|
|
281
|
+
|
|
282
|
+
**Response Time:**
|
|
283
|
+
- Typical: 30-120 seconds
|
|
284
|
+
- Maximum: 5 minutes (timeout)
|
|
285
|
+
|
|
286
|
+
**Success Response:**
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"success": true,
|
|
290
|
+
"coolify_app_uuid": "...",
|
|
291
|
+
"app_name": "my-app",
|
|
292
|
+
"domain": "https://myapp.startanaicompany.com",
|
|
293
|
+
"deployment_status": "finished",
|
|
294
|
+
"deployment_uuid": "...",
|
|
295
|
+
"git_branch": "master"
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Failure Response (HTTP 200 with success: false):**
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"success": false,
|
|
303
|
+
"coolify_app_uuid": "...",
|
|
304
|
+
"app_name": "my-app",
|
|
305
|
+
"deployment_status": "failed",
|
|
306
|
+
"message": "Port 8080 is already in use. Remove host port bindings...",
|
|
307
|
+
"errors": [
|
|
308
|
+
{
|
|
309
|
+
"type": "PORT_CONFLICT",
|
|
310
|
+
"message": "Port 8080 is already in use...",
|
|
311
|
+
"detail": "Bind for 0.0.0.0:8080 failed..."
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
"relevant_logs": [...],
|
|
315
|
+
"last_logs": [...]
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Error Types:**
|
|
320
|
+
- `PORT_CONFLICT` - Host port binding conflict in docker-compose.yml
|
|
321
|
+
- `BUILD_FAILED` - Build process returned non-zero exit code
|
|
322
|
+
- `TIMEOUT` - Deployment didn't complete in 5 minutes
|
|
323
|
+
- `UNKNOWN` - Generic deployment failure
|
|
324
|
+
|
|
325
|
+
**Important Notes:**
|
|
326
|
+
1. **Application is created even if deployment fails** - the UUID is saved to `.saac/config.json`
|
|
327
|
+
2. Failed deployments return HTTP 200 (not 4xx) with `success: false`
|
|
328
|
+
3. CLI must check the `success` field, not just HTTP status code
|
|
329
|
+
4. Detailed error information is displayed with actionable advice
|
|
330
|
+
5. User can fix the issue and run `saac deploy` to retry
|
|
331
|
+
|
|
332
|
+
**Error Display:**
|
|
333
|
+
The CLI displays comprehensive error information:
|
|
334
|
+
- Error summary message
|
|
335
|
+
- Structured error details with types
|
|
336
|
+
- Relevant error logs (filtered)
|
|
337
|
+
- Last log lines for context
|
|
338
|
+
- Actionable advice based on error type:
|
|
339
|
+
- `PORT_CONFLICT`: Remove host port bindings from docker-compose.yml
|
|
340
|
+
- `BUILD_FAILED`: Check Dockerfile, run `docker build .` locally
|
|
341
|
+
- `TIMEOUT`: Check `saac status` and `saac logs`, may still be running
|
|
342
|
+
|
|
278
343
|
### Update Command Implementation
|
|
279
344
|
|
|
280
345
|
The `update` command allows modifying application configuration after deployment using `PATCH /api/v1/applications/:uuid`.
|
|
@@ -373,7 +438,7 @@ saac git disconnect git.startanaicompany.com
|
|
|
373
438
|
|
|
374
439
|
### Deploy Command Implementation
|
|
375
440
|
|
|
376
|
-
The `deploy` command triggers deployment for
|
|
441
|
+
The `deploy` command triggers deployment and **waits for completion** (up to 5 minutes).
|
|
377
442
|
|
|
378
443
|
**Usage:**
|
|
379
444
|
```bash
|
|
@@ -385,16 +450,155 @@ saac deploy --force
|
|
|
385
450
|
1. Validates authentication (session token not expired)
|
|
386
451
|
2. Checks for project config (`.saac/config.json`)
|
|
387
452
|
3. Makes POST request to `/api/v1/applications/:uuid/deploy`
|
|
388
|
-
4.
|
|
389
|
-
5.
|
|
453
|
+
4. **Waits for deployment to complete** (up to 5 minutes)
|
|
454
|
+
5. Displays deployment status with detailed error information on failure
|
|
390
455
|
|
|
391
|
-
**Response
|
|
392
|
-
-
|
|
393
|
-
-
|
|
394
|
-
|
|
456
|
+
**Response Time:**
|
|
457
|
+
- Typical: 30-120 seconds
|
|
458
|
+
- Maximum: 5 minutes (timeout)
|
|
459
|
+
|
|
460
|
+
**Success Response:**
|
|
461
|
+
```json
|
|
462
|
+
{
|
|
463
|
+
"success": true,
|
|
464
|
+
"status": "finished",
|
|
465
|
+
"deployment_uuid": "...",
|
|
466
|
+
"git_branch": "master",
|
|
467
|
+
"domain": "https://myapp.startanaicompany.com",
|
|
468
|
+
"traefik_status": "queued"
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Failure Response:**
|
|
473
|
+
```json
|
|
474
|
+
{
|
|
475
|
+
"success": false,
|
|
476
|
+
"status": "failed",
|
|
477
|
+
"message": "Build failed with exit code 1",
|
|
478
|
+
"errors": [
|
|
479
|
+
{
|
|
480
|
+
"type": "BUILD_FAILED",
|
|
481
|
+
"message": "Build failed with exit code 1",
|
|
482
|
+
"detail": "npm ERR! code ELIFECYCLE..."
|
|
483
|
+
}
|
|
484
|
+
],
|
|
485
|
+
"relevant_logs": [...],
|
|
486
|
+
"last_logs": [...]
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Error Types:**
|
|
491
|
+
- `PORT_CONFLICT` - Host port binding conflict
|
|
492
|
+
- `BUILD_FAILED` - Build process failed
|
|
493
|
+
- `TIMEOUT` - Deployment didn't complete in 5 minutes
|
|
494
|
+
- `UNKNOWN` - Generic failure
|
|
495
|
+
|
|
496
|
+
**Error Display:**
|
|
497
|
+
The CLI displays:
|
|
498
|
+
1. Error summary message
|
|
499
|
+
2. Structured error details with types
|
|
500
|
+
3. Relevant logs (filtered error logs)
|
|
501
|
+
4. Last 5 log lines for context
|
|
502
|
+
5. Actionable advice based on error type
|
|
503
|
+
6. Suggestion to view full logs with `saac logs --follow`
|
|
395
504
|
|
|
396
505
|
**Note:** The `--force` flag is defined in the CLI but not currently used by the API.
|
|
397
506
|
|
|
507
|
+
### OTP-Based Login (NEW in 1.4.16)
|
|
508
|
+
|
|
509
|
+
The login command now supports two authentication methods:
|
|
510
|
+
|
|
511
|
+
**Method 1: Login with API Key (Fast Path)**
|
|
512
|
+
```bash
|
|
513
|
+
saac login -e user@example.com -k cw_abc123
|
|
514
|
+
# → Immediate session token, no email verification needed
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Method 2: Login with OTP (Recovery Path)**
|
|
518
|
+
```bash
|
|
519
|
+
# Step 1: Request OTP
|
|
520
|
+
saac login -e user@example.com
|
|
521
|
+
# → Verification code sent to email (6 digits, 5-minute expiration)
|
|
522
|
+
|
|
523
|
+
# Step 2: Verify OTP
|
|
524
|
+
saac login -e user@example.com --otp 123456
|
|
525
|
+
# → Session token created, user is now logged in
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Why This Feature?**
|
|
529
|
+
|
|
530
|
+
Solves the **API key lockout problem**:
|
|
531
|
+
- User loses API key
|
|
532
|
+
- All sessions expire
|
|
533
|
+
- Cannot login (no API key)
|
|
534
|
+
- Cannot regenerate key (requires login)
|
|
535
|
+
- **LOCKED OUT** ❌
|
|
536
|
+
|
|
537
|
+
With OTP login:
|
|
538
|
+
- User can always recover via email ✅
|
|
539
|
+
- No support tickets needed ✅
|
|
540
|
+
- Self-service account recovery ✅
|
|
541
|
+
|
|
542
|
+
**Implementation Details:**
|
|
543
|
+
|
|
544
|
+
The `login.js` command now has three modes:
|
|
545
|
+
1. **API key login** - If `-k` flag provided (existing flow)
|
|
546
|
+
2. **OTP request** - If no `-k` or `--otp` flag (new flow)
|
|
547
|
+
3. **OTP verification** - If `--otp` flag provided (new flow)
|
|
548
|
+
|
|
549
|
+
**Backend Requirements:**
|
|
550
|
+
- `POST /api/v1/auth/login-otp` - Generate and send OTP
|
|
551
|
+
- `POST /api/v1/auth/verify-otp` - Verify OTP and create session
|
|
552
|
+
|
|
553
|
+
See `/data/sharedinfo/login-feature-update.md` for complete API specifications.
|
|
554
|
+
|
|
555
|
+
### API Key Management (NEW in 1.4.16)
|
|
556
|
+
|
|
557
|
+
Users can now regenerate their API keys if lost or compromised.
|
|
558
|
+
|
|
559
|
+
**Commands:**
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
# Regenerate API key (requires authentication)
|
|
563
|
+
saac keys regenerate
|
|
564
|
+
# → Shows new API key (only once!)
|
|
565
|
+
|
|
566
|
+
# Show API key info
|
|
567
|
+
saac keys show
|
|
568
|
+
# → Displays key prefix, created date, last used
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Recovery Flow:**
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
# 1. User loses API key but is not logged in
|
|
575
|
+
saac login -e user@example.com
|
|
576
|
+
# → OTP sent to email
|
|
577
|
+
|
|
578
|
+
# 2. Verify OTP
|
|
579
|
+
saac login -e user@example.com --otp 123456
|
|
580
|
+
# → Logged in with session token
|
|
581
|
+
|
|
582
|
+
# 3. Generate new API key
|
|
583
|
+
saac keys regenerate
|
|
584
|
+
# → New API key: cw_new_key_xyz...
|
|
585
|
+
|
|
586
|
+
# 4. On next machine, use new API key
|
|
587
|
+
saac login -e user@example.com -k cw_new_key_xyz...
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Security Notes:**
|
|
591
|
+
- Regenerating API key invalidates the old key immediately
|
|
592
|
+
- Existing session tokens remain valid (no disruption)
|
|
593
|
+
- Email notification sent when key is regenerated
|
|
594
|
+
- Full API key shown only once (must be saved)
|
|
595
|
+
|
|
596
|
+
**Backend Requirements:**
|
|
597
|
+
- `POST /api/v1/users/regenerate-key` - Generate new API key
|
|
598
|
+
- `GET /api/v1/users/api-key` - Get API key info (optional)
|
|
599
|
+
|
|
600
|
+
See `/data/sharedinfo/login-feature-update.md` for complete specifications.
|
|
601
|
+
|
|
398
602
|
### Init Command Implementation
|
|
399
603
|
|
|
400
604
|
The `init` command links an existing SAAC application to the current directory.
|
|
@@ -493,19 +697,26 @@ The wrapper API expects Git repositories to be hosted on the StartAnAiCompany Gi
|
|
|
493
697
|
- During registration, Gitea username can be auto-detected or manually provided
|
|
494
698
|
- Applications reference repositories in the format: `git@git.startanaicompany.com:user/repo.git`
|
|
495
699
|
|
|
496
|
-
## Current Status - Version 1.4.
|
|
700
|
+
## Current Status - Version 1.4.17
|
|
497
701
|
|
|
498
702
|
### Completed Features
|
|
499
703
|
|
|
500
704
|
**Authentication & Sessions:**
|
|
501
705
|
- ✅ `saac register` - Register with email only (git_username auto-detected from email)
|
|
502
|
-
- ✅ `saac login` -
|
|
706
|
+
- ✅ `saac login` - Two methods: API key (fast) or OTP (recovery)
|
|
707
|
+
- `saac login -e email -k api-key` - Login with API key
|
|
708
|
+
- `saac login -e email` - Request OTP via email
|
|
709
|
+
- `saac login -e email --otp code` - Verify OTP and login
|
|
503
710
|
- ✅ `saac verify` - Email verification, shows FULL API key for user to save
|
|
504
711
|
- ✅ `saac logout` - Logout from current device
|
|
505
712
|
- ✅ `saac logout-all` - Revoke all sessions
|
|
506
713
|
- ✅ `saac sessions` - List all active sessions
|
|
507
714
|
- ✅ `saac whoami` - Show current user info
|
|
508
715
|
|
|
716
|
+
**API Key Management (NEW in 1.4.16):**
|
|
717
|
+
- ✅ `saac keys regenerate` - Generate new API key (invalidates old one)
|
|
718
|
+
- ✅ `saac keys show` - Show API key information (prefix, created, last used)
|
|
719
|
+
|
|
509
720
|
**Git OAuth (NEW in 1.4.0):**
|
|
510
721
|
- ✅ `saac git connect [host]` - OAuth flow for Git authentication
|
|
511
722
|
- ✅ `saac git list` - List connected Git accounts
|
|
@@ -706,4 +917,4 @@ Before publishing to npm:
|
|
|
706
917
|
- `dotenv` - Environment variables
|
|
707
918
|
- `open` - Open browser for OAuth (v8.4.2 for compatibility with chalk v4)
|
|
708
919
|
|
|
709
|
-
**Version:** 1.4.
|
|
920
|
+
**Version:** 1.4.17 (current)
|
package/bin/saac.js
CHANGED
|
@@ -16,6 +16,7 @@ const verify = require('../src/commands/verify');
|
|
|
16
16
|
const logout = require('../src/commands/logout');
|
|
17
17
|
const logoutAll = require('../src/commands/logoutAll');
|
|
18
18
|
const sessions = require('../src/commands/sessions');
|
|
19
|
+
const keys = require('../src/commands/keys');
|
|
19
20
|
const git = require('../src/commands/git');
|
|
20
21
|
const init = require('../src/commands/init');
|
|
21
22
|
const create = require('../src/commands/create');
|
|
@@ -56,7 +57,8 @@ program
|
|
|
56
57
|
.command('login')
|
|
57
58
|
.description('Login with existing account')
|
|
58
59
|
.option('-e, --email <email>', 'Email address')
|
|
59
|
-
.option('-k, --api-key <key>', 'API key')
|
|
60
|
+
.option('-k, --api-key <key>', 'API key (for fast login)')
|
|
61
|
+
.option('--otp <code>', 'One-time password from email')
|
|
60
62
|
.action(login);
|
|
61
63
|
|
|
62
64
|
program
|
|
@@ -80,6 +82,22 @@ program
|
|
|
80
82
|
.description('List all active sessions')
|
|
81
83
|
.action(sessions);
|
|
82
84
|
|
|
85
|
+
// API Key management
|
|
86
|
+
const keysCommand = program
|
|
87
|
+
.command('keys')
|
|
88
|
+
.description('Manage API keys');
|
|
89
|
+
|
|
90
|
+
keysCommand
|
|
91
|
+
.command('regenerate')
|
|
92
|
+
.description('Generate a new API key (invalidates old one)')
|
|
93
|
+
.action(keys.regenerate);
|
|
94
|
+
|
|
95
|
+
keysCommand
|
|
96
|
+
.command('show')
|
|
97
|
+
.alias('info')
|
|
98
|
+
.description('Show API key information')
|
|
99
|
+
.action(keys.show);
|
|
100
|
+
|
|
83
101
|
// Git OAuth commands
|
|
84
102
|
const gitCommand = program
|
|
85
103
|
.command('git')
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -8,6 +8,7 @@ const logger = require('../lib/logger');
|
|
|
8
8
|
const oauth = require('../lib/oauth');
|
|
9
9
|
const inquirer = require('inquirer');
|
|
10
10
|
const { execSync } = require('child_process');
|
|
11
|
+
const errorDisplay = require('../lib/errorDisplay');
|
|
11
12
|
|
|
12
13
|
async function create(name, options) {
|
|
13
14
|
try {
|
|
@@ -281,14 +282,12 @@ async function create(name, options) {
|
|
|
281
282
|
|
|
282
283
|
logger.newline();
|
|
283
284
|
|
|
284
|
-
const spin = logger.spinner('Creating application...').start();
|
|
285
|
+
const spin = logger.spinner('Creating application and deploying (this may take up to 5 minutes)...').start();
|
|
285
286
|
|
|
286
287
|
try {
|
|
287
288
|
const result = await api.createApplication(appData);
|
|
288
289
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Save project configuration
|
|
290
|
+
// Always save project configuration (even if deployment failed)
|
|
292
291
|
saveProjectConfig({
|
|
293
292
|
applicationUuid: result.coolify_app_uuid,
|
|
294
293
|
applicationName: result.app_name,
|
|
@@ -297,13 +296,35 @@ async function create(name, options) {
|
|
|
297
296
|
gitRepository: appData.git_repository,
|
|
298
297
|
});
|
|
299
298
|
|
|
299
|
+
// Check if deployment failed
|
|
300
|
+
if (result.success === false) {
|
|
301
|
+
spin.fail('Deployment failed');
|
|
302
|
+
|
|
303
|
+
// Display detailed error information
|
|
304
|
+
errorDisplay.displayDeploymentError(result, logger);
|
|
305
|
+
|
|
306
|
+
// Show recovery instructions
|
|
307
|
+
errorDisplay.displayCreateRecoveryInstructions(result, logger);
|
|
308
|
+
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// SUCCESS: Application created and deployed
|
|
313
|
+
spin.succeed('Application created and deployed successfully!');
|
|
314
|
+
|
|
300
315
|
logger.newline();
|
|
301
|
-
logger.success('
|
|
316
|
+
logger.success('Your application is live!');
|
|
302
317
|
logger.newline();
|
|
303
318
|
logger.field('Name', result.app_name);
|
|
304
319
|
logger.field('Domain', result.domain);
|
|
305
320
|
logger.field('UUID', result.coolify_app_uuid);
|
|
306
|
-
logger.field('Status', result.deployment_status);
|
|
321
|
+
logger.field('Status', result.deployment_status || 'finished');
|
|
322
|
+
if (result.git_branch) {
|
|
323
|
+
logger.field('Branch', result.git_branch);
|
|
324
|
+
}
|
|
325
|
+
if (result.deployment_uuid) {
|
|
326
|
+
logger.field('Deployment ID', result.deployment_uuid);
|
|
327
|
+
}
|
|
307
328
|
logger.newline();
|
|
308
329
|
|
|
309
330
|
// Show next steps
|
package/src/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
6
|
const { getProjectConfig, isAuthenticated } = require('../lib/config');
|
|
7
7
|
const logger = require('../lib/logger');
|
|
8
|
+
const errorDisplay = require('../lib/errorDisplay');
|
|
8
9
|
|
|
9
10
|
async function deploy(options) {
|
|
10
11
|
try {
|
|
@@ -25,30 +26,62 @@ async function deploy(options) {
|
|
|
25
26
|
const { applicationUuid, applicationName } = projectConfig;
|
|
26
27
|
|
|
27
28
|
logger.section(`Deploying ${applicationName}`);
|
|
29
|
+
logger.newline();
|
|
28
30
|
|
|
29
|
-
const spin = logger.spinner('
|
|
31
|
+
const spin = logger.spinner('Deploying application (waiting for completion, up to 5 minutes)...').start();
|
|
30
32
|
|
|
31
33
|
try {
|
|
32
34
|
const result = await api.deployApplication(applicationUuid);
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// Check if deployment failed
|
|
37
|
+
if (result.success === false) {
|
|
38
|
+
spin.fail('Deployment failed');
|
|
39
|
+
|
|
40
|
+
// Display detailed error information
|
|
41
|
+
errorDisplay.displayDeploymentError(result, logger);
|
|
42
|
+
|
|
43
|
+
// Handle timeout specifically
|
|
44
|
+
if (result.status === 'timeout') {
|
|
45
|
+
errorDisplay.displayTimeoutInstructions(logger);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// SUCCESS: Deployment completed
|
|
52
|
+
spin.succeed('Deployment completed successfully!');
|
|
35
53
|
|
|
36
54
|
logger.newline();
|
|
37
|
-
logger.success('
|
|
55
|
+
logger.success('Your application has been deployed!');
|
|
38
56
|
logger.newline();
|
|
39
57
|
logger.field('Application', applicationName);
|
|
40
58
|
logger.field('Status', result.status);
|
|
59
|
+
if (result.git_branch) {
|
|
60
|
+
logger.field('Branch', result.git_branch);
|
|
61
|
+
}
|
|
41
62
|
if (result.domain) {
|
|
42
63
|
logger.field('Domain', result.domain);
|
|
43
64
|
}
|
|
44
|
-
|
|
65
|
+
if (result.deployment_uuid || result.deployment_id) {
|
|
66
|
+
logger.field('Deployment ID', result.deployment_uuid || result.deployment_id);
|
|
67
|
+
}
|
|
45
68
|
logger.newline();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
69
|
+
|
|
70
|
+
// Show Traefik status if present
|
|
71
|
+
if (result.traefik_status === 'queued') {
|
|
72
|
+
logger.info('Routing configuration is being applied (may take a few seconds)');
|
|
73
|
+
logger.newline();
|
|
74
|
+
} else if (result.traefik_status === 'failed') {
|
|
75
|
+
logger.warn('Routing configuration failed - application may not be accessible');
|
|
76
|
+
logger.newline();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.info('Useful commands:');
|
|
80
|
+
logger.log(` saac logs --follow View live deployment logs`);
|
|
81
|
+
logger.log(` saac status Check application status`);
|
|
49
82
|
|
|
50
83
|
} catch (error) {
|
|
51
|
-
spin.fail('Deployment failed');
|
|
84
|
+
spin.fail('Deployment request failed');
|
|
52
85
|
throw error;
|
|
53
86
|
}
|
|
54
87
|
} catch (error) {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key management commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const api = require('../lib/api');
|
|
6
|
+
const { getUser, isAuthenticated } = require('../lib/config');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regenerate API key
|
|
12
|
+
*/
|
|
13
|
+
async function regenerate() {
|
|
14
|
+
try {
|
|
15
|
+
// Must be authenticated (via session token)
|
|
16
|
+
if (!isAuthenticated()) {
|
|
17
|
+
logger.error('Not logged in');
|
|
18
|
+
logger.newline();
|
|
19
|
+
logger.info('You must be logged in to regenerate your API key');
|
|
20
|
+
logger.newline();
|
|
21
|
+
logger.info('Login using email verification:');
|
|
22
|
+
logger.log(' saac login -e <email> # Request OTP');
|
|
23
|
+
logger.log(' saac login -e <email> --otp <code> # Verify OTP');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const user = getUser();
|
|
28
|
+
|
|
29
|
+
logger.section('Regenerate API Key');
|
|
30
|
+
logger.newline();
|
|
31
|
+
|
|
32
|
+
logger.warn('This will generate a new API key and invalidate your current one.');
|
|
33
|
+
logger.warn('Your active session tokens will continue to work.');
|
|
34
|
+
logger.newline();
|
|
35
|
+
|
|
36
|
+
// Confirmation prompt
|
|
37
|
+
const { confirm } = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'confirm',
|
|
40
|
+
name: 'confirm',
|
|
41
|
+
message: 'Are you sure you want to regenerate your API key?',
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
if (!confirm) {
|
|
47
|
+
logger.info('Cancelled');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spin = logger.spinner('Generating new API key...').start();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await api.regenerateApiKey();
|
|
55
|
+
|
|
56
|
+
spin.succeed('New API key generated!');
|
|
57
|
+
|
|
58
|
+
logger.newline();
|
|
59
|
+
logger.success('Your new API key:');
|
|
60
|
+
logger.newline();
|
|
61
|
+
|
|
62
|
+
// Show FULL API key (it's only shown once)
|
|
63
|
+
logger.field('API Key', result.api_key);
|
|
64
|
+
|
|
65
|
+
logger.newline();
|
|
66
|
+
logger.warn('Save this key securely. It will not be shown again.');
|
|
67
|
+
logger.newline();
|
|
68
|
+
|
|
69
|
+
logger.info('Your existing sessions remain active.');
|
|
70
|
+
logger.info('Use this new key for future logins or CI/CD.');
|
|
71
|
+
logger.newline();
|
|
72
|
+
|
|
73
|
+
logger.info('To login with the new key:');
|
|
74
|
+
logger.log(` saac login -e ${user.email} -k ${result.api_key}`);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
spin.fail('Failed to regenerate API key');
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error(error.response?.data?.message || error.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Show API key info (without revealing full key)
|
|
88
|
+
*/
|
|
89
|
+
async function show() {
|
|
90
|
+
try {
|
|
91
|
+
if (!isAuthenticated()) {
|
|
92
|
+
logger.error('Not logged in');
|
|
93
|
+
logger.newline();
|
|
94
|
+
logger.info('Login first:');
|
|
95
|
+
logger.log(' saac login -e <email>');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.section('API Key Information');
|
|
100
|
+
logger.newline();
|
|
101
|
+
|
|
102
|
+
const spin = logger.spinner('Fetching API key info...').start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await api.getApiKeyInfo();
|
|
106
|
+
|
|
107
|
+
spin.succeed('API key info retrieved');
|
|
108
|
+
|
|
109
|
+
logger.newline();
|
|
110
|
+
logger.field('Key Prefix', result.key_prefix); // e.g., "cw_RJ1gH8..."
|
|
111
|
+
logger.field('Created', new Date(result.created_at).toLocaleDateString());
|
|
112
|
+
logger.field('Last Used', result.last_used_at
|
|
113
|
+
? new Date(result.last_used_at).toLocaleString()
|
|
114
|
+
: 'Never');
|
|
115
|
+
|
|
116
|
+
logger.newline();
|
|
117
|
+
logger.info('Commands:');
|
|
118
|
+
logger.log(' saac keys regenerate Generate new API key');
|
|
119
|
+
logger.log(' saac sessions View active sessions');
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
spin.fail('Failed to fetch API key info');
|
|
123
|
+
|
|
124
|
+
// If endpoint doesn't exist yet, show helpful message
|
|
125
|
+
if (error.response?.status === 404) {
|
|
126
|
+
logger.newline();
|
|
127
|
+
logger.warn('API key info endpoint not available yet');
|
|
128
|
+
logger.info('You can still regenerate your key with:');
|
|
129
|
+
logger.log(' saac keys regenerate');
|
|
130
|
+
} else {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error(error.response?.data?.message || error.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
regenerate,
|
|
142
|
+
show,
|
|
143
|
+
};
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Login command
|
|
2
|
+
* Login command - Two methods:
|
|
3
|
+
* 1. With API key (fast): saac login -e email -k api_key
|
|
4
|
+
* 2. With OTP (recovery): saac login -e email, then saac login -e email --otp code
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const inquirer = require('inquirer');
|
|
@@ -10,21 +12,23 @@ const logger = require('../lib/logger');
|
|
|
10
12
|
|
|
11
13
|
async function login(options) {
|
|
12
14
|
try {
|
|
13
|
-
// Require
|
|
14
|
-
if (!options.email
|
|
15
|
-
logger.error('Email
|
|
15
|
+
// Require email flag
|
|
16
|
+
if (!options.email) {
|
|
17
|
+
logger.error('Email is required');
|
|
16
18
|
logger.newline();
|
|
17
19
|
logger.info('Usage:');
|
|
18
|
-
logger.log(' saac login -e <email> -k <api-key>');
|
|
19
|
-
logger.log(' saac login
|
|
20
|
+
logger.log(' saac login -e <email> -k <api-key> # Login with API key');
|
|
21
|
+
logger.log(' saac login -e <email> # Request OTP via email');
|
|
22
|
+
logger.log(' saac login -e <email> --otp <code> # Verify OTP and login');
|
|
20
23
|
logger.newline();
|
|
21
|
-
logger.info('
|
|
22
|
-
logger.log(' saac login -e user@example.com -k
|
|
24
|
+
logger.info('Examples:');
|
|
25
|
+
logger.log(' saac login -e user@example.com -k cw_abc123 # Fast login');
|
|
26
|
+
logger.log(' saac login -e user@example.com # Send OTP to email');
|
|
27
|
+
logger.log(' saac login -e user@example.com --otp 123456 # Verify OTP');
|
|
23
28
|
process.exit(1);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
const email = options.email;
|
|
27
|
-
const apiKey = options.apiKey;
|
|
28
32
|
|
|
29
33
|
// Validate email format
|
|
30
34
|
if (!validator.isEmail(email)) {
|
|
@@ -33,46 +37,143 @@ async function login(options) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
logger.section('Login to StartAnAiCompany');
|
|
40
|
+
logger.newline();
|
|
36
41
|
|
|
37
|
-
// Login
|
|
38
|
-
|
|
42
|
+
// CASE 1: Login with API key (fast path)
|
|
43
|
+
if (options.apiKey) {
|
|
44
|
+
return await loginWithApiKey(email, options.apiKey);
|
|
45
|
+
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
// CASE 2: Verify OTP (second step)
|
|
48
|
+
if (options.otp) {
|
|
49
|
+
return await verifyOtpAndLogin(email, options.otp);
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
// CASE 3: Request OTP (first step)
|
|
53
|
+
return await requestOtp(email);
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expiresAt: result.expires_at,
|
|
52
|
-
verified: result.user.verified,
|
|
53
|
-
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(error.response?.data?.message || error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Login with API key (existing flow)
|
|
63
|
+
*/
|
|
64
|
+
async function loginWithApiKey(email, apiKey) {
|
|
65
|
+
const spin = logger.spinner('Logging in with API key...').start();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await api.login(email, apiKey);
|
|
69
|
+
|
|
70
|
+
spin.succeed('Login successful!');
|
|
71
|
+
|
|
72
|
+
// Save session token
|
|
73
|
+
saveUser({
|
|
74
|
+
email: result.user.email || email,
|
|
75
|
+
userId: result.user.id,
|
|
76
|
+
sessionToken: result.session_token,
|
|
77
|
+
expiresAt: result.expires_at,
|
|
78
|
+
verified: result.user.verified,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
logger.newline();
|
|
82
|
+
logger.success('You are now logged in!');
|
|
83
|
+
logger.newline();
|
|
84
|
+
logger.field('Email', result.user.email || email);
|
|
85
|
+
logger.field('Verified', result.user.verified ? 'Yes' : 'No');
|
|
86
|
+
|
|
87
|
+
if (result.expires_at) {
|
|
88
|
+
const expirationDate = new Date(result.expires_at);
|
|
89
|
+
logger.field('Session expires', expirationDate.toLocaleDateString());
|
|
70
90
|
}
|
|
91
|
+
|
|
71
92
|
} catch (error) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
spin.fail('Login failed');
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request OTP via email (new flow - step 1)
|
|
100
|
+
*/
|
|
101
|
+
async function requestOtp(email) {
|
|
102
|
+
const spin = logger.spinner('Sending verification code to email...').start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await api.requestLoginOtp(email);
|
|
106
|
+
|
|
107
|
+
spin.succeed('Verification code sent!');
|
|
108
|
+
|
|
109
|
+
logger.newline();
|
|
110
|
+
logger.success(`A verification code has been sent to ${logger.chalk.cyan(email)}`);
|
|
111
|
+
logger.newline();
|
|
112
|
+
logger.info('Check your email and run:');
|
|
113
|
+
logger.log(` ${logger.chalk.yellow(`saac login -e ${email} --otp <code>`)}`);
|
|
114
|
+
logger.newline();
|
|
115
|
+
logger.warn('Note: Check MailHog at https://mailhog.goryan.io for the verification code');
|
|
116
|
+
logger.newline();
|
|
117
|
+
|
|
118
|
+
if (result.otp_expires_at) {
|
|
119
|
+
const expiresAt = new Date(result.otp_expires_at);
|
|
120
|
+
const now = new Date();
|
|
121
|
+
const minutesLeft = Math.ceil((expiresAt - now) / 60000);
|
|
122
|
+
logger.info(`Code expires in ${minutesLeft} minutes`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
spin.fail('Failed to send verification code');
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify OTP and complete login (new flow - step 2)
|
|
133
|
+
*/
|
|
134
|
+
async function verifyOtpAndLogin(email, otpCode) {
|
|
135
|
+
const spin = logger.spinner('Verifying code...').start();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const result = await api.verifyLoginOtp(email, otpCode);
|
|
139
|
+
|
|
140
|
+
spin.succeed('Verification successful!');
|
|
141
|
+
|
|
142
|
+
// Save session token
|
|
143
|
+
saveUser({
|
|
144
|
+
email: result.user.email || email,
|
|
145
|
+
userId: result.user.id,
|
|
146
|
+
sessionToken: result.session_token,
|
|
147
|
+
expiresAt: result.expires_at,
|
|
148
|
+
verified: result.user.verified,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
logger.newline();
|
|
152
|
+
logger.success('You are now logged in!');
|
|
153
|
+
logger.newline();
|
|
154
|
+
logger.field('Email', result.user.email || email);
|
|
155
|
+
logger.field('Verified', result.user.verified ? 'Yes' : 'No');
|
|
156
|
+
|
|
157
|
+
if (result.expires_at) {
|
|
158
|
+
const expirationDate = new Date(result.expires_at);
|
|
159
|
+
logger.field('Session expires', expirationDate.toLocaleDateString());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.newline();
|
|
163
|
+
logger.info('💡 Tip: Generate an API key for faster future logins:');
|
|
164
|
+
logger.log(' saac keys regenerate');
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
spin.fail('Verification failed');
|
|
168
|
+
|
|
169
|
+
if (error.response?.status === 401) {
|
|
170
|
+
logger.newline();
|
|
171
|
+
logger.error('Invalid or expired verification code');
|
|
172
|
+
logger.info('Request a new code:');
|
|
173
|
+
logger.log(` saac login -e ${email}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw error;
|
|
76
177
|
}
|
|
77
178
|
}
|
|
78
179
|
|
package/src/lib/api.js
CHANGED
|
@@ -9,8 +9,9 @@ const pkg = require('../../package.json');
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Create axios instance with base configuration
|
|
12
|
+
* @param {number} timeout - Timeout in milliseconds (default: 30000)
|
|
12
13
|
*/
|
|
13
|
-
function createClient() {
|
|
14
|
+
function createClient(timeout = 30000) {
|
|
14
15
|
const user = getUser();
|
|
15
16
|
const envApiKey = process.env.SAAC_API_KEY; // For CI/CD
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ function createClient() {
|
|
|
33
34
|
|
|
34
35
|
return axios.create({
|
|
35
36
|
baseURL: getApiUrl(),
|
|
36
|
-
timeout:
|
|
37
|
+
timeout: timeout,
|
|
37
38
|
headers,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -88,9 +89,11 @@ async function getUserInfo() {
|
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* Create a new application
|
|
92
|
+
* Note: This waits for deployment to complete (up to 5 minutes)
|
|
91
93
|
*/
|
|
92
94
|
async function createApplication(appData) {
|
|
93
|
-
|
|
95
|
+
// Use 5-minute timeout for deployment waiting
|
|
96
|
+
const client = createClient(300000); // 5 minutes
|
|
94
97
|
const response = await client.post('/applications', appData);
|
|
95
98
|
return response.data;
|
|
96
99
|
}
|
|
@@ -115,9 +118,11 @@ async function getApplication(uuid) {
|
|
|
115
118
|
|
|
116
119
|
/**
|
|
117
120
|
* Deploy application
|
|
121
|
+
* Note: This waits for deployment to complete (up to 5 minutes)
|
|
118
122
|
*/
|
|
119
123
|
async function deployApplication(uuid) {
|
|
120
|
-
|
|
124
|
+
// Use 5-minute timeout for deployment waiting
|
|
125
|
+
const client = createClient(300000); // 5 minutes
|
|
121
126
|
const response = await client.post(`/applications/${uuid}/deploy`);
|
|
122
127
|
return response.data;
|
|
123
128
|
}
|
|
@@ -181,6 +186,61 @@ async function healthCheck() {
|
|
|
181
186
|
return response.data;
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Request login OTP (no API key required)
|
|
191
|
+
*/
|
|
192
|
+
async function requestLoginOtp(email) {
|
|
193
|
+
const client = axios.create({
|
|
194
|
+
baseURL: getApiUrl(),
|
|
195
|
+
timeout: 30000,
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const response = await client.post('/auth/login-otp', { email });
|
|
203
|
+
return response.data;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Verify login OTP and get session token
|
|
208
|
+
*/
|
|
209
|
+
async function verifyLoginOtp(email, otpCode) {
|
|
210
|
+
const client = axios.create({
|
|
211
|
+
baseURL: getApiUrl(),
|
|
212
|
+
timeout: 30000,
|
|
213
|
+
headers: {
|
|
214
|
+
'Content-Type': 'application/json',
|
|
215
|
+
'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const response = await client.post('/auth/verify-otp', {
|
|
220
|
+
email,
|
|
221
|
+
otp_code: otpCode,
|
|
222
|
+
});
|
|
223
|
+
return response.data;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Regenerate API key (requires authentication)
|
|
228
|
+
*/
|
|
229
|
+
async function regenerateApiKey() {
|
|
230
|
+
const client = createClient();
|
|
231
|
+
const response = await client.post('/users/regenerate-key');
|
|
232
|
+
return response.data;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get API key info (prefix, created date, last used)
|
|
237
|
+
*/
|
|
238
|
+
async function getApiKeyInfo() {
|
|
239
|
+
const client = createClient();
|
|
240
|
+
const response = await client.get('/users/api-key');
|
|
241
|
+
return response.data;
|
|
242
|
+
}
|
|
243
|
+
|
|
184
244
|
module.exports = {
|
|
185
245
|
createClient,
|
|
186
246
|
login,
|
|
@@ -197,4 +257,8 @@ module.exports = {
|
|
|
197
257
|
updateDomain,
|
|
198
258
|
deleteApplication,
|
|
199
259
|
healthCheck,
|
|
260
|
+
requestLoginOtp,
|
|
261
|
+
verifyLoginOtp,
|
|
262
|
+
regenerateApiKey,
|
|
263
|
+
getApiKeyInfo,
|
|
200
264
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Display Utilities
|
|
3
|
+
* Formats and displays deployment errors with actionable advice
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get actionable advice for specific error types
|
|
10
|
+
*/
|
|
11
|
+
function getErrorAdvice(errorType) {
|
|
12
|
+
const advice = {
|
|
13
|
+
PORT_CONFLICT: [
|
|
14
|
+
'Fix: Remove host port bindings from your docker-compose.yml',
|
|
15
|
+
' Change "8080:8080" to just "8080"',
|
|
16
|
+
' Traefik will handle external routing automatically',
|
|
17
|
+
],
|
|
18
|
+
BUILD_FAILED: [
|
|
19
|
+
'Fix: Check your Dockerfile and build configuration',
|
|
20
|
+
' Run "docker build ." locally to debug',
|
|
21
|
+
' Verify all dependencies are properly specified',
|
|
22
|
+
],
|
|
23
|
+
TIMEOUT: [
|
|
24
|
+
'Note: Deployment may still be running in the background',
|
|
25
|
+
' Check status with: saac status',
|
|
26
|
+
' View logs with: saac logs --follow',
|
|
27
|
+
],
|
|
28
|
+
UNKNOWN: [
|
|
29
|
+
'Tip: Check the deployment logs for more details',
|
|
30
|
+
' Run: saac logs --follow',
|
|
31
|
+
],
|
|
32
|
+
PARSE_ERROR: [
|
|
33
|
+
'Note: Could not parse deployment logs',
|
|
34
|
+
' Contact support if this persists',
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return advice[errorType] || advice.UNKNOWN;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format log lines for display
|
|
43
|
+
*/
|
|
44
|
+
function formatLogs(logs, maxLines = 10) {
|
|
45
|
+
if (!logs || logs.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If logs is an array of objects with 'output' field
|
|
50
|
+
if (logs[0] && typeof logs[0] === 'object' && logs[0].output) {
|
|
51
|
+
return logs.slice(-maxLines).map(log => log.output);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If logs is an array of strings
|
|
55
|
+
return logs.slice(-maxLines);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Display detailed deployment error information
|
|
60
|
+
*/
|
|
61
|
+
function displayDeploymentError(result, logger) {
|
|
62
|
+
logger.newline();
|
|
63
|
+
logger.error(`Deployment Error: ${result.message || 'Deployment failed'}`);
|
|
64
|
+
logger.newline();
|
|
65
|
+
|
|
66
|
+
// Display deployment info
|
|
67
|
+
if (result.deployment_status || result.status) {
|
|
68
|
+
logger.field('Status', result.deployment_status || result.status);
|
|
69
|
+
}
|
|
70
|
+
if (result.git_branch) {
|
|
71
|
+
logger.field('Branch', result.git_branch);
|
|
72
|
+
}
|
|
73
|
+
if (result.deployment_uuid) {
|
|
74
|
+
logger.field('Deployment ID', result.deployment_uuid);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.newline();
|
|
78
|
+
|
|
79
|
+
// Display structured errors
|
|
80
|
+
if (result.errors && result.errors.length > 0) {
|
|
81
|
+
logger.info('Error Details:');
|
|
82
|
+
result.errors.forEach(err => {
|
|
83
|
+
logger.log(` ${chalk.yellow(`[${err.type}]`)} ${err.message}`);
|
|
84
|
+
if (err.detail) {
|
|
85
|
+
const detailLines = err.detail.split('\n').slice(0, 3); // First 3 lines
|
|
86
|
+
detailLines.forEach(line => {
|
|
87
|
+
logger.log(` ${chalk.gray(line)}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
logger.newline();
|
|
92
|
+
|
|
93
|
+
// Display actionable advice for the first error
|
|
94
|
+
const firstError = result.errors[0];
|
|
95
|
+
if (firstError && firstError.type) {
|
|
96
|
+
const advice = getErrorAdvice(firstError.type);
|
|
97
|
+
if (advice && advice.length > 0) {
|
|
98
|
+
logger.info('Suggested Fix:');
|
|
99
|
+
advice.forEach(line => {
|
|
100
|
+
logger.log(` ${chalk.cyan(line)}`);
|
|
101
|
+
});
|
|
102
|
+
logger.newline();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Display relevant logs (filtered error logs)
|
|
108
|
+
if (result.relevant_logs && result.relevant_logs.length > 0) {
|
|
109
|
+
logger.info('Relevant Logs:');
|
|
110
|
+
const logLines = formatLogs(result.relevant_logs, 5);
|
|
111
|
+
logLines.forEach(line => {
|
|
112
|
+
logger.log(` ${chalk.gray(line)}`);
|
|
113
|
+
});
|
|
114
|
+
logger.newline();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Display last logs (context)
|
|
118
|
+
if (result.last_logs && result.last_logs.length > 0) {
|
|
119
|
+
logger.info('Recent Log Output:');
|
|
120
|
+
const logLines = formatLogs(result.last_logs, 5);
|
|
121
|
+
logLines.forEach(line => {
|
|
122
|
+
logger.log(` ${chalk.gray(line)}`);
|
|
123
|
+
});
|
|
124
|
+
logger.newline();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Suggest viewing full logs
|
|
128
|
+
if (result.coolify_app_uuid || result.deployment_uuid) {
|
|
129
|
+
const uuid = result.coolify_app_uuid || result.deployment_uuid;
|
|
130
|
+
logger.info('View full logs:');
|
|
131
|
+
logger.log(` ${chalk.yellow('saac logs --follow')}`);
|
|
132
|
+
logger.newline();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Display recovery instructions after failed create
|
|
138
|
+
*/
|
|
139
|
+
function displayCreateRecoveryInstructions(result, logger) {
|
|
140
|
+
if (result.coolify_app_uuid) {
|
|
141
|
+
logger.warn(`Application "${result.app_name}" was created but deployment failed.`);
|
|
142
|
+
logger.info('Fix the issue in your repository, then redeploy:');
|
|
143
|
+
logger.log(` ${chalk.yellow('saac deploy')}`);
|
|
144
|
+
logger.newline();
|
|
145
|
+
logger.info('Or delete and recreate:');
|
|
146
|
+
logger.log(` ${chalk.yellow(`saac delete ${result.coolify_app_uuid}`)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Display timeout-specific instructions
|
|
152
|
+
*/
|
|
153
|
+
function displayTimeoutInstructions(logger) {
|
|
154
|
+
logger.warn('Deployment timed out after 5 minutes');
|
|
155
|
+
logger.newline();
|
|
156
|
+
logger.info('The deployment may still be running in the background.');
|
|
157
|
+
logger.info('Check the status:');
|
|
158
|
+
logger.log(` ${chalk.yellow('saac status')}`);
|
|
159
|
+
logger.newline();
|
|
160
|
+
logger.info('Or view live logs:');
|
|
161
|
+
logger.log(` ${chalk.yellow('saac logs --follow')}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
getErrorAdvice,
|
|
166
|
+
formatLogs,
|
|
167
|
+
displayDeploymentError,
|
|
168
|
+
displayCreateRecoveryInstructions,
|
|
169
|
+
displayTimeoutInstructions,
|
|
170
|
+
};
|