suthep 0.1.0 → 0.2.0-beta.1

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 (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
@@ -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,78 @@
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 { deployCommand } from './deploy'
6
+ import { downCommand } from './down'
7
+
8
+ interface RedeployOptions {
9
+ file: string
10
+ all: boolean
11
+ serviceName?: string
12
+ https: boolean
13
+ nginx: boolean
14
+ }
15
+
16
+ export async function redeployCommand(options: RedeployOptions): Promise<void> {
17
+ console.log(chalk.blue.bold('\nšŸ”„ Redeploying Services\n'))
18
+
19
+ try {
20
+ // Load configuration to validate service names
21
+ if (!(await fs.pathExists(options.file))) {
22
+ throw new Error(`Configuration file not found: ${options.file}`)
23
+ }
24
+
25
+ const config = await loadConfig(options.file)
26
+
27
+ // Determine which services to redeploy
28
+ let servicesToRedeploy: ServiceConfig[] = []
29
+
30
+ if (options.all) {
31
+ servicesToRedeploy = config.services
32
+ console.log(
33
+ chalk.cyan(
34
+ `šŸ“‹ Redeploying all services: ${servicesToRedeploy.map((s) => s.name).join(', ')}\n`
35
+ )
36
+ )
37
+ } else if (options.serviceName) {
38
+ const service = config.services.find((s) => s.name === options.serviceName)
39
+ if (!service) {
40
+ throw new Error(
41
+ `Service "${
42
+ options.serviceName
43
+ }" not found in configuration. Available services: ${config.services
44
+ .map((s) => s.name)
45
+ .join(', ')}`
46
+ )
47
+ }
48
+ servicesToRedeploy = [service]
49
+ console.log(chalk.cyan(`šŸ“‹ Redeploying service: ${options.serviceName}\n`))
50
+ } else {
51
+ throw new Error('Either specify a service name or use --all flag')
52
+ }
53
+
54
+ // Step 1: Bring down the services
55
+ console.log(chalk.yellow('Step 1: Bringing down services...\n'))
56
+ await downCommand({
57
+ file: options.file,
58
+ all: options.all,
59
+ serviceName: options.serviceName,
60
+ })
61
+
62
+ // Step 2: Deploy the services (which will bring them back up)
63
+ console.log(chalk.yellow('\nStep 2: Deploying services...\n'))
64
+ await deployCommand({
65
+ file: options.file,
66
+ https: options.https,
67
+ nginx: options.nginx,
68
+ })
69
+
70
+ console.log(chalk.green.bold('\nāœ… Services redeployed successfully!\n'))
71
+ } catch (error) {
72
+ console.error(
73
+ chalk.red('\nāŒ Failed to redeploy services:'),
74
+ error instanceof Error ? error.message : error
75
+ )
76
+ process.exit(1)
77
+ }
78
+ }
@@ -0,0 +1,271 @@
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 { waitForService } from '../utils/deployment'
6
+ import { isContainerRunning, startDockerContainer } from '../utils/docker'
7
+ import {
8
+ enableSite,
9
+ generateMultiServiceNginxConfig,
10
+ generateNginxConfig,
11
+ reloadNginx,
12
+ writeNginxConfig,
13
+ } from '../utils/nginx'
14
+
15
+ interface UpOptions {
16
+ file: string
17
+ all: boolean
18
+ serviceName?: string
19
+ https: boolean
20
+ nginx: boolean
21
+ }
22
+
23
+ export async function upCommand(options: UpOptions): Promise<void> {
24
+ console.log(chalk.blue.bold('\nšŸš€ Bringing Up Services\n'))
25
+
26
+ try {
27
+ // Load configuration
28
+ if (!(await fs.pathExists(options.file))) {
29
+ throw new Error(`Configuration file not found: ${options.file}`)
30
+ }
31
+
32
+ console.log(chalk.cyan(`šŸ“„ Loading configuration from ${options.file}...`))
33
+ const config = await loadConfig(options.file)
34
+
35
+ console.log(chalk.green(`āœ… Configuration loaded for project: ${config.project.name}`))
36
+
37
+ // Determine which services to bring up
38
+ let servicesToUp: ServiceConfig[] = []
39
+
40
+ if (options.all) {
41
+ servicesToUp = config.services
42
+ console.log(
43
+ chalk.cyan(`šŸ“‹ Bringing up all services: ${servicesToUp.map((s) => s.name).join(', ')}\n`)
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
+ servicesToUp = [service]
57
+ console.log(chalk.cyan(`šŸ“‹ Bringing up 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 servicesToUp) {
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
+ // Start Docker containers
77
+ for (const service of servicesToUp) {
78
+ if (service.docker) {
79
+ console.log(chalk.cyan(`\n🐳 Starting Docker container for service: ${service.name}`))
80
+ try {
81
+ await startDockerContainer(service)
82
+ console.log(chalk.green(` āœ… Container started for service: ${service.name}`))
83
+ } catch (error) {
84
+ console.error(
85
+ chalk.red(` āŒ Failed to start container for service ${service.name}:`),
86
+ error instanceof Error ? error.message : error
87
+ )
88
+ throw error
89
+ }
90
+ }
91
+ }
92
+
93
+ // Wait for services to be healthy
94
+ for (const service of servicesToUp) {
95
+ if (service.healthCheck) {
96
+ console.log(chalk.cyan(`\nšŸ„ Waiting for service ${service.name} to be healthy...`))
97
+ const isHealthy = await waitForService(service, config.deployment.healthCheckTimeout)
98
+ if (isHealthy) {
99
+ console.log(chalk.green(` āœ… Service ${service.name} is healthy`))
100
+ } else {
101
+ console.log(
102
+ chalk.yellow(` āš ļø Service ${service.name} health check timeout, continuing anyway...`)
103
+ )
104
+ }
105
+ }
106
+ }
107
+
108
+ // Configure Nginx
109
+ if (options.nginx && allDomains.size > 0) {
110
+ console.log(chalk.cyan(`\nāš™ļø Configuring Nginx reverse proxy...`))
111
+
112
+ // Create a set of service names being brought up for quick lookup
113
+ const servicesBeingUpped = new Set(servicesToUp.map((s) => s.name))
114
+
115
+ // For each domain, find all services that use it (from all config services)
116
+ // and include all active services (both newly brought up and already running)
117
+ for (const domain of allDomains) {
118
+ const configName = domain.replace(/\./g, '_')
119
+
120
+ // Find all services that use this domain (from entire config)
121
+ const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
122
+
123
+ // Check which services are actually running (have active containers)
124
+ const activeServices: ServiceConfig[] = []
125
+ for (const service of allServicesForDomain) {
126
+ if (service.docker) {
127
+ const isRunning = await isContainerRunning(service.docker.container)
128
+ if (isRunning) {
129
+ activeServices.push(service)
130
+ }
131
+ } else {
132
+ // Service without docker - include if it's being brought up or assume it's running
133
+ if (servicesBeingUpped.has(service.name)) {
134
+ activeServices.push(service)
135
+ } else {
136
+ // For non-docker services, assume they're running if not explicitly being brought up
137
+ // This is a best-effort approach
138
+ activeServices.push(service)
139
+ }
140
+ }
141
+ }
142
+
143
+ if (activeServices.length === 0) {
144
+ console.log(
145
+ chalk.yellow(` āš ļø No active services found for ${domain}, skipping Nginx config`)
146
+ )
147
+ continue
148
+ }
149
+
150
+ try {
151
+ // Log domain and services configuration
152
+ if (activeServices.length > 1) {
153
+ console.log(
154
+ chalk.cyan(
155
+ ` šŸ“‹ Configuring ${domain} with ${
156
+ activeServices.length
157
+ } active service(s): ${activeServices.map((s) => s.name).join(', ')}`
158
+ )
159
+ )
160
+ } else {
161
+ console.log(
162
+ chalk.cyan(` šŸ“‹ Configuring ${domain} for service: ${activeServices[0].name}`)
163
+ )
164
+ }
165
+
166
+ // Generate Nginx config for all active services on this domain
167
+ let nginxConfigContent: string
168
+ if (activeServices.length === 1) {
169
+ nginxConfigContent = generateNginxConfig(activeServices[0], false)
170
+ } else {
171
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)
172
+ }
173
+
174
+ // Write config file
175
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
176
+ await enableSite(configName, config.nginx.configPath)
177
+
178
+ console.log(chalk.green(` āœ… Nginx configured for ${domain}`))
179
+ } catch (error) {
180
+ console.error(
181
+ chalk.red(` āŒ Failed to configure Nginx for ${domain}:`),
182
+ error instanceof Error ? error.message : error
183
+ )
184
+ throw error
185
+ }
186
+ }
187
+
188
+ // Update with HTTPS if enabled
189
+ if (options.https) {
190
+ console.log(chalk.cyan(`\nšŸ”„ Updating Nginx configs with HTTPS...`))
191
+ for (const domain of allDomains) {
192
+ const configName = domain.replace(/\./g, '_')
193
+
194
+ // Find all active services for this domain again
195
+ const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
196
+ const activeServices: ServiceConfig[] = []
197
+ for (const service of allServicesForDomain) {
198
+ if (service.docker) {
199
+ const isRunning = await isContainerRunning(service.docker.container)
200
+ if (isRunning) {
201
+ activeServices.push(service)
202
+ }
203
+ } else {
204
+ if (servicesBeingUpped.has(service.name)) {
205
+ activeServices.push(service)
206
+ } else {
207
+ activeServices.push(service)
208
+ }
209
+ }
210
+ }
211
+
212
+ if (activeServices.length === 0) {
213
+ continue
214
+ }
215
+
216
+ try {
217
+ let nginxConfigContent: string
218
+ if (activeServices.length === 1) {
219
+ nginxConfigContent = generateNginxConfig(activeServices[0], true)
220
+ } else {
221
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)
222
+ }
223
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
224
+ console.log(chalk.green(` āœ… HTTPS config updated for ${domain}`))
225
+ } catch (error) {
226
+ console.error(
227
+ chalk.red(` āŒ Failed to update HTTPS config for ${domain}:`),
228
+ error instanceof Error ? error.message : error
229
+ )
230
+ throw error
231
+ }
232
+ }
233
+ }
234
+
235
+ // Reload Nginx
236
+ console.log(chalk.cyan(`\nšŸ”„ Reloading Nginx...`))
237
+ try {
238
+ await reloadNginx(config.nginx.reloadCommand)
239
+ console.log(chalk.green(` āœ… Nginx reloaded`))
240
+ } catch (error) {
241
+ console.error(
242
+ chalk.red(` āŒ Failed to reload Nginx:`),
243
+ error instanceof Error ? error.message : error
244
+ )
245
+ throw error
246
+ }
247
+ }
248
+
249
+ console.log(chalk.green.bold('\nāœ… Services brought up successfully!\n'))
250
+
251
+ // Print service URLs
252
+ if (allDomains.size > 0) {
253
+ console.log(chalk.cyan('šŸ“‹ Service URLs:'))
254
+ for (const service of servicesToUp) {
255
+ for (const domain of service.domains) {
256
+ const protocol = options.https ? 'https' : 'http'
257
+ const servicePath = service.path || '/'
258
+ const fullPath = servicePath === '/' ? '' : servicePath
259
+ console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))
260
+ }
261
+ }
262
+ console.log()
263
+ }
264
+ } catch (error) {
265
+ console.error(
266
+ chalk.red('\nāŒ Failed to bring up services:'),
267
+ error instanceof Error ? error.message : error
268
+ )
269
+ process.exit(1)
270
+ }
271
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,25 @@
1
1
  import { Command } from 'commander'
2
+ import { readFileSync } from 'fs'
3
+ import { dirname, join } from 'path'
4
+ import { fileURLToPath } from 'url'
2
5
  import { deployCommand } from './commands/deploy'
6
+ import { downCommand } from './commands/down'
3
7
  import { initCommand } from './commands/init'
8
+ import { redeployCommand } from './commands/redeploy'
4
9
  import { setupCommand } from './commands/setup'
10
+ import { upCommand } from './commands/up'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+ const packageJsonPath = join(__dirname, '..', 'package.json')
15
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
5
16
 
6
17
  const program = new Command()
7
18
 
8
19
  program
9
20
  .name('suthep')
10
21
  .description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')
11
- .version('0.1.0')
22
+ .version(packageJson.version)
12
23
 
13
24
  program
14
25
  .command('init')
@@ -31,4 +42,54 @@ program
31
42
  .option('--no-nginx', 'Skip Nginx configuration')
32
43
  .action(deployCommand)
33
44
 
45
+ program
46
+ .command('down')
47
+ .description('Bring down services (stop containers and disable Nginx configs)')
48
+ .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
49
+ .option('--all', 'Bring down all services', false)
50
+ .argument('[service-name]', 'Name of the service to bring down')
51
+ .action((serviceName, options) => {
52
+ downCommand({
53
+ file: options.file || 'suthep.yml',
54
+ all: options.all || false,
55
+ serviceName: serviceName,
56
+ })
57
+ })
58
+
59
+ program
60
+ .command('up')
61
+ .description('Bring up services (start containers and enable Nginx configs)')
62
+ .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
63
+ .option('--all', 'Bring up all services', false)
64
+ .option('--no-https', 'Skip HTTPS setup')
65
+ .option('--no-nginx', 'Skip Nginx configuration')
66
+ .argument('[service-name]', 'Name of the service to bring up')
67
+ .action((serviceName, options) => {
68
+ upCommand({
69
+ file: options.file || 'suthep.yml',
70
+ all: options.all || false,
71
+ serviceName: serviceName,
72
+ https: options.https !== false,
73
+ nginx: options.nginx !== false,
74
+ })
75
+ })
76
+
77
+ program
78
+ .command('redeploy')
79
+ .description('Redeploy services (bring down and deploy again)')
80
+ .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
81
+ .option('--all', 'Redeploy all services', false)
82
+ .option('--no-https', 'Skip HTTPS setup')
83
+ .option('--no-nginx', 'Skip Nginx configuration')
84
+ .argument('[service-name]', 'Name of the service to redeploy')
85
+ .action((serviceName, options) => {
86
+ redeployCommand({
87
+ file: options.file || 'suthep.yml',
88
+ all: options.all || false,
89
+ serviceName: serviceName,
90
+ https: options.https !== false,
91
+ nginx: options.nginx !== false,
92
+ })
93
+ })
94
+
34
95
  program.parse()