suthep 0.1.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 (65) hide show
  1. package/.editorconfig +17 -0
  2. package/.prettierignore +6 -0
  3. package/.prettierrc +7 -0
  4. package/.vscode/settings.json +19 -0
  5. package/LICENSE +21 -0
  6. package/README.md +217 -0
  7. package/dist/commands/deploy.js +318 -0
  8. package/dist/commands/deploy.js.map +1 -0
  9. package/dist/commands/init.js +188 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/setup.js +90 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/index.js +19 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/certbot.js +64 -0
  16. package/dist/utils/certbot.js.map +1 -0
  17. package/dist/utils/config-loader.js +95 -0
  18. package/dist/utils/config-loader.js.map +1 -0
  19. package/dist/utils/deployment.js +76 -0
  20. package/dist/utils/deployment.js.map +1 -0
  21. package/dist/utils/docker.js +393 -0
  22. package/dist/utils/docker.js.map +1 -0
  23. package/dist/utils/nginx.js +303 -0
  24. package/dist/utils/nginx.js.map +1 -0
  25. package/docs/README.md +95 -0
  26. package/docs/TRANSLATIONS.md +211 -0
  27. package/docs/en/README.md +76 -0
  28. package/docs/en/api-reference.md +545 -0
  29. package/docs/en/architecture.md +369 -0
  30. package/docs/en/commands.md +273 -0
  31. package/docs/en/configuration.md +347 -0
  32. package/docs/en/developer-guide.md +588 -0
  33. package/docs/en/docker-ports-config.md +333 -0
  34. package/docs/en/examples.md +537 -0
  35. package/docs/en/getting-started.md +202 -0
  36. package/docs/en/port-binding.md +268 -0
  37. package/docs/en/troubleshooting.md +441 -0
  38. package/docs/th/README.md +64 -0
  39. package/docs/th/commands.md +202 -0
  40. package/docs/th/configuration.md +325 -0
  41. package/docs/th/getting-started.md +203 -0
  42. package/example/README.md +85 -0
  43. package/example/docker-compose.yml +76 -0
  44. package/example/docker-ports-example.yml +81 -0
  45. package/example/muacle.yml +47 -0
  46. package/example/port-binding-example.yml +45 -0
  47. package/example/suthep.yml +46 -0
  48. package/example/suthep=1.yml +46 -0
  49. package/package.json +45 -0
  50. package/src/commands/deploy.ts +405 -0
  51. package/src/commands/init.ts +214 -0
  52. package/src/commands/setup.ts +112 -0
  53. package/src/index.ts +42 -0
  54. package/src/types/config.ts +52 -0
  55. package/src/utils/certbot.ts +144 -0
  56. package/src/utils/config-loader.ts +121 -0
  57. package/src/utils/deployment.ts +157 -0
  58. package/src/utils/docker.ts +755 -0
  59. package/src/utils/nginx.ts +326 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +98 -0
  62. package/test +0 -0
  63. package/todo.md +6 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +46 -0
