@startanaicompany/cli 1.4.16 → 1.4.18

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.
@@ -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
- spin.succeed('Application created successfully!');
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('Application created!');
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
@@ -1,4 +1,191 @@
1
- // TODO: Implement delete command
2
- module.exports = async function() {
3
- console.log('delete command - Coming soon!');
4
- };
1
+ /**
2
+ * Delete Command - Permanently delete application
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const inquirer = require('inquirer');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Delete application with confirmation
14
+ * @param {object} options - Command options
15
+ */
16
+ async function deleteApp(options) {
17
+ try {
18
+ // Check authentication
19
+ if (!isAuthenticated()) {
20
+ logger.error('Not logged in. Run: saac login');
21
+ process.exit(1);
22
+ }
23
+
24
+ // Check for project config
25
+ const projectConfig = getProjectConfig();
26
+ if (!projectConfig || !projectConfig.applicationUuid) {
27
+ logger.error('No application found in current directory');
28
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
29
+ logger.newline();
30
+ logger.info('Or initialize with:');
31
+ logger.log(' saac init');
32
+ process.exit(1);
33
+ }
34
+
35
+ const { applicationUuid } = projectConfig;
36
+
37
+ // Fetch application details for confirmation
38
+ const fetchSpin = logger.spinner('Fetching application details...').start();
39
+
40
+ let app;
41
+ try {
42
+ app = await api.getApplication(applicationUuid);
43
+ fetchSpin.succeed('Application details retrieved');
44
+ } catch (error) {
45
+ fetchSpin.fail('Failed to fetch application details');
46
+ throw error;
47
+ }
48
+
49
+ logger.newline();
50
+
51
+ // Show confirmation prompt (unless --yes flag)
52
+ if (!options.yes) {
53
+ logger.warn('WARNING: This will permanently delete your application!');
54
+ logger.newline();
55
+
56
+ logger.field('Application', app.name);
57
+ logger.field('Domain', app.domain || `https://${app.subdomain}.startanaicompany.com`);
58
+ logger.field('UUID', app.uuid);
59
+ logger.field('Status', app.status);
60
+
61
+ logger.newline();
62
+
63
+ logger.error('This action cannot be undone. All data will be lost:');
64
+ logger.log(' • Application configuration');
65
+ logger.log(' • All deployments');
66
+ logger.log(' • Environment variables');
67
+ logger.log(' • All logs');
68
+ logger.log(' • DNS records');
69
+
70
+ logger.newline();
71
+
72
+ const answers = await inquirer.prompt([
73
+ {
74
+ type: 'input',
75
+ name: 'confirmation',
76
+ message: 'Type \'yes\' to confirm deletion:',
77
+ validate: (input) => {
78
+ if (input === 'yes') {
79
+ return true;
80
+ }
81
+ return 'Please type \'yes\' to confirm';
82
+ }
83
+ }
84
+ ]);
85
+
86
+ if (answers.confirmation !== 'yes') {
87
+ logger.error('Deletion cancelled');
88
+ process.exit(0);
89
+ }
90
+
91
+ logger.newline();
92
+ }
93
+
94
+ // Delete application
95
+ const deleteSpin = logger.spinner('Deleting application...').start();
96
+
97
+ try {
98
+ const result = await api.deleteApplication(app.uuid);
99
+
100
+ deleteSpin.succeed('Application deleted successfully!');
101
+
102
+ logger.newline();
103
+
104
+ logger.info('Resources deleted:');
105
+ for (const [resource, deleted] of Object.entries(result.resources_deleted || {})) {
106
+ const icon = deleted ? '✓' : '✗';
107
+ const name = resource.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
108
+ const color = deleted ? logger.chalk.green : logger.chalk.red;
109
+ logger.log(` ${color(icon)} ${name}`);
110
+ }
111
+
112
+ logger.newline();
113
+
114
+ logger.success(`Application '${result.application_name}' has been permanently deleted.`);
115
+
116
+ logger.newline();
117
+
118
+ logger.info('Next steps:');
119
+ logger.log(' • Remove local project: rm -rf .saac/');
120
+ logger.log(' • Create new application: saac create');
121
+ logger.log(' • View other applications: saac list');
122
+
123
+ // Remove local config
124
+ const configDir = path.join(process.cwd(), '.saac');
125
+ if (fs.existsSync(configDir)) {
126
+ fs.rmSync(configDir, { recursive: true, force: true });
127
+ logger.newline();
128
+ logger.success('Local configuration removed');
129
+ }
130
+
131
+ } catch (error) {
132
+ deleteSpin.fail('Failed to delete application');
133
+
134
+ if (error.response?.status === 404) {
135
+ logger.newline();
136
+ logger.warn('Application not found or already deleted');
137
+ } else if (error.response?.status === 409) {
138
+ logger.newline();
139
+ const data = error.response.data;
140
+
141
+ if (data.error === 'DEPLOYMENT_IN_PROGRESS') {
142
+ logger.warn('Cannot delete application while deployment is in progress');
143
+ logger.newline();
144
+ logger.info('Details:');
145
+ logger.field(' Deployment Status', data.current_deployment_status);
146
+ if (data.deployment_uuid) {
147
+ logger.field(' Deployment UUID', data.deployment_uuid);
148
+ }
149
+ logger.newline();
150
+ logger.info('Suggestion:');
151
+ logger.log(' Wait for deployment to finish or fail before deleting');
152
+ logger.newline();
153
+ logger.info('Check deployment status:');
154
+ logger.log(' saac deployments');
155
+ }
156
+ } else if (error.response?.status === 500) {
157
+ logger.newline();
158
+ const data = error.response.data;
159
+
160
+ if (data.error === 'PARTIAL_DELETION') {
161
+ logger.warn('Application partially deleted. Some resources may remain.');
162
+ logger.newline();
163
+ logger.info('Cleanup status:');
164
+ if (data.details) {
165
+ for (const [resource, deleted] of Object.entries(data.details)) {
166
+ if (resource !== 'error') {
167
+ const icon = deleted ? '✓' : '✗';
168
+ const name = resource.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
169
+ logger.log(` ${icon} ${name}`);
170
+ }
171
+ }
172
+ if (data.details.error) {
173
+ logger.newline();
174
+ logger.error('Error: ' + data.details.error);
175
+ }
176
+ }
177
+ logger.newline();
178
+ logger.info('Please contact support to complete cleanup');
179
+ }
180
+ }
181
+
182
+ throw error;
183
+ }
184
+
185
+ } catch (error) {
186
+ logger.error(error.response?.data?.message || error.message);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ module.exports = deleteApp;
@@ -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('Triggering deployment...').start();
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
- spin.succeed('Deployment triggered!');
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('Deployment started successfully');
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
- logger.field('Deployment ID', result.deployment_id);
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
- logger.info(
47
- `View logs with: ${logger.chalk.yellow('saac logs --follow')}`
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,130 @@
1
+ /**
2
+ * Deployments command - List deployment history
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+
10
+ async function deployments(options) {
11
+ try {
12
+ // Check authentication
13
+ if (!isAuthenticated()) {
14
+ logger.error('Not logged in. Run: saac login');
15
+ process.exit(1);
16
+ }
17
+
18
+ // Check for project config
19
+ const projectConfig = getProjectConfig();
20
+ if (!projectConfig || !projectConfig.applicationUuid) {
21
+ logger.error('No application found in current directory');
22
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
23
+ logger.newline();
24
+ logger.info('Or initialize with:');
25
+ logger.log(' saac init');
26
+ process.exit(1);
27
+ }
28
+
29
+ const { applicationUuid, applicationName } = projectConfig;
30
+
31
+ logger.section(`Deployment History: ${applicationName}`);
32
+ logger.newline();
33
+
34
+ const spin = logger.spinner('Fetching deployment history...').start();
35
+
36
+ try {
37
+ // Build query parameters
38
+ const params = {};
39
+ if (options.limit) {
40
+ params.limit = parseInt(options.limit, 10);
41
+ }
42
+ if (options.offset) {
43
+ params.offset = parseInt(options.offset, 10);
44
+ }
45
+
46
+ const result = await api.getDeployments(applicationUuid, params);
47
+
48
+ spin.succeed('Deployment history retrieved');
49
+
50
+ if (!result.deployments || result.deployments.length === 0) {
51
+ logger.newline();
52
+ logger.warn('No deployments found for this application');
53
+ logger.newline();
54
+ logger.info('Deploy your application with:');
55
+ logger.log(' saac deploy');
56
+ return;
57
+ }
58
+
59
+ logger.newline();
60
+
61
+ // Build table data
62
+ const data = [
63
+ ['UUID', 'Status', 'Branch', 'Commit', 'Duration', 'Trigger', 'Date'],
64
+ ];
65
+
66
+ result.deployments.forEach((d) => {
67
+ const uuid = d.deployment_uuid ? d.deployment_uuid.substring(0, 23) + '...' : 'N/A';
68
+
69
+ // Colorize status
70
+ let statusDisplay;
71
+ if (d.status === 'finished') {
72
+ statusDisplay = logger.chalk.green('finished');
73
+ } else if (d.status === 'failed') {
74
+ statusDisplay = logger.chalk.red('failed');
75
+ } else if (d.status === 'running' || d.status === 'queued') {
76
+ statusDisplay = logger.chalk.yellow(d.status);
77
+ } else {
78
+ statusDisplay = logger.chalk.gray(d.status || 'unknown');
79
+ }
80
+
81
+ const branch = d.git_branch || 'N/A';
82
+ const commit = d.git_commit ? d.git_commit.substring(0, 7) : 'N/A';
83
+ const duration = d.duration_seconds ? `${d.duration_seconds}s` : '-';
84
+ const trigger = d.triggered_by || 'api';
85
+ const date = d.started_at ? new Date(d.started_at).toLocaleString() : 'N/A';
86
+
87
+ data.push([
88
+ uuid,
89
+ statusDisplay,
90
+ branch,
91
+ commit,
92
+ duration,
93
+ trigger,
94
+ date,
95
+ ]);
96
+ });
97
+
98
+ console.log(table(data, {
99
+ header: {
100
+ alignment: 'center',
101
+ content: `Showing ${result.deployments.length} of ${result.total} deployments`,
102
+ },
103
+ }));
104
+
105
+ // Show pagination info
106
+ if (result.total > result.deployments.length) {
107
+ const remaining = result.total - result.deployments.length;
108
+ logger.info(`${remaining} more deployment(s) available`);
109
+ logger.newline();
110
+ logger.info('View more with:');
111
+ logger.log(` saac deployments --limit ${options.limit || 20} --offset ${(options.offset || 0) + (options.limit || 20)}`);
112
+ }
113
+
114
+ logger.newline();
115
+ logger.info('View deployment logs:');
116
+ logger.log(' saac logs --deployment # Latest deployment');
117
+ logger.log(' saac logs --deployment <uuid> # Specific deployment');
118
+
119
+ } catch (error) {
120
+ spin.fail('Failed to fetch deployment history');
121
+ throw error;
122
+ }
123
+
124
+ } catch (error) {
125
+ logger.error(error.response?.data?.message || error.message);
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ module.exports = deployments;
@@ -1,4 +1,205 @@
1
- // TODO: Implement domain command
2
- module.exports = async function() {
3
- console.log('domain command - Coming soon!');
1
+ /**
2
+ * Domain Management Commands - Manage application domain and subdomain
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated, saveProjectConfig } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+
9
+ /**
10
+ * Set/change application subdomain
11
+ * @param {string} subdomain - New subdomain
12
+ * @param {object} options - Command options
13
+ */
14
+ async function set(subdomain, options) {
15
+ try {
16
+ // Check authentication
17
+ if (!isAuthenticated()) {
18
+ logger.error('Not logged in. Run: saac login');
19
+ process.exit(1);
20
+ }
21
+
22
+ // Check for project config
23
+ const projectConfig = getProjectConfig();
24
+ if (!projectConfig || !projectConfig.applicationUuid) {
25
+ logger.error('No application found in current directory');
26
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
27
+ logger.newline();
28
+ logger.info('Or initialize with:');
29
+ logger.log(' saac init');
30
+ process.exit(1);
31
+ }
32
+
33
+ const { applicationUuid, applicationName } = projectConfig;
34
+
35
+ // Validate subdomain format
36
+ const subdomainRegex = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
37
+ if (!subdomainRegex.test(subdomain)) {
38
+ logger.error('Invalid subdomain format');
39
+ logger.newline();
40
+ logger.info('Subdomain must:');
41
+ logger.log(' • Be 3-63 characters long');
42
+ logger.log(' • Contain only lowercase letters, numbers, and hyphens');
43
+ logger.log(' • Not start or end with a hyphen');
44
+ logger.newline();
45
+ logger.info('Valid examples:');
46
+ logger.log(' my-app, johnsmith, app-123');
47
+ logger.newline();
48
+ logger.info('Invalid examples:');
49
+ logger.log(' My-App (uppercase), -myapp (starts with hyphen), a (too short)');
50
+ process.exit(1);
51
+ }
52
+
53
+ const domainSuffix = options.domainSuffix || 'startanaicompany.com';
54
+
55
+ logger.section('Updating Domain');
56
+ logger.newline();
57
+
58
+ logger.info('Configuration:');
59
+ logger.field(' Application', applicationName);
60
+ logger.field(' New Subdomain', subdomain);
61
+ logger.field(' Domain Suffix', domainSuffix);
62
+ logger.field(' New Domain', `https://${subdomain}.${domainSuffix}`);
63
+
64
+ logger.newline();
65
+
66
+ const spin = logger.spinner('Updating domain...').start();
67
+
68
+ try {
69
+ const result = await api.updateDomain(applicationUuid, subdomain, domainSuffix);
70
+
71
+ spin.succeed('Domain updated successfully!');
72
+
73
+ logger.newline();
74
+
75
+ logger.field('Old Domain', result.old_domain);
76
+ logger.field('New Domain', result.new_domain);
77
+
78
+ logger.newline();
79
+ logger.info('DNS propagation may take 1-5 minutes');
80
+ logger.info('Your application will be accessible at the new domain shortly.');
81
+
82
+ // Update local config
83
+ projectConfig.subdomain = subdomain;
84
+ projectConfig.domain = result.new_domain;
85
+ saveProjectConfig(projectConfig);
86
+
87
+ logger.newline();
88
+ logger.success('Local configuration updated');
89
+
90
+ } catch (error) {
91
+ spin.fail('Failed to update domain');
92
+
93
+ if (error.response?.status === 409) {
94
+ logger.newline();
95
+ const data = error.response.data;
96
+
97
+ if (data.error === 'SUBDOMAIN_TAKEN') {
98
+ logger.warn('Subdomain is already taken');
99
+ logger.newline();
100
+
101
+ if (data.suggestions && data.suggestions.length > 0) {
102
+ logger.info('Try these alternatives:');
103
+ data.suggestions.forEach(suggestion => {
104
+ logger.log(` • ${suggestion}`);
105
+ });
106
+ }
107
+ } else if (data.error === 'SUBDOMAIN_BLOCKED') {
108
+ logger.warn('Subdomain is reserved and cannot be used');
109
+ logger.newline();
110
+ logger.info('Reserved subdomains:');
111
+ if (data.blocklist) {
112
+ data.blocklist.forEach(blocked => {
113
+ logger.log(` • ${blocked}`);
114
+ });
115
+ }
116
+ }
117
+ } else if (error.response?.status === 403) {
118
+ logger.newline();
119
+ const data = error.response.data;
120
+
121
+ if (data.error === 'DOMAIN_SUFFIX_NOT_ALLOWED') {
122
+ logger.warn('Custom domain suffix not available on your plan');
123
+ logger.newline();
124
+ logger.info('Allowed suffixes:');
125
+ if (data.allowed_suffixes) {
126
+ data.allowed_suffixes.forEach(suffix => {
127
+ logger.log(` • ${suffix}`);
128
+ });
129
+ }
130
+ logger.newline();
131
+ if (data.upgrade_required) {
132
+ logger.info('Upgrade to Pro plan for custom domain support');
133
+ }
134
+ }
135
+ }
136
+
137
+ throw error;
138
+ }
139
+
140
+ } catch (error) {
141
+ logger.error(error.response?.data?.message || error.message);
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Show current domain information
148
+ */
149
+ async function show() {
150
+ try {
151
+ // Check authentication
152
+ if (!isAuthenticated()) {
153
+ logger.error('Not logged in. Run: saac login');
154
+ process.exit(1);
155
+ }
156
+
157
+ // Check for project config
158
+ const projectConfig = getProjectConfig();
159
+ if (!projectConfig || !projectConfig.applicationUuid) {
160
+ logger.error('No application found in current directory');
161
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
162
+ logger.newline();
163
+ logger.info('Or initialize with:');
164
+ logger.log(' saac init');
165
+ process.exit(1);
166
+ }
167
+
168
+ const { applicationUuid } = projectConfig;
169
+
170
+ const spin = logger.spinner('Fetching application details...').start();
171
+
172
+ try {
173
+ const app = await api.getApplication(applicationUuid);
174
+
175
+ spin.succeed('Application details retrieved');
176
+
177
+ logger.newline();
178
+
179
+ logger.section(`Domain Information: ${app.name}`);
180
+ logger.newline();
181
+
182
+ logger.field('Domain', app.domain || `https://${app.subdomain}.startanaicompany.com`);
183
+ logger.field('Subdomain', app.subdomain);
184
+ logger.field('Domain Suffix', 'startanaicompany.com');
185
+ logger.field('Status', app.status);
186
+
187
+ logger.newline();
188
+ logger.info('To change domain:');
189
+ logger.log(' saac domain set <new-subdomain>');
190
+
191
+ } catch (error) {
192
+ spin.fail('Failed to fetch application details');
193
+ throw error;
194
+ }
195
+
196
+ } catch (error) {
197
+ logger.error(error.response?.data?.message || error.message);
198
+ process.exit(1);
199
+ }
200
+ }
201
+
202
+ module.exports = {
203
+ set,
204
+ show,
4
205
  };