suthep 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.editorconfig +17 -0
  2. package/.github/workflows/publish.yml +42 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +7 -0
  5. package/.scannerwork/.sonar_lock +0 -0
  6. package/.scannerwork/report-task.txt +6 -0
  7. package/.vscode/settings.json +19 -0
  8. package/LICENSE +21 -0
  9. package/README.md +317 -0
  10. package/dist/commands/deploy.js +371 -0
  11. package/dist/commands/deploy.js.map +1 -0
  12. package/dist/commands/down.js +179 -0
  13. package/dist/commands/down.js.map +1 -0
  14. package/dist/commands/init.js +188 -0
  15. package/dist/commands/init.js.map +1 -0
  16. package/dist/commands/setup.js +90 -0
  17. package/dist/commands/setup.js.map +1 -0
  18. package/dist/commands/up.js +213 -0
  19. package/dist/commands/up.js.map +1 -0
  20. package/dist/index.js +66 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/utils/certbot.js +64 -0
  23. package/dist/utils/certbot.js.map +1 -0
  24. package/dist/utils/config-loader.js +127 -0
  25. package/dist/utils/config-loader.js.map +1 -0
  26. package/dist/utils/deployment.js +85 -0
  27. package/dist/utils/deployment.js.map +1 -0
  28. package/dist/utils/docker.js +425 -0
  29. package/dist/utils/docker.js.map +1 -0
  30. package/dist/utils/env-loader.js +53 -0
  31. package/dist/utils/env-loader.js.map +1 -0
  32. package/dist/utils/nginx.js +378 -0
  33. package/dist/utils/nginx.js.map +1 -0
  34. package/docs/README.md +38 -0
  35. package/docs/english/01-introduction.md +84 -0
  36. package/docs/english/02-installation.md +200 -0
  37. package/docs/english/03-quick-start.md +258 -0
  38. package/docs/english/04-configuration.md +433 -0
  39. package/docs/english/05-commands.md +336 -0
  40. package/docs/english/06-examples.md +456 -0
  41. package/docs/english/07-troubleshooting.md +417 -0
  42. package/docs/english/08-advanced.md +411 -0
  43. package/docs/english/README.md +48 -0
  44. package/docs/thai/01-introduction.md +84 -0
  45. package/docs/thai/02-installation.md +200 -0
  46. package/docs/thai/03-quick-start.md +258 -0
  47. package/docs/thai/04-configuration.md +433 -0
  48. package/docs/thai/05-commands.md +336 -0
  49. package/docs/thai/06-examples.md +456 -0
  50. package/docs/thai/07-troubleshooting.md +417 -0
  51. package/docs/thai/08-advanced.md +411 -0
  52. package/docs/thai/README.md +48 -0
  53. package/example/suthep-complete.yml +103 -0
  54. package/example/suthep-docker-only.yml +71 -0
  55. package/example/suthep-env-example.yml +113 -0
  56. package/example/suthep-no-docker.yml +51 -0
  57. package/example/suthep-path-routing.yml +62 -0
  58. package/example/suthep.example.yml +88 -0
  59. package/package.json +51 -0
  60. package/src/commands/deploy.ts +488 -0
  61. package/src/commands/down.ts +240 -0
  62. package/src/commands/init.ts +214 -0
  63. package/src/commands/setup.ts +112 -0
  64. package/src/commands/up.ts +271 -0
  65. package/src/index.ts +109 -0
  66. package/src/types/config.ts +52 -0
  67. package/src/utils/__tests__/certbot.test.ts +222 -0
  68. package/src/utils/__tests__/config-loader.test.ts +419 -0
  69. package/src/utils/__tests__/deployment.test.ts +243 -0
  70. package/src/utils/__tests__/nginx.test.ts +412 -0
  71. package/src/utils/certbot.ts +144 -0
  72. package/src/utils/config-loader.ts +184 -0
  73. package/src/utils/deployment.ts +157 -0
  74. package/src/utils/docker.ts +768 -0
  75. package/src/utils/env-loader.ts +135 -0
  76. package/src/utils/nginx.ts +443 -0
  77. package/suthep-1.0.0.tgz +0 -0
  78. package/suthep.example.yml +98 -0
  79. package/suthep.yml +39 -0
  80. package/todo.md +6 -0
  81. package/tsconfig.json +26 -0
  82. package/vite.config.ts +46 -0
  83. package/vitest.config.ts +21 -0