package/.editorconfig ADDED
@@ -0,0 +1,17 @@
1
+ # Editor configuration, see http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ max_line_length = off
13
+ trim_trailing_whitespace = false
14
+
15
+ [{Makefile,**.mk}]
16
+ indent_style = tab
17
+ indent_size = 2
@@ -0,0 +1,6 @@
1
+ **/dist
2
+ **/coverage
3
+ **/.turbo
4
+ **/node_modules
5
+
6
+ package.json
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "tabWidth": 2,
3
+ "trailingComma": "es5",
4
+ "printWidth": 100,
5
+ "singleQuote": true,
6
+ "semi": false
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "eslint.workingDirectories": [
3
+ {
4
+ "mode": "auto"
5
+ }
6
+ ],
7
+ "[typescript]": {
8
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
9
+ },
10
+ "editor.codeActionsOnSave": {
11
+ "source.organizeImports": "explicit"
12
+ },
13
+ "editor.formatOnSave": true,
14
+ "editor.insertSpaces": false,
15
+ "files.eol": "\n",
16
+ "files.insertFinalNewline": true,
17
+ "files.trimTrailingWhitespace": true
18
+ }
19
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Montol Saklor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # Suthep
2
+
3
+ A powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
4
+
5
+ ## Features
6
+
7
+ - โœ… Automatic Nginx reverse proxy setup
8
+ - โœ… Automatic HTTPS with Certbot
9
+ - โœ… Zero-downtime deployment
10
+ - โœ… Docker container support
11
+ - โœ… Multiple domain/subdomain support
12
+ - โœ… Health check integration
13
+ - โœ… YAML-based configuration
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g suthep
19
+ # or
20
+ yarn global add suthep
21
+ # or
22
+ pnpm add -g suthep
23
+ ```
24
+
25
+ The tool uses `tsx` to run TypeScript directly, so no build step is required for development. For production, you can still build:
26
+
27
+ ```bash
28
+ npm run build # Optional: compile to JavaScript
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ 1. **Initialize a configuration file:**
34
+ ```bash
35
+ deploy init
36
+ ```
37
+ This will create a `deploy.yml` file with interactive prompts.
38
+
39
+ 2. **Or use the example configuration:**
40
+ ```bash
41
+ cp example.yml deploy.yml
42
+ # Edit deploy.yml with your settings
43
+ ```
44
+
45
+ 3. **Setup prerequisites (Nginx and Certbot):**
46
+ ```bash
47
+ deploy setup
48
+ ```
49
+
50
+ 4. **Deploy your project:**
51
+ ```bash
52
+ deploy deploy
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ The configuration file (`deploy.yml`) supports the following structure:
58
+
59
+ ```yaml
60
+ project:
61
+ name: my-app
62
+ version: 1.0.0
63
+
64
+ services:
65
+ - name: api
66
+ port: 3000
67
+ domains:
68
+ - api.example.com
69
+ - www.api.example.com
70
+ healthCheck:
71
+ path: /health
72
+ interval: 30
73
+ environment:
74
+ NODE_ENV: production
75
+
76
+ - name: webapp
77
+ port: 8080
78
+ docker:
79
+ image: nginx:latest
80
+ container: webapp-container
81
+ port: 80
82
+ domains:
83
+ - example.com
84
+ - www.example.com
85
+
86
+ nginx:
87
+ configPath: /etc/nginx/sites-available
88
+ reloadCommand: sudo nginx -t && sudo systemctl reload nginx
89
+
90
+ certbot:
91
+ email: admin@example.com
92
+ staging: false
93
+
94
+ deployment:
95
+ strategy: rolling
96
+ healthCheckTimeout: 30000
97
+ ```
98
+
99
+ ### Service Configuration
100
+
101
+ Each service can have:
102
+
103
+ - **name**: Unique service name
104
+ - **port**: Port the service runs on
105
+ - **domains**: Array of domain names or subdomains
106
+ - **docker** (optional):
107
+ - **image**: Docker image to use
108
+ - **container**: Container name
109
+ - **port**: Container's internal port
110
+ - **healthCheck** (optional):
111
+ - **path**: Health check endpoint path
112
+ - **interval**: Check interval in seconds
113
+ - **environment** (optional): Environment variables
114
+
115
+ ## Commands
116
+
117
+ ### `deploy init`
118
+
119
+ Initialize a new deployment configuration file.
120
+
121
+ ```bash
122
+ deploy init [-f deploy.yml]
123
+ ```
124
+
125
+ ### `deploy setup`
126
+
127
+ Setup Nginx and Certbot on the system.
128
+
129
+ ```bash
130
+ deploy setup [--nginx-only] [--certbot-only]
131
+ ```
132
+
133
+ ### `deploy deploy`
134
+
135
+ Deploy a project using the configuration file.
136
+
137
+ ```bash
138
+ deploy deploy [-f deploy.yml] [--no-https] [--no-nginx]
139
+ ```
140
+
141
+ Options:
142
+ - `-f, --file`: Configuration file path (default: `deploy.yml`)
143
+ - `--no-https`: Skip HTTPS setup
144
+ - `--no-nginx`: Skip Nginx configuration
145
+
146
+ ## Examples
147
+
148
+ ### Simple Node.js Service
149
+
150
+ ```yaml
151
+ services:
152
+ - name: api
153
+ port: 3000
154
+ domains:
155
+ - api.example.com
156
+ healthCheck:
157
+ path: /health
158
+ ```
159
+
160
+ ### Docker Container Service
161
+
162
+ ```yaml
163
+ services:
164
+ - name: webapp
165
+ port: 8080
166
+ docker:
167
+ image: myapp/webapp:latest
168
+ container: webapp-container
169
+ port: 80
170
+ domains:
171
+ - example.com
172
+ ```
173
+
174
+ ### Multiple Domains
175
+
176
+ ```yaml
177
+ services:
178
+ - name: dashboard
179
+ port: 5000
180
+ domains:
181
+ - dashboard.example.com
182
+ - admin.example.com
183
+ - app.example.com
184
+ ```
185
+
186
+ ### Connect to Existing Docker Container
187
+
188
+ ```yaml
189
+ services:
190
+ - name: database-proxy
191
+ port: 5432
192
+ docker:
193
+ container: postgres-container
194
+ port: 5432
195
+ domains:
196
+ - db.example.com
197
+ ```
198
+
199
+ ## Requirements
200
+
201
+ - Node.js 16+
202
+ - Nginx (installed via `deploy setup`)
203
+ - Certbot (installed via `deploy setup`)
204
+ - Docker (optional, for Docker-based services)
205
+ - sudo access (for Nginx and Certbot operations)
206
+
207
+ ## Cost Optimization
208
+
209
+ This tool helps save costs on VMs by:
210
+ - Efficiently managing multiple services on a single server
211
+ - Automatic reverse proxy setup reduces manual configuration
212
+ - Zero-downtime deployments reduce service interruptions
213
+ - Health checks ensure service reliability
214
+
215
+ ## License
216
+
217
+ [MIT](LICENSE)
@@ -0,0 +1,318 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs-extra";
3
+ import { certificateExists, requestCertificate } from "../utils/certbot.js";
4
+ import { loadConfig } from "../utils/config-loader.js";
5
+ import { deployService, performHealthCheck } from "../utils/deployment.js";
6
+ import { startDockerContainerZeroDowntime, startDockerContainer, swapContainersForZeroDowntime, cleanupTempContainer } from "../utils/docker.js";
7
+ import { generateNginxConfig, generateMultiServiceNginxConfig, writeNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
8
+ async function deployCommand(options) {
9
+ console.log(chalk.blue.bold("\n๐Ÿš€ Deploying Services\n"));
10
+ try {
11
+ if (!await fs.pathExists(options.file)) {
12
+ throw new Error(`Configuration file not found: ${options.file}`);
13
+ }
14
+ console.log(chalk.cyan(`๐Ÿ“„ Loading configuration from ${options.file}...`));
15
+ const config = await loadConfig(options.file);
16
+ console.log(chalk.green(`โœ… Configuration loaded for project: ${config.project.name}`));
17
+ console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(", ")}
18
+ `));
19
+ const domainToServices = /* @__PURE__ */ new Map();
20
+ const allDomains = /* @__PURE__ */ new Set();
21
+ for (const service of config.services) {
22
+ for (const domain of service.domains) {
23
+ allDomains.add(domain);
24
+ if (!domainToServices.has(domain)) {
25
+ domainToServices.set(domain, []);
26
+ }
27
+ domainToServices.get(domain).push(service);
28
+ }
29
+ }
30
+ const serviceTempInfo = /* @__PURE__ */ new Map();
31
+ for (const service of config.services) {
32
+ console.log(chalk.cyan(`
33
+ ๐Ÿ“ฆ Deploying service: ${service.name}`));
34
+ try {
35
+ if (service.docker) {
36
+ console.log(chalk.dim(" ๐Ÿณ Managing Docker container..."));
37
+ if (config.deployment.strategy === "blue-green" || config.deployment.strategy === "rolling") {
38
+ const tempInfo2 = await startDockerContainerZeroDowntime(service);
39
+ serviceTempInfo.set(service.name, tempInfo2);
40
+ if (tempInfo2 && tempInfo2.oldContainerExists) {
41
+ console.log(
42
+ chalk.cyan(
43
+ ` ๐Ÿ”„ Zero-downtime deployment: new container on port ${tempInfo2.tempPort}`
44
+ )
45
+ );
46
+ }
47
+ } else {
48
+ await startDockerContainer(service);
49
+ serviceTempInfo.set(service.name, null);
50
+ }
51
+ } else {
52
+ serviceTempInfo.set(service.name, null);
53
+ }
54
+ const tempInfo = serviceTempInfo.get(service.name) || null;
55
+ await deployService(service, config.deployment, tempInfo);
56
+ if (service.healthCheck) {
57
+ console.log(chalk.dim(` ๐Ÿฅ Performing health check...`));
58
+ const checkPort = tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port;
59
+ const isHealthy = await performHealthCheck(
60
+ `http://localhost:${checkPort}${service.healthCheck.path}`,
61
+ config.deployment.healthCheckTimeout
62
+ );
63
+ if (isHealthy) {
64
+ console.log(chalk.green(` โœ… Service ${service.name} is healthy`));
65
+ } else {
66
+ throw new Error(`Health check failed for service ${service.name}`);
67
+ }
68
+ }
69
+ console.log(chalk.green.bold(`โœจ Service ${service.name} deployed successfully!`));
70
+ } catch (error) {
71
+ console.error(
72
+ chalk.red(`
73
+ โŒ Failed to deploy service ${service.name}:`),
74
+ error instanceof Error ? error.message : error
75
+ );
76
+ throw error;
77
+ }
78
+ }
79
+ const generateNginxConfigsForDomain = (domain, withHttps, portOverrides) => {
80
+ const servicesForDomain = domainToServices.get(domain);
81
+ if (servicesForDomain.length === 1) {
82
+ const service = servicesForDomain[0];
83
+ const portOverride = portOverrides?.get(service.name);
84
+ return generateNginxConfig(service, withHttps, portOverride);
85
+ } else {
86
+ return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides);
87
+ }
88
+ };
89
+ const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(
90
+ (info) => info !== null && info.oldContainerExists
91
+ );
92
+ if (options.nginx) {
93
+ if (needsZeroDowntimeNginx) {
94
+ console.log(chalk.cyan(`
95
+ โš™๏ธ Updating Nginx for zero-downtime deployment...`));
96
+ const tempPortOverrides = /* @__PURE__ */ new Map();
97
+ for (const service of config.services) {
98
+ const tempInfo = serviceTempInfo.get(service.name);
99
+ if (tempInfo && tempInfo.oldContainerExists) {
100
+ tempPortOverrides.set(service.name, tempInfo.tempPort);
101
+ }
102
+ }
103
+ for (const domain of allDomains) {
104
+ const configName = domain.replace(/\./g, "_");
105
+ try {
106
+ const nginxConfigContent = generateNginxConfigsForDomain(
107
+ domain,
108
+ false,
109
+ tempPortOverrides
110
+ );
111
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
112
+ await enableSite(configName, config.nginx.configPath);
113
+ console.log(chalk.green(` โœ… Nginx updated for ${domain} (temporary ports)`));
114
+ } catch (error) {
115
+ console.error(
116
+ chalk.red(` โŒ Failed to update Nginx for ${domain}:`),
117
+ error instanceof Error ? error.message : error
118
+ );
119
+ throw error;
120
+ }
121
+ }
122
+ console.log(chalk.cyan(`
123
+ ๐Ÿ”„ Reloading Nginx to switch to new containers...`));
124
+ await reloadNginx(config.nginx.reloadCommand);
125
+ console.log(chalk.green(` โœ… Nginx reloaded, traffic now routed to new containers`));
126
+ console.log(chalk.cyan(`
127
+ ๐Ÿ”„ Swapping containers for zero-downtime...`));
128
+ for (const service of config.services) {
129
+ const tempInfo = serviceTempInfo.get(service.name);
130
+ if (tempInfo && tempInfo.oldContainerExists && service.docker) {
131
+ try {
132
+ await swapContainersForZeroDowntime(service, tempInfo);
133
+ console.log(chalk.green(` โœ… Container swapped for ${service.name}`));
134
+ } catch (error) {
135
+ console.error(
136
+ chalk.red(` โŒ Failed to swap container for ${service.name}:`),
137
+ error instanceof Error ? error.message : error
138
+ );
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+ console.log(chalk.cyan(`
144
+ โš™๏ธ Updating Nginx back to production ports...`));
145
+ for (const domain of allDomains) {
146
+ const configName = domain.replace(/\./g, "_");
147
+ try {
148
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
149
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
150
+ await enableSite(configName, config.nginx.configPath);
151
+ console.log(chalk.green(` โœ… Nginx updated for ${domain} (production ports)`));
152
+ } catch (error) {
153
+ console.error(
154
+ chalk.red(` โŒ Failed to update Nginx for ${domain}:`),
155
+ error instanceof Error ? error.message : error
156
+ );
157
+ throw error;
158
+ }
159
+ }
160
+ console.log(chalk.cyan(`
161
+ ๐Ÿ”„ Reloading Nginx to switch to production ports...`));
162
+ await reloadNginx(config.nginx.reloadCommand);
163
+ console.log(chalk.green(` โœ… Nginx reloaded, traffic now routed to production containers`));
164
+ console.log(chalk.cyan(`
165
+ ๐Ÿงน Cleaning up temporary containers...`));
166
+ for (const service of config.services) {
167
+ const tempInfo = serviceTempInfo.get(service.name);
168
+ if (tempInfo && tempInfo.oldContainerExists) {
169
+ await cleanupTempContainer(tempInfo.tempContainerName);
170
+ }
171
+ }
172
+ } else {
173
+ console.log(chalk.cyan(`
174
+ โš™๏ธ Configuring Nginx reverse proxy...`));
175
+ for (const domain of allDomains) {
176
+ const servicesForDomain = domainToServices.get(domain);
177
+ const configName = domain.replace(/\./g, "_");
178
+ try {
179
+ if (servicesForDomain.length > 1) {
180
+ console.log(
181
+ chalk.cyan(
182
+ ` ๐Ÿ“‹ Configuring ${domain} with ${servicesForDomain.length} services: ${servicesForDomain.map((s) => s.name).join(", ")}`
183
+ )
184
+ );
185
+ console.log(
186
+ chalk.dim(
187
+ ` All services will share the same nginx config file: ${configName}.conf`
188
+ )
189
+ );
190
+ }
191
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
192
+ const wasOverridden = await writeNginxConfig(
193
+ configName,
194
+ config.nginx.configPath,
195
+ nginxConfigContent
196
+ );
197
+ if (wasOverridden) {
198
+ console.log(
199
+ chalk.yellow(
200
+ ` ๐Ÿ”„ Nginx config "${configName}.conf" already exists, deleting and recreating with new configuration...`
201
+ )
202
+ );
203
+ }
204
+ await enableSite(configName, config.nginx.configPath);
205
+ console.log(chalk.green(` โœ… Nginx configured for ${domain}`));
206
+ } catch (error) {
207
+ console.error(
208
+ chalk.red(` โŒ Failed to configure Nginx for ${domain}:`),
209
+ error instanceof Error ? error.message : error
210
+ );
211
+ throw error;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ if (options.https && allDomains.size > 0) {
217
+ console.log(chalk.cyan(`
218
+ ๐Ÿ” Setting up HTTPS certificates...`));
219
+ for (const domain of allDomains) {
220
+ try {
221
+ const exists = await certificateExists(domain);
222
+ if (exists) {
223
+ console.log(
224
+ chalk.green(
225
+ ` โœ… SSL certificate already exists for ${domain}, skipping certificate creation`
226
+ )
227
+ );
228
+ console.log(
229
+ chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)
230
+ );
231
+ } else {
232
+ console.log(chalk.cyan(` ๐Ÿ“œ Requesting SSL certificate for ${domain}...`));
233
+ try {
234
+ await requestCertificate(domain, config.certbot.email, config.certbot.staging);
235
+ console.log(chalk.green(` โœ… SSL certificate obtained for ${domain}`));
236
+ } catch (error) {
237
+ const errorMessage = error?.message || String(error) || "";
238
+ if (errorMessage.includes("already exists") || errorMessage.includes("Skipping certificate creation")) {
239
+ console.log(
240
+ chalk.green(
241
+ ` โœ… SSL certificate already exists for ${domain} (detected during request), skipping...`
242
+ )
243
+ );
244
+ } else {
245
+ throw error;
246
+ }
247
+ }
248
+ }
249
+ } catch (error) {
250
+ console.log(
251
+ chalk.yellow(
252
+ ` โš ๏ธ Failed to obtain SSL for ${domain}: ${error instanceof Error ? error.message : error}`
253
+ )
254
+ );
255
+ }
256
+ }
257
+ if (options.nginx) {
258
+ console.log(chalk.cyan(`
259
+ ๐Ÿ”„ Updating Nginx configs with HTTPS...`));
260
+ for (const domain of allDomains) {
261
+ const configName = domain.replace(/\./g, "_");
262
+ try {
263
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, true);
264
+ const wasOverridden = await writeNginxConfig(
265
+ configName,
266
+ config.nginx.configPath,
267
+ nginxConfigContent
268
+ );
269
+ if (wasOverridden) {
270
+ console.log(
271
+ chalk.yellow(
272
+ ` ๐Ÿ”„ Nginx config "${configName}.conf" already exists, deleting and recreating with new HTTPS configuration...`
273
+ )
274
+ );
275
+ }
276
+ console.log(chalk.green(` โœ… HTTPS config updated for ${domain}`));
277
+ } catch (error) {
278
+ console.error(
279
+ chalk.red(` โŒ Failed to update HTTPS config for ${domain}:`),
280
+ error instanceof Error ? error.message : error
281
+ );
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+ }
287
+ if (options.nginx && !needsZeroDowntimeNginx) {
288
+ console.log(chalk.cyan(`
289
+ ๐Ÿ”„ Reloading Nginx...`));
290
+ await reloadNginx(config.nginx.reloadCommand);
291
+ } else if (options.nginx && needsZeroDowntimeNginx) {
292
+ console.log(chalk.cyan(`
293
+ ๐Ÿ”„ Final Nginx reload with HTTPS...`));
294
+ await reloadNginx(config.nginx.reloadCommand);
295
+ }
296
+ console.log(chalk.green.bold("\n๐ŸŽ‰ All services deployed successfully!\n"));
297
+ console.log(chalk.cyan("๐Ÿ“‹ Service URLs:"));
298
+ for (const service of config.services) {
299
+ for (const domain of service.domains) {
300
+ const protocol = options.https ? "https" : "http";
301
+ const servicePath = service.path || "/";
302
+ const fullPath = servicePath === "/" ? "" : servicePath;
303
+ console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`));
304
+ }
305
+ }
306
+ console.log();
307
+ } catch (error) {
308
+ console.error(
309
+ chalk.red("\nโŒ Deployment failed:"),
310
+ error instanceof Error ? error.message : error
311
+ );
312
+ process.exit(1);
313
+ }
314
+ }
315
+ export {
316
+ deployCommand
317
+ };
318
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy.js","sources":["../../src/commands/deploy.ts"],"sourcesContent":["import chalk from 'chalk'\nimport fs from 'fs-extra'\nimport type { ServiceConfig } from '../types/config'\nimport { certificateExists, requestCertificate } from '../utils/certbot'\nimport { loadConfig } from '../utils/config-loader'\nimport { deployService, performHealthCheck } from '../utils/deployment'\nimport {\n cleanupTempContainer,\n startDockerContainer,\n startDockerContainerZeroDowntime,\n swapContainersForZeroDowntime,\n type ZeroDowntimeContainerInfo,\n} from '../utils/docker'\nimport {\n enableSite,\n generateMultiServiceNginxConfig,\n generateNginxConfig,\n reloadNginx,\n writeNginxConfig,\n} from '../utils/nginx'\n\ninterface DeployOptions {\n file: string\n https: boolean\n nginx: boolean\n}\n\nexport async function deployCommand(options: DeployOptions): Promise<void> {\n console.log(chalk.blue.bold('\\n๐Ÿš€ Deploying Services\\n'))\n\n try {\n // Load configuration\n if (!(await fs.pathExists(options.file))) {\n throw new Error(`Configuration file not found: ${options.file}`)\n }\n\n console.log(chalk.cyan(`๐Ÿ“„ Loading configuration from ${options.file}...`))\n const config = await loadConfig(options.file)\n\n console.log(chalk.green(`โœ… Configuration loaded for project: ${config.project.name}`))\n console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\\n`))\n\n // Group services by domain\n const domainToServices = new Map<string, ServiceConfig[]>()\n const allDomains = new Set<string>()\n\n for (const service of config.services) {\n for (const domain of service.domains) {\n allDomains.add(domain)\n if (!domainToServices.has(domain)) {\n domainToServices.set(domain, [])\n }\n domainToServices.get(domain)!.push(service)\n }\n }\n\n // Deploy each service (Docker, health checks, etc.)\n // Track zero-downtime info for services that need it\n const serviceTempInfo = new Map<string, ZeroDowntimeContainerInfo | null>()\n\n for (const service of config.services) {\n console.log(chalk.cyan(`\\n๐Ÿ“ฆ Deploying service: ${service.name}`))\n\n try {\n // Start Docker container if configured\n if (service.docker) {\n console.log(chalk.dim(' ๐Ÿณ Managing Docker container...'))\n\n // Use zero-downtime deployment if strategy is blue-green or rolling\n if (\n config.deployment.strategy === 'blue-green' ||\n config.deployment.strategy === 'rolling'\n ) {\n const tempInfo = await startDockerContainerZeroDowntime(service)\n serviceTempInfo.set(service.name, tempInfo)\n\n if (tempInfo && tempInfo.oldContainerExists) {\n console.log(\n chalk.cyan(\n ` ๐Ÿ”„ Zero-downtime deployment: new container on port ${tempInfo.tempPort}`\n )\n )\n }\n } else {\n // Fallback to regular deployment\n await startDockerContainer(service)\n serviceTempInfo.set(service.name, null)\n }\n } else {\n serviceTempInfo.set(service.name, null)\n }\n\n // Deploy the service (with temp info for zero-downtime)\n const tempInfo = serviceTempInfo.get(service.name) || null\n await deployService(service, config.deployment, tempInfo)\n\n // Perform health check on appropriate port\n if (service.healthCheck) {\n console.log(chalk.dim(` ๐Ÿฅ Performing health check...`))\n const checkPort =\n tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port\n const isHealthy = await performHealthCheck(\n `http://localhost:${checkPort}${service.healthCheck.path}`,\n config.deployment.healthCheckTimeout\n )\n\n if (isHealthy) {\n console.log(chalk.green(` โœ… Service ${service.name} is healthy`))\n } else {\n throw new Error(`Health check failed for service ${service.name}`)\n }\n }\n\n console.log(chalk.green.bold(`โœจ Service ${service.name} deployed successfully!`))\n } catch (error) {\n console.error(\n chalk.red(`\\nโŒ Failed to deploy service ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Helper function to generate nginx configs with optional port overrides\n const generateNginxConfigsForDomain = (\n domain: string,\n withHttps: boolean,\n portOverrides?: Map<string, number>\n ): string => {\n const servicesForDomain = domainToServices.get(domain)!\n if (servicesForDomain.length === 1) {\n const service = servicesForDomain[0]\n const portOverride = portOverrides?.get(service.name)\n return generateNginxConfig(service, withHttps, portOverride)\n } else {\n return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides)\n }\n }\n\n // Check if we need zero-downtime nginx updates (any service has temp container)\n const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(\n (info) => info !== null && info.oldContainerExists\n )\n\n // Configure Nginx per domain\n if (options.nginx) {\n // If zero-downtime, first update nginx to point to temp ports\n if (needsZeroDowntimeNginx) {\n console.log(chalk.cyan(`\\nโš™๏ธ Updating Nginx for zero-downtime deployment...`))\n\n // Build port override map for temp ports\n const tempPortOverrides = new Map<string, number>()\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists) {\n tempPortOverrides.set(service.name, tempInfo.tempPort)\n }\n }\n\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(\n domain,\n false,\n tempPortOverrides\n )\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n await enableSite(configName, config.nginx.configPath)\n console.log(chalk.green(` โœ… Nginx updated for ${domain} (temporary ports)`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to update Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Reload nginx to switch to temp ports (graceful reload, no connection drops)\n console.log(chalk.cyan(`\\n๐Ÿ”„ Reloading Nginx to switch to new containers...`))\n await reloadNginx(config.nginx.reloadCommand)\n console.log(chalk.green(` โœ… Nginx reloaded, traffic now routed to new containers`))\n\n // Now swap containers (stop old, promote new)\n console.log(chalk.cyan(`\\n๐Ÿ”„ Swapping containers for zero-downtime...`))\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists && service.docker) {\n try {\n await swapContainersForZeroDowntime(service, tempInfo)\n console.log(chalk.green(` โœ… Container swapped for ${service.name}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to swap container for ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n\n // Update nginx back to original ports (before stopping temp containers)\n console.log(chalk.cyan(`\\nโš™๏ธ Updating Nginx back to production ports...`))\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(domain, false)\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n await enableSite(configName, config.nginx.configPath)\n console.log(chalk.green(` โœ… Nginx updated for ${domain} (production ports)`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to update Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Reload nginx to switch to production ports (graceful reload)\n console.log(chalk.cyan(`\\n๐Ÿ”„ Reloading Nginx to switch to production ports...`))\n await reloadNginx(config.nginx.reloadCommand)\n console.log(chalk.green(` โœ… Nginx reloaded, traffic now routed to production containers`))\n\n // Clean up temp containers (nginx already pointing to production, so safe to remove)\n console.log(chalk.cyan(`\\n๐Ÿงน Cleaning up temporary containers...`))\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists) {\n await cleanupTempContainer(tempInfo.tempContainerName)\n }\n }\n } else {\n // Regular nginx configuration (no zero-downtime needed)\n console.log(chalk.cyan(`\\nโš™๏ธ Configuring Nginx reverse proxy...`))\n\n for (const domain of allDomains) {\n const servicesForDomain = domainToServices.get(domain)!\n const configName = domain.replace(/\\./g, '_')\n\n try {\n // Log domain and services configuration\n if (servicesForDomain.length > 1) {\n console.log(\n chalk.cyan(\n ` ๐Ÿ“‹ Configuring ${domain} with ${\n servicesForDomain.length\n } services: ${servicesForDomain.map((s) => s.name).join(', ')}`\n )\n )\n console.log(\n chalk.dim(\n ` All services will share the same nginx config file: ${configName}.conf`\n )\n )\n }\n\n // Generate Nginx config\n const nginxConfigContent = generateNginxConfigsForDomain(domain, false)\n\n // Check if config file already exists and write/override it\n const wasOverridden = await writeNginxConfig(\n configName,\n config.nginx.configPath,\n nginxConfigContent\n )\n\n if (wasOverridden) {\n console.log(\n chalk.yellow(\n ` ๐Ÿ”„ Nginx config \"${configName}.conf\" already exists, deleting and recreating with new configuration...`\n )\n )\n }\n\n await enableSite(configName, config.nginx.configPath)\n\n console.log(chalk.green(` โœ… Nginx configured for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to configure Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n }\n\n // Setup HTTPS with Certbot (per domain, not per service)\n if (options.https && allDomains.size > 0) {\n console.log(chalk.cyan(`\\n๐Ÿ” Setting up HTTPS certificates...`))\n\n for (const domain of allDomains) {\n try {\n // Check if certificate already exists\n const exists = await certificateExists(domain)\n if (exists) {\n console.log(\n chalk.green(\n ` โœ… SSL certificate already exists for ${domain}, skipping certificate creation`\n )\n )\n console.log(\n chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)\n )\n } else {\n // Request new certificate\n console.log(chalk.cyan(` ๐Ÿ“œ Requesting SSL certificate for ${domain}...`))\n try {\n await requestCertificate(domain, config.certbot.email, config.certbot.staging)\n console.log(chalk.green(` โœ… SSL certificate obtained for ${domain}`))\n } catch (error: any) {\n // Check if error is because certificate already exists (race condition or check missed it)\n const errorMessage = error?.message || String(error) || ''\n if (\n errorMessage.includes('already exists') ||\n errorMessage.includes('Skipping certificate creation')\n ) {\n console.log(\n chalk.green(\n ` โœ… SSL certificate already exists for ${domain} (detected during request), skipping...`\n )\n )\n } else {\n throw error // Re-throw if it's a different error\n }\n }\n }\n } catch (error) {\n console.log(\n chalk.yellow(\n ` โš ๏ธ Failed to obtain SSL for ${domain}: ${\n error instanceof Error ? error.message : error\n }`\n )\n )\n }\n }\n\n // Update Nginx configs with HTTPS\n if (options.nginx) {\n console.log(chalk.cyan(`\\n๐Ÿ”„ Updating Nginx configs with HTTPS...`))\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(domain, true)\n const wasOverridden = await writeNginxConfig(\n configName,\n config.nginx.configPath,\n nginxConfigContent\n )\n\n if (wasOverridden) {\n console.log(\n chalk.yellow(\n ` ๐Ÿ”„ Nginx config \"${configName}.conf\" already exists, deleting and recreating with new HTTPS configuration...`\n )\n )\n }\n console.log(chalk.green(` โœ… HTTPS config updated for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to update HTTPS config for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n }\n\n // Final reload Nginx after all configurations (only if we didn't already reload for zero-downtime)\n if (options.nginx && !needsZeroDowntimeNginx) {\n console.log(chalk.cyan(`\\n๐Ÿ”„ Reloading Nginx...`))\n await reloadNginx(config.nginx.reloadCommand)\n } else if (options.nginx && needsZeroDowntimeNginx) {\n // Final reload after HTTPS update\n console.log(chalk.cyan(`\\n๐Ÿ”„ Final Nginx reload with HTTPS...`))\n await reloadNginx(config.nginx.reloadCommand)\n }\n\n console.log(chalk.green.bold('\\n๐ŸŽ‰ All services deployed successfully!\\n'))\n\n // Print service URLs\n console.log(chalk.cyan('๐Ÿ“‹ Service URLs:'))\n for (const service of config.services) {\n for (const domain of service.domains) {\n const protocol = options.https ? 'https' : 'http'\n const servicePath = service.path || '/'\n const fullPath = servicePath === '/' ? '' : servicePath\n console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))\n }\n }\n console.log()\n } catch (error) {\n console.error(\n chalk.red('\\nโŒ Deployment failed:'),\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n"],"names":["tempInfo"],"mappings":"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"}