@@ -0,0 +1,240 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import type { ServiceConfig } from '../types/config'
4
+ import { loadConfig } from '../utils/config-loader'
5
+ import { isContainerRunning, removeDockerContainer, stopDockerContainer } from '../utils/docker'
6
+ import {
7
+ disableSite,
8
+ enableSite,
9
+ generateMultiServiceNginxConfig,
10
+ generateNginxConfig,
11
+ reloadNginx,
12
+ writeNginxConfig,
13
+ } from '../utils/nginx'
14
+
15
+ interface DownOptions {
16
+ file: string
17
+ all: boolean
18
+ serviceName?: string
19
+ }
20
+
21
+ export async function downCommand(options: DownOptions): Promise<void> {
22
+ console.log(chalk.blue.bold('\nšŸ›‘ Bringing Down Services\n'))
23
+
24
+ try {
25
+ // Load configuration
26
+ if (!(await fs.pathExists(options.file))) {
27
+ throw new Error(`Configuration file not found: ${options.file}`)
28
+ }
29
+
30
+ console.log(chalk.cyan(`šŸ“„ Loading configuration from ${options.file}...`))
31
+ const config = await loadConfig(options.file)
32
+
33
+ console.log(chalk.green(`āœ… Configuration loaded for project: ${config.project.name}`))
34
+
35
+ // Determine which services to bring down
36
+ let servicesToDown: ServiceConfig[] = []
37
+
38
+ if (options.all) {
39
+ servicesToDown = config.services
40
+ console.log(
41
+ chalk.cyan(
42
+ `šŸ“‹ Bringing down all services: ${servicesToDown.map((s) => s.name).join(', ')}\n`
43
+ )
44
+ )
45
+ } else if (options.serviceName) {
46
+ const service = config.services.find((s) => s.name === options.serviceName)
47
+ if (!service) {
48
+ throw new Error(
49
+ `Service "${
50
+ options.serviceName
51
+ }" not found in configuration. Available services: ${config.services
52
+ .map((s) => s.name)
53
+ .join(', ')}`
54
+ )
55
+ }
56
+ servicesToDown = [service]
57
+ console.log(chalk.cyan(`šŸ“‹ Bringing down service: ${options.serviceName}\n`))
58
+ } else {
59
+ throw new Error('Either specify a service name or use --all flag')
60
+ }
61
+
62
+ // Group services by domain for nginx config management
63
+ const domainToServices = new Map<string, ServiceConfig[]>()
64
+ const allDomains = new Set<string>()
65
+
66
+ for (const service of servicesToDown) {
67
+ for (const domain of service.domains) {
68
+ allDomains.add(domain)
69
+ if (!domainToServices.has(domain)) {
70
+ domainToServices.set(domain, [])
71
+ }
72
+ domainToServices.get(domain)!.push(service)
73
+ }
74
+ }
75
+
76
+ // Stop and remove Docker containers
77
+ for (const service of servicesToDown) {
78
+ if (service.docker) {
79
+ console.log(chalk.cyan(`\n🐳 Stopping Docker container for service: ${service.name}`))
80
+ try {
81
+ const containerName = service.docker.container
82
+
83
+ // Stop container
84
+ try {
85
+ await stopDockerContainer(containerName)
86
+ console.log(chalk.green(` āœ… Stopped container: ${containerName}`))
87
+ } catch (error: any) {
88
+ const errorMessage = error?.message || String(error) || 'Unknown error'
89
+ if (
90
+ errorMessage.toLowerCase().includes('no such container') ||
91
+ errorMessage.toLowerCase().includes('container not found')
92
+ ) {
93
+ console.log(
94
+ chalk.yellow(` āš ļø Container ${containerName} not found (already stopped)`)
95
+ )
96
+ } else {
97
+ throw error
98
+ }
99
+ }
100
+
101
+ // Remove container
102
+ try {
103
+ await removeDockerContainer(containerName)
104
+ console.log(chalk.green(` āœ… Removed container: ${containerName}`))
105
+ } catch (error: any) {
106
+ const errorMessage = error?.message || String(error) || 'Unknown error'
107
+ if (
108
+ errorMessage.toLowerCase().includes('no such container') ||
109
+ errorMessage.toLowerCase().includes('container not found')
110
+ ) {
111
+ console.log(
112
+ chalk.yellow(` āš ļø Container ${containerName} not found (already removed)`)
113
+ )
114
+ } else {
115
+ throw error
116
+ }
117
+ }
118
+ } catch (error) {
119
+ console.error(
120
+ chalk.red(` āŒ Failed to stop/remove container for service ${service.name}:`),
121
+ error instanceof Error ? error.message : error
122
+ )
123
+ throw error
124
+ }
125
+ }
126
+ }
127
+
128
+ // Handle Nginx configs by domain
129
+ // Check which services remain active for each domain
130
+ if (allDomains.size > 0) {
131
+ console.log(chalk.cyan(`\nāš™ļø Updating Nginx configurations...`))
132
+
133
+ // Create a set of service names being brought down for quick lookup
134
+ const servicesBeingDowned = new Set(servicesToDown.map((s) => s.name))
135
+
136
+ // For each domain, find all services that use it (from all config services)
137
+ // and determine which ones remain active
138
+ for (const domain of allDomains) {
139
+ const configName = domain.replace(/\./g, '_')
140
+
141
+ // Find all services that use this domain (from entire config)
142
+ const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
143
+
144
+ // Filter out services being brought down
145
+ const remainingServices = allServicesForDomain.filter(
146
+ (s) => !servicesBeingDowned.has(s.name)
147
+ )
148
+
149
+ // Check if remaining services are actually running (have active containers)
150
+ const activeServices: ServiceConfig[] = []
151
+ for (const service of remainingServices) {
152
+ if (service.docker) {
153
+ const isRunning = await isContainerRunning(service.docker.container)
154
+ if (isRunning) {
155
+ activeServices.push(service)
156
+ }
157
+ } else {
158
+ // Service without docker - assume it's running if not being brought down
159
+ activeServices.push(service)
160
+ }
161
+ }
162
+
163
+ try {
164
+ if (activeServices.length === 0) {
165
+ // No active services on this domain, disable the config
166
+ console.log(
167
+ chalk.cyan(` šŸ“‹ No active services on ${domain}, disabling Nginx config...`)
168
+ )
169
+ await disableSite(configName, config.nginx.configPath)
170
+ console.log(chalk.green(` āœ… Disabled Nginx config for ${domain}`))
171
+ } else {
172
+ // Regenerate config with remaining active services
173
+ console.log(
174
+ chalk.cyan(
175
+ ` šŸ“‹ Regenerating Nginx config for ${domain} with ${
176
+ activeServices.length
177
+ } active service(s): ${activeServices.map((s) => s.name).join(', ')}`
178
+ )
179
+ )
180
+
181
+ // Generate config for remaining services
182
+ let nginxConfigContent: string
183
+ if (activeServices.length === 1) {
184
+ nginxConfigContent = generateNginxConfig(activeServices[0], false)
185
+ } else {
186
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)
187
+ }
188
+
189
+ // Check if HTTPS is enabled (check if certificate exists)
190
+ const { certificateExists } = await import('../utils/certbot')
191
+ const hasHttps = await certificateExists(domain)
192
+ if (hasHttps) {
193
+ // Regenerate with HTTPS
194
+ if (activeServices.length === 1) {
195
+ nginxConfigContent = generateNginxConfig(activeServices[0], true)
196
+ } else {
197
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)
198
+ }
199
+ }
200
+
201
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
202
+ await enableSite(configName, config.nginx.configPath)
203
+ console.log(
204
+ chalk.green(
205
+ ` āœ… Updated Nginx config for ${domain} (${activeServices.length} service(s) active)`
206
+ )
207
+ )
208
+ }
209
+ } catch (error) {
210
+ console.error(
211
+ chalk.red(` āŒ Failed to update Nginx config for ${domain}:`),
212
+ error instanceof Error ? error.message : error
213
+ )
214
+ throw error
215
+ }
216
+ }
217
+
218
+ // Reload Nginx to apply changes
219
+ console.log(chalk.cyan(`\nšŸ”„ Reloading Nginx...`))
220
+ try {
221
+ await reloadNginx(config.nginx.reloadCommand)
222
+ console.log(chalk.green(` āœ… Nginx reloaded`))
223
+ } catch (error) {
224
+ console.error(
225
+ chalk.red(` āŒ Failed to reload Nginx:`),
226
+ error instanceof Error ? error.message : error
227
+ )
228
+ throw error
229
+ }
230
+ }
231
+
232
+ console.log(chalk.green.bold('\nāœ… Services brought down successfully!\n'))
233
+ } catch (error) {
234
+ console.error(
235
+ chalk.red('\nāŒ Failed to bring down services:'),
236
+ error instanceof Error ? error.message : error
237
+ )
238
+ process.exit(1)
239
+ }
240
+ }
@@ -0,0 +1,214 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import inquirer from 'inquirer'
4
+ import type { DeployConfig } from '../types/config'
5
+ import { saveConfig } from '../utils/config-loader'
6
+
7
+ interface InitOptions {
8
+ file: string
9
+ }
10
+
11
+ export async function initCommand(options: InitOptions): Promise<void> {
12
+ console.log(chalk.blue.bold('\nšŸš€ Suthep Deployment Configuration\n'))
13
+
14
+ // Check if file already exists
15
+ if (await fs.pathExists(options.file)) {
16
+ const { overwrite } = await inquirer.prompt([
17
+ {
18
+ type: 'confirm',
19
+ name: 'overwrite',
20
+ message: `File ${options.file} already exists. Overwrite?`,
21
+ default: false,
22
+ },
23
+ ])
24
+
25
+ if (!overwrite) {
26
+ console.log(chalk.yellow('Aborted.'))
27
+ return
28
+ }
29
+ }
30
+
31
+ // Gather project information
32
+ const projectAnswers = await inquirer.prompt([
33
+ {
34
+ type: 'input',
35
+ name: 'projectName',
36
+ message: 'Project name:',
37
+ default: 'my-app',
38
+ },
39
+ {
40
+ type: 'input',
41
+ name: 'projectVersion',
42
+ message: 'Project version:',
43
+ default: '1.0.0',
44
+ },
45
+ ])
46
+
47
+ // Gather service information
48
+ const services = []
49
+ let addMoreServices = true
50
+
51
+ while (addMoreServices) {
52
+ console.log(chalk.cyan(`\nšŸ“¦ Service ${services.length + 1} Configuration`))
53
+
54
+ const serviceAnswers = await inquirer.prompt([
55
+ {
56
+ type: 'input',
57
+ name: 'name',
58
+ message: 'Service name:',
59
+ validate: (input) => input.trim() !== '' || 'Service name is required',
60
+ },
61
+ {
62
+ type: 'number',
63
+ name: 'port',
64
+ message: 'Service port:',
65
+ default: 3000,
66
+ validate: (input: number | undefined) => {
67
+ if (input === undefined) return 'Port is required'
68
+ return (input > 0 && input < 65536) || 'Port must be between 1 and 65535'
69
+ },
70
+ },
71
+ {
72
+ type: 'input',
73
+ name: 'domains',
74
+ message: 'Domain names (comma-separated):',
75
+ validate: (input) => input.trim() !== '' || 'At least one domain is required',
76
+ filter: (input: string) => input.split(',').map((d: string) => d.trim()),
77
+ },
78
+ {
79
+ type: 'confirm',
80
+ name: 'useDocker',
81
+ message: 'Use Docker?',
82
+ default: false,
83
+ },
84
+ ])
85
+
86
+ // Docker configuration
87
+ let dockerConfig = undefined
88
+ if (serviceAnswers.useDocker) {
89
+ const dockerAnswers = await inquirer.prompt([
90
+ {
91
+ type: 'input',
92
+ name: 'image',
93
+ message: 'Docker image (leave empty to connect to existing container):',
94
+ },
95
+ {
96
+ type: 'input',
97
+ name: 'container',
98
+ message: 'Container name:',
99
+ validate: (input) => input.trim() !== '' || 'Container name is required',
100
+ },
101
+ {
102
+ type: 'number',
103
+ name: 'port',
104
+ message: 'Container port:',
105
+ default: serviceAnswers.port,
106
+ },
107
+ ])
108
+
109
+ dockerConfig = {
110
+ image: dockerAnswers.image || undefined,
111
+ container: dockerAnswers.container,
112
+ port: dockerAnswers.port,
113
+ }
114
+ }
115
+
116
+ // Health check configuration
117
+ const { addHealthCheck } = await inquirer.prompt([
118
+ {
119
+ type: 'confirm',
120
+ name: 'addHealthCheck',
121
+ message: 'Add health check?',
122
+ default: true,
123
+ },
124
+ ])
125
+
126
+ let healthCheck = undefined
127
+ if (addHealthCheck) {
128
+ const healthCheckAnswers = await inquirer.prompt([
129
+ {
130
+ type: 'input',
131
+ name: 'path',
132
+ message: 'Health check path:',
133
+ default: '/health',
134
+ },
135
+ {
136
+ type: 'number',
137
+ name: 'interval',
138
+ message: 'Health check interval (seconds):',
139
+ default: 30,
140
+ },
141
+ ])
142
+
143
+ healthCheck = healthCheckAnswers
144
+ }
145
+
146
+ services.push({
147
+ name: serviceAnswers.name,
148
+ port: serviceAnswers.port,
149
+ domains: serviceAnswers.domains,
150
+ docker: dockerConfig,
151
+ healthCheck,
152
+ })
153
+
154
+ const { addMore } = await inquirer.prompt([
155
+ {
156
+ type: 'confirm',
157
+ name: 'addMore',
158
+ message: 'Add another service?',
159
+ default: false,
160
+ },
161
+ ])
162
+
163
+ addMoreServices = addMore
164
+ }
165
+
166
+ // Certbot configuration
167
+ const certbotAnswers = await inquirer.prompt([
168
+ {
169
+ type: 'input',
170
+ name: 'email',
171
+ message: 'Email for SSL certificates:',
172
+ validate: (input) => {
173
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
174
+ return emailRegex.test(input) || 'Please enter a valid email address'
175
+ },
176
+ },
177
+ {
178
+ type: 'confirm',
179
+ name: 'staging',
180
+ message: 'Use Certbot staging environment? (for testing)',
181
+ default: false,
182
+ },
183
+ ])
184
+
185
+ // Build configuration object
186
+ const config: DeployConfig = {
187
+ project: {
188
+ name: projectAnswers.projectName,
189
+ version: projectAnswers.projectVersion,
190
+ },
191
+ services,
192
+ nginx: {
193
+ configPath: '/etc/nginx/sites-available',
194
+ reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',
195
+ },
196
+ certbot: {
197
+ email: certbotAnswers.email,
198
+ staging: certbotAnswers.staging,
199
+ },
200
+ deployment: {
201
+ strategy: 'rolling',
202
+ healthCheckTimeout: 30000,
203
+ },
204
+ }
205
+
206
+ // Save configuration
207
+ await saveConfig(options.file, config)
208
+
209
+ console.log(chalk.green(`\nāœ… Configuration saved to ${options.file}`))
210
+ console.log(chalk.dim('\nNext steps:'))
211
+ console.log(chalk.dim(` 1. Review and edit ${options.file} if needed`))
212
+ console.log(chalk.dim(' 2. Run: suthep setup'))
213
+ console.log(chalk.dim(' 3. Run: suthep deploy\n'))
214
+ }
@@ -0,0 +1,112 @@
1
+ import chalk from 'chalk';
2
+ import { execa } from 'execa';
3
+
4
+ interface SetupOptions {
5
+ nginxOnly?: boolean;
6
+ certbotOnly?: boolean;
7
+ }
8
+
9
+ export async function setupCommand(options: SetupOptions): Promise<void> {
10
+ console.log(chalk.blue.bold('\nšŸ”§ Setting up prerequisites\n'));
11
+
12
+ const setupNginx = !options.certbotOnly;
13
+ const setupCertbot = !options.nginxOnly;
14
+
15
+ try {
16
+ // Setup Nginx
17
+ if (setupNginx) {
18
+ console.log(chalk.cyan('šŸ“¦ Installing Nginx...'));
19
+
20
+ // Check if Nginx is already installed
21
+ try {
22
+ await execa('nginx', ['-v']);
23
+ console.log(chalk.green('āœ… Nginx is already installed'));
24
+ } catch {
25
+ // Install Nginx based on OS
26
+ const platform = process.platform;
27
+
28
+ if (platform === 'linux') {
29
+ // Detect Linux distribution
30
+ try {
31
+ await execa('apt-get', ['--version']);
32
+ console.log(chalk.dim('Using apt-get...'));
33
+ await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
34
+ await execa('sudo', ['apt-get', 'install', '-y', 'nginx'], { stdio: 'inherit' });
35
+ } catch {
36
+ try {
37
+ await execa('yum', ['--version']);
38
+ console.log(chalk.dim('Using yum...'));
39
+ await execa('sudo', ['yum', 'install', '-y', 'nginx'], { stdio: 'inherit' });
40
+ } catch {
41
+ throw new Error('Unsupported Linux distribution. Please install Nginx manually.');
42
+ }
43
+ }
44
+ } else if (platform === 'darwin') {
45
+ console.log(chalk.dim('Using Homebrew...'));
46
+ await execa('brew', ['install', 'nginx'], { stdio: 'inherit' });
47
+ } else {
48
+ throw new Error(`Unsupported platform: ${platform}. Please install Nginx manually.`);
49
+ }
50
+
51
+ console.log(chalk.green('āœ… Nginx installed successfully'));
52
+ }
53
+
54
+ // Start Nginx service
55
+ console.log(chalk.cyan('šŸš€ Starting Nginx service...'));
56
+ try {
57
+ await execa('sudo', ['systemctl', 'start', 'nginx']);
58
+ await execa('sudo', ['systemctl', 'enable', 'nginx']);
59
+ console.log(chalk.green('āœ… Nginx service started'));
60
+ } catch (error) {
61
+ console.log(chalk.yellow('āš ļø Could not start Nginx via systemctl (might not be available)'));
62
+ }
63
+ }
64
+
65
+ // Setup Certbot
66
+ if (setupCertbot) {
67
+ console.log(chalk.cyan('\nšŸ” Installing Certbot...'));
68
+
69
+ // Check if Certbot is already installed
70
+ try {
71
+ await execa('certbot', ['--version']);
72
+ console.log(chalk.green('āœ… Certbot is already installed'));
73
+ } catch {
74
+ const platform = process.platform;
75
+
76
+ if (platform === 'linux') {
77
+ // Install Certbot based on package manager
78
+ try {
79
+ await execa('apt-get', ['--version']);
80
+ console.log(chalk.dim('Using apt-get...'));
81
+ await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
82
+ await execa('sudo', ['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
83
+ } catch {
84
+ try {
85
+ await execa('yum', ['--version']);
86
+ console.log(chalk.dim('Using yum...'));
87
+ await execa('sudo', ['yum', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
88
+ } catch {
89
+ throw new Error('Unsupported Linux distribution. Please install Certbot manually.');
90
+ }
91
+ }
92
+ } else if (platform === 'darwin') {
93
+ console.log(chalk.dim('Using Homebrew...'));
94
+ await execa('brew', ['install', 'certbot'], { stdio: 'inherit' });
95
+ } else {
96
+ throw new Error(`Unsupported platform: ${platform}. Please install Certbot manually.`);
97
+ }
98
+
99
+ console.log(chalk.green('āœ… Certbot installed successfully'));
100
+ }
101
+ }
102
+
103
+ console.log(chalk.green.bold('\n✨ Setup completed successfully!\n'));
104
+ console.log(chalk.dim('Next steps:'));
105
+ console.log(chalk.dim(' 1. Create a configuration file: suthep init'));
106
+ console.log(chalk.dim(' 2. Deploy your services: suthep deploy\n'));
107
+
108
+ } catch (error) {
109
+ console.error(chalk.red('\nāŒ Setup failed:'), error instanceof Error ? error.message : error);
110
+ process.exit(1);
111
+ }
112
+ }