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,135 @@
1
+ import { config } from 'dotenv'
2
+ import fs from 'fs-extra'
3
+ import { resolve } from 'path'
4
+
5
+ // Store loaded .env variables globally so they can be accessed by other modules
6
+ let loadedEnvVars: Record<string, string> = {}
7
+
8
+ /**
9
+ * Get the currently loaded .env variables
10
+ */
11
+ export function getLoadedEnvVars(): Record<string, string> {
12
+ return { ...loadedEnvVars }
13
+ }
14
+
15
+ /**
16
+ * Set the loaded .env variables (used internally)
17
+ */
18
+ export function setLoadedEnvVars(vars: Record<string, string>): void {
19
+ loadedEnvVars = { ...vars }
20
+ }
21
+
22
+ /**
23
+ * Load environment variables from .env files
24
+ * Searches for .env files in the following order:
25
+ * 1. .env.local (highest priority, should be gitignored)
26
+ * 2. .env
27
+ *
28
+ * @param configDir Optional directory path to search for .env files (defaults to current working directory)
29
+ * @returns Object containing loaded environment variables
30
+ */
31
+ export async function loadEnvFiles(configDir?: string): Promise<Record<string, string>> {
32
+ const baseDir = configDir || process.cwd()
33
+ const envFiles = ['.env.local', '.env']
34
+ const loadedVars: Record<string, string> = {}
35
+
36
+ for (const envFile of envFiles) {
37
+ const envPath = resolve(baseDir, envFile)
38
+
39
+ if (await fs.pathExists(envPath)) {
40
+ try {
41
+ // Load .env file using dotenv
42
+ const result = config({ path: envPath, override: false })
43
+
44
+ if (result.error) {
45
+ console.warn(`Warning: Failed to load ${envFile}: ${result.error.message}`)
46
+ continue
47
+ }
48
+
49
+ // Merge loaded variables (later files override earlier ones)
50
+ if (result.parsed) {
51
+ Object.assign(loadedVars, result.parsed)
52
+ }
53
+ } catch (error) {
54
+ console.warn(
55
+ `Warning: Error loading ${envFile}: ${error instanceof Error ? error.message : error}`
56
+ )
57
+ }
58
+ }
59
+ }
60
+
61
+ return loadedVars
62
+ }
63
+
64
+ /**
65
+ * Load and apply environment variables from .env files to process.env
66
+ * This will load .env files and merge them into process.env
67
+ *
68
+ * @param configDir Optional directory path to search for .env files
69
+ * @returns Object containing loaded environment variables
70
+ */
71
+ export async function loadAndApplyEnvFiles(configDir?: string): Promise<Record<string, string>> {
72
+ const baseDir = configDir || process.cwd()
73
+ const envFiles = ['.env.local', '.env']
74
+ const loadedVars: Record<string, string> = {}
75
+
76
+ for (const envFile of envFiles) {
77
+ const envPath = resolve(baseDir, envFile)
78
+
79
+ if (await fs.pathExists(envPath)) {
80
+ try {
81
+ // Load .env file using dotenv and apply to process.env
82
+ // override: false means existing process.env vars take precedence
83
+ const result = config({ path: envPath, override: false })
84
+
85
+ if (result.error) {
86
+ console.warn(`Warning: Failed to load ${envFile}: ${result.error.message}`)
87
+ continue
88
+ }
89
+
90
+ // Merge loaded variables into our return object
91
+ if (result.parsed) {
92
+ Object.assign(loadedVars, result.parsed)
93
+ }
94
+ } catch (error) {
95
+ console.warn(
96
+ `Warning: Error loading ${envFile}: ${error instanceof Error ? error.message : error}`
97
+ )
98
+ }
99
+ }
100
+ }
101
+
102
+ // Store loaded vars globally for use by other modules
103
+ setLoadedEnvVars(loadedVars)
104
+
105
+ return loadedVars
106
+ }
107
+
108
+ /**
109
+ * Merge environment variables from multiple sources
110
+ * Priority order (highest to lowest): CLI env vars > Service env vars > .env file vars
111
+ *
112
+ * @param envVars Environment variables from .env files
113
+ * @param serviceEnv Service-specific environment variables from config
114
+ * @param cliEnvVars Environment variables from CLI (highest priority)
115
+ * @returns Merged environment variables object
116
+ */
117
+ export function mergeEnvVars(
118
+ envVars: Record<string, string>,
119
+ serviceEnv?: Record<string, string>,
120
+ cliEnvVars?: Record<string, string>
121
+ ): Record<string, string> {
122
+ const merged = { ...envVars }
123
+
124
+ // Service environment variables override .env file variables
125
+ if (serviceEnv) {
126
+ Object.assign(merged, serviceEnv)
127
+ }
128
+
129
+ // CLI environment variables override both .env and service variables (highest priority)
130
+ if (cliEnvVars) {
131
+ Object.assign(merged, cliEnvVars)
132
+ }
133
+
134
+ return merged
135
+ }
@@ -0,0 +1,443 @@
1
+ import { execa } from 'execa'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import type { ServiceConfig } from '../types/config'
5
+
6
+ /**
7
+ * Check if a domain is a root domain (no subdomain)
8
+ * Root domain: example.com (2 parts)
9
+ * Subdomain: dev.example.com (3+ parts)
10
+ */
11
+ export function isRootDomain(domain: string): boolean {
12
+ // Remove 'www.' prefix if present for checking
13
+ const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain
14
+ const partsWithoutWww = domainWithoutWww.split('.')
15
+ // Root domain has exactly 2 parts (domain + TLD)
16
+ return partsWithoutWww.length === 2
17
+ }
18
+
19
+ /**
20
+ * Get canonical domain for a given domain
21
+ * For root domains with both www and non-www, returns www version
22
+ * Otherwise returns the domain itself
23
+ */
24
+ export function getCanonicalDomain(domain: string, allDomains: Set<string>): string {
25
+ if (isRootDomain(domain)) {
26
+ const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain
27
+ const wwwVersion = `www.${domainWithoutWww}`
28
+ const nonWwwVersion = domainWithoutWww
29
+
30
+ // If we have both www and non-www versions, prefer www
31
+ if (allDomains.has(wwwVersion) && allDomains.has(nonWwwVersion)) {
32
+ return wwwVersion
33
+ }
34
+ }
35
+ return domain
36
+ }
37
+
38
+ /**
39
+ * Normalize domain list to handle www/non-www variants
40
+ * For root domains: combine www and non-www in same server_name
41
+ * For subdomains: keep as configured
42
+ * Returns canonical domain (for SSL certs) and combined server names
43
+ */
44
+ function normalizeDomains(domains: string[]): {
45
+ canonical: string
46
+ serverNames: string
47
+ } {
48
+ // Use first domain as canonical (for SSL certificates)
49
+ let canonical: string = domains[0]
50
+
51
+ // For root domains with both www and non-www, prefer www as canonical
52
+ const domainSet = new Set(domains)
53
+ for (const domain of domains) {
54
+ if (isRootDomain(domain)) {
55
+ const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain
56
+ const wwwVersion = `www.${domainWithoutWww}`
57
+ const nonWwwVersion = domainWithoutWww
58
+
59
+ // If we have both www and non-www versions of a root domain
60
+ if (domainSet.has(wwwVersion) && domainSet.has(nonWwwVersion)) {
61
+ // Prefer www for root domains (for SSL cert path)
62
+ canonical = wwwVersion
63
+ break
64
+ }
65
+ }
66
+ }
67
+
68
+ // Combine all domains in server_name
69
+ // For root domains with both www and non-www, ensure www comes first
70
+ const sortedDomains = [...domains]
71
+ if (canonical.startsWith('www.')) {
72
+ const nonWww = canonical.substring(4)
73
+ const wwwIndex = sortedDomains.indexOf(canonical)
74
+ const nonWwwIndex = sortedDomains.indexOf(nonWww)
75
+ if (wwwIndex !== -1 && nonWwwIndex !== -1 && wwwIndex > nonWwwIndex) {
76
+ // Swap to put www first
77
+ ;[sortedDomains[wwwIndex], sortedDomains[nonWwwIndex]] = [
78
+ sortedDomains[nonWwwIndex],
79
+ sortedDomains[wwwIndex],
80
+ ]
81
+ }
82
+ }
83
+ const serverNames = sortedDomains.join(' ')
84
+
85
+ return {
86
+ canonical,
87
+ serverNames,
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Generate Nginx server block configuration for a service
93
+ */
94
+ export function generateNginxConfig(
95
+ service: ServiceConfig,
96
+ withHttps: boolean,
97
+ portOverride?: number
98
+ ): string {
99
+ // Normalize domains - combine www and root domains in same server_name
100
+ const { canonical, serverNames } = normalizeDomains(service.domains)
101
+
102
+ // Use canonical domain for upstream naming and SSL certificates
103
+ const domainSafe = canonical.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
104
+ const upstreamName = `${domainSafe}_${service.name}`
105
+ const servicePath = service.path || '/'
106
+ const port = portOverride || service.port
107
+
108
+ let config = `# Nginx configuration for ${service.name}\n\n`
109
+
110
+ // Upstream configuration
111
+ config += `upstream ${upstreamName} {\n`
112
+ config += ` server localhost:${port} max_fails=3 fail_timeout=30s;\n`
113
+ config += ` keepalive 32;\n`
114
+ config += `}\n\n`
115
+
116
+ if (withHttps) {
117
+ // HTTP server - redirect to HTTPS
118
+ config += `server {\n`
119
+ config += ` listen 80;\n`
120
+ config += ` listen [::]:80;\n`
121
+ config += ` server_name ${serverNames};\n\n`
122
+ config += ` # Redirect all HTTP to HTTPS\n`
123
+ config += ` return 301 https://$server_name$request_uri;\n`
124
+ config += `}\n\n`
125
+
126
+ // HTTPS server
127
+ config += `server {\n`
128
+ config += ` listen 443 ssl http2;\n`
129
+ config += ` listen [::]:443 ssl http2;\n`
130
+ config += ` server_name ${serverNames};\n\n`
131
+
132
+ // SSL configuration
133
+ config += ` # SSL Configuration\n`
134
+ config += ` ssl_certificate /etc/letsencrypt/live/${canonical}/fullchain.pem;\n`
135
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${canonical}/privkey.pem;\n`
136
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
137
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
138
+ config += ` ssl_prefer_server_ciphers on;\n\n`
139
+ } else {
140
+ // HTTP only server
141
+ config += `server {\n`
142
+ config += ` listen 80;\n`
143
+ config += ` listen [::]:80;\n`
144
+ config += ` server_name ${serverNames};\n\n`
145
+ }
146
+
147
+ // Logging
148
+ config += ` # Logging\n`
149
+ config += ` access_log /var/log/nginx/${service.name}_access.log;\n`
150
+ config += ` error_log /var/log/nginx/${service.name}_error.log;\n\n`
151
+
152
+ // Client settings
153
+ config += ` # Client settings\n`
154
+ config += ` client_max_body_size 100M;\n\n`
155
+
156
+ // Proxy settings
157
+ config += ` # Proxy settings\n`
158
+ config += ` location ${servicePath} {\n`
159
+ config += ` proxy_pass http://${upstreamName};\n`
160
+ config += ` proxy_http_version 1.1;\n`
161
+ config += ` proxy_set_header Upgrade $http_upgrade;\n`
162
+ config += ` proxy_set_header Connection 'upgrade';\n`
163
+ config += ` proxy_set_header Host $host;\n`
164
+ config += ` proxy_set_header X-Real-IP $remote_addr;\n`
165
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`
166
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`
167
+ config += ` proxy_cache_bypass $http_upgrade;\n`
168
+ config += ` proxy_connect_timeout 60s;\n`
169
+ config += ` proxy_send_timeout 60s;\n`
170
+ config += ` proxy_read_timeout 60s;\n`
171
+ config += ` }\n`
172
+
173
+ // Health check endpoint (if configured)
174
+ if (service.healthCheck) {
175
+ config += `\n # Health check endpoint\n`
176
+ config += ` location ${service.healthCheck.path} {\n`
177
+ config += ` proxy_pass http://${upstreamName};\n`
178
+ config += ` access_log off;\n`
179
+ config += ` }\n`
180
+ }
181
+
182
+ config += `}\n`
183
+
184
+ return config
185
+ }
186
+
187
+ /**
188
+ * Generate Nginx configuration for multiple services on the same domain
189
+ * Groups services by domain and creates location blocks for each service path
190
+ * Combines upstreams when multiple services share the same port
191
+ */
192
+ export function generateMultiServiceNginxConfig(
193
+ services: ServiceConfig[],
194
+ domain: string,
195
+ withHttps: boolean,
196
+ portOverrides?: Map<string, number>
197
+ ): string {
198
+ const upstreams: string[] = []
199
+ const locations: string[] = []
200
+ const healthChecks: string[] = []
201
+
202
+ // Collect all domains from services to check for www/non-www variants
203
+ const allServiceDomains = new Set<string>()
204
+ for (const service of services) {
205
+ for (const d of service.domains) {
206
+ allServiceDomains.add(d)
207
+ }
208
+ }
209
+
210
+ // Determine canonical domain (for SSL certs) and combined server names
211
+ // For root domains: combine www and non-www in same server_name
212
+ // For subdomains: use as-is
213
+ let canonicalDomain = domain
214
+ let serverNames = domain
215
+
216
+ if (isRootDomain(domain)) {
217
+ const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain
218
+ const wwwVersion = `www.${domainWithoutWww}`
219
+ const nonWwwVersion = domainWithoutWww
220
+
221
+ if (allServiceDomains.has(wwwVersion) && allServiceDomains.has(nonWwwVersion)) {
222
+ // Prefer www for root domains (for SSL cert path)
223
+ canonicalDomain = wwwVersion
224
+ // Combine both in server_name (www first)
225
+ serverNames = `${wwwVersion} ${nonWwwVersion}`
226
+ }
227
+ }
228
+ // Subdomains are left as-is
229
+
230
+ // Sort services by path length (longest first) to ensure specific paths are matched before general ones
231
+ const sortedServices = [...services].sort((a, b) => {
232
+ const pathA = (a.path || '/').length
233
+ const pathB = (b.path || '/').length
234
+ return pathB - pathA
235
+ })
236
+
237
+ // Map port to upstream name - combine services with same port into one upstream
238
+ const portToUpstreamName = new Map<number, string>()
239
+ const domainSafe = canonicalDomain.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
240
+
241
+ // First pass: create upstreams grouped by port
242
+ for (const service of sortedServices) {
243
+ const port = portOverrides?.get(service.name) || service.port
244
+
245
+ // If we haven't seen this port before, create a new upstream
246
+ if (!portToUpstreamName.has(port)) {
247
+ // Use domain_port format for upstream name to ensure uniqueness
248
+ // Since ports are unique within a domain, this format ensures no conflicts
249
+ const upstreamName = `${domainSafe}_port_${port}`
250
+
251
+ portToUpstreamName.set(port, upstreamName)
252
+
253
+ // Generate upstream block
254
+ upstreams.push(`upstream ${upstreamName} {`)
255
+ upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`)
256
+ upstreams.push(` keepalive 32;`)
257
+ upstreams.push(`}`)
258
+ }
259
+ }
260
+
261
+ // Second pass: create location blocks for each service, using the shared upstream
262
+ for (const service of sortedServices) {
263
+ const servicePath = service.path || '/'
264
+ const port = portOverrides?.get(service.name) || service.port
265
+ const upstreamName = portToUpstreamName.get(port)!
266
+
267
+ // Generate location block
268
+ if (servicePath === '/') {
269
+ // Root path - use exact match or default
270
+ locations.push(` # Service: ${service.name}`)
271
+ locations.push(` location / {`)
272
+ } else {
273
+ // Specific path - use prefix match
274
+ locations.push(` # Service: ${service.name}`)
275
+ locations.push(` location ${servicePath} {`)
276
+ }
277
+
278
+ locations.push(` proxy_pass http://${upstreamName};`)
279
+ locations.push(` proxy_http_version 1.1;`)
280
+ locations.push(` proxy_set_header Upgrade $http_upgrade;`)
281
+ locations.push(` proxy_set_header Connection 'upgrade';`)
282
+ locations.push(` proxy_set_header Host $host;`)
283
+ locations.push(` proxy_set_header X-Real-IP $remote_addr;`)
284
+ locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
285
+ locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
286
+ locations.push(` proxy_cache_bypass $http_upgrade;`)
287
+ locations.push(` proxy_connect_timeout 60s;`)
288
+ locations.push(` proxy_send_timeout 60s;`)
289
+ locations.push(` proxy_read_timeout 60s;`)
290
+ locations.push(` }`)
291
+
292
+ // Health check endpoint
293
+ if (service.healthCheck) {
294
+ healthChecks.push(` # Health check for ${service.name}`)
295
+ healthChecks.push(` location ${service.healthCheck.path} {`)
296
+ healthChecks.push(` proxy_pass http://${upstreamName};`)
297
+ healthChecks.push(` access_log off;`)
298
+ healthChecks.push(` }`)
299
+ }
300
+ }
301
+
302
+ let config = `# Nginx configuration for ${canonicalDomain}\n`
303
+ config += `# Multiple services on the same domain\n\n`
304
+
305
+ // Add upstreams
306
+ config += upstreams.join('\n') + '\n\n'
307
+
308
+ if (withHttps) {
309
+ // HTTP server - redirect to HTTPS
310
+ config += `server {\n`
311
+ config += ` listen 80;\n`
312
+ config += ` listen [::]:80;\n`
313
+ config += ` server_name ${serverNames};\n\n`
314
+ config += ` # Redirect all HTTP to HTTPS\n`
315
+ config += ` return 301 https://$server_name$request_uri;\n`
316
+ config += `}\n\n`
317
+
318
+ // HTTPS server
319
+ config += `server {\n`
320
+ config += ` listen 443 ssl http2;\n`
321
+ config += ` listen [::]:443 ssl http2;\n`
322
+ config += ` server_name ${serverNames};\n\n`
323
+
324
+ // SSL configuration
325
+ config += ` # SSL Configuration\n`
326
+ config += ` ssl_certificate /etc/letsencrypt/live/${canonicalDomain}/fullchain.pem;\n`
327
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${canonicalDomain}/privkey.pem;\n`
328
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
329
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
330
+ config += ` ssl_prefer_server_ciphers on;\n\n`
331
+ } else {
332
+ // HTTP only server
333
+ config += `server {\n`
334
+ config += ` listen 80;\n`
335
+ config += ` listen [::]:80;\n`
336
+ config += ` server_name ${serverNames};\n\n`
337
+ }
338
+
339
+ // Logging
340
+ config += ` # Logging\n`
341
+ config += ` access_log /var/log/nginx/${canonicalDomain}_access.log;\n`
342
+ config += ` error_log /var/log/nginx/${canonicalDomain}_error.log;\n\n`
343
+
344
+ // Client settings
345
+ config += ` # Client settings\n`
346
+ config += ` client_max_body_size 100M;\n\n`
347
+
348
+ // Location blocks
349
+ config += ` # Service locations\n`
350
+ config += locations.join('\n') + '\n\n'
351
+
352
+ // Health check endpoints
353
+ if (healthChecks.length > 0) {
354
+ config += ` # Health check endpoints\n`
355
+ config += healthChecks.join('\n') + '\n'
356
+ }
357
+
358
+ config += `}\n`
359
+
360
+ return config
361
+ }
362
+
363
+ /**
364
+ * Check if an Nginx config file exists
365
+ */
366
+ export async function configExists(configName: string, configPath: string): Promise<boolean> {
367
+ const configFilePath = path.join(configPath, `${configName}.conf`)
368
+ return await fs.pathExists(configFilePath)
369
+ }
370
+
371
+ /**
372
+ * Write Nginx configuration file, deleting existing file if it exists and creating new one
373
+ */
374
+ export async function writeNginxConfig(
375
+ configName: string,
376
+ configPath: string,
377
+ configContent: string
378
+ ): Promise<boolean> {
379
+ const configFilePath = path.join(configPath, `${configName}.conf`)
380
+ const exists = await fs.pathExists(configFilePath)
381
+
382
+ // If config file exists, delete it first
383
+ if (exists) {
384
+ await fs.remove(configFilePath)
385
+ }
386
+
387
+ // Create new config file
388
+ await fs.writeFile(configFilePath, configContent)
389
+
390
+ return exists // Return true if file existed (was deleted and recreated)
391
+ }
392
+
393
+ /**
394
+ * Enable an Nginx site by creating a symbolic link
395
+ */
396
+ export async function enableSite(siteName: string, configPath: string): Promise<void> {
397
+ const availablePath = path.join(configPath, `${siteName}.conf`)
398
+ const enabledPath = availablePath.replace('sites-available', 'sites-enabled')
399
+
400
+ // Create sites-enabled directory if it doesn't exist
401
+ await fs.ensureDir(path.dirname(enabledPath))
402
+
403
+ // Remove existing symlink if present
404
+ if (await fs.pathExists(enabledPath)) {
405
+ await fs.remove(enabledPath)
406
+ }
407
+
408
+ // Create symlink
409
+ await execa('sudo', ['ln', '-sf', availablePath, enabledPath])
410
+ }
411
+
412
+ /**
413
+ * Test and reload Nginx configuration
414
+ */
415
+ export async function reloadNginx(reloadCommand: string): Promise<void> {
416
+ try {
417
+ // Test configuration first
418
+ await execa('sudo', ['nginx', '-t'])
419
+
420
+ // Reload Nginx
421
+ const parts = reloadCommand.split(' ')
422
+ if (parts.length > 0) {
423
+ // Simple execution of provided command
424
+ await execa(parts[0], parts.slice(1), { shell: true })
425
+ }
426
+ } catch (error) {
427
+ throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Disable an Nginx site
433
+ */
434
+ export async function disableSite(siteName: string, configPath: string): Promise<void> {
435
+ const enabledPath = path.join(
436
+ configPath.replace('sites-available', 'sites-enabled'),
437
+ `${siteName}.conf`
438
+ )
439
+
440
+ if (await fs.pathExists(enabledPath)) {
441
+ await fs.remove(enabledPath)
442
+ }
443
+ }
Binary file
@@ -0,0 +1,98 @@
1
+ project:
2
+ name: my-app
3
+ version: 1.0.0
4
+
5
+ services:
6
+ # Example 1: Simple Node.js service
7
+ - name: api
8
+ port: 3000
9
+ domains:
10
+ - api.example.com
11
+ - www.api.example.com
12
+ healthCheck:
13
+ path: /health
14
+ interval: 30
15
+ environment:
16
+ NODE_ENV: production
17
+ PORT: 3000
18
+
19
+ # Example 2: Docker container service
20
+ - name: webapp
21
+ port: 8080
22
+ docker:
23
+ image: nginx:latest
24
+ container: webapp-container
25
+ port: 80
26
+ domains:
27
+ - example.com
28
+ - www.example.com
29
+ healthCheck:
30
+ path: /
31
+ interval: 30
32
+
33
+ # Example 3: Multiple subdomains with Docker
34
+ - name: dashboard
35
+ port: 5000
36
+ docker:
37
+ image: myapp/dashboard:latest
38
+ container: dashboard-container
39
+ port: 5000
40
+ domains:
41
+ - dashboard.example.com
42
+ - admin.example.com
43
+ healthCheck:
44
+ path: /api/health
45
+ interval: 60
46
+ environment:
47
+ DATABASE_URL: postgresql://localhost:5432/dashboard
48
+ REDIS_URL: redis://localhost:6379
49
+
50
+ # Example 4: Service connecting to existing Docker container
51
+ - name: database-proxy
52
+ port: 5432
53
+ docker:
54
+ container: postgres-container
55
+ port: 5432
56
+ domains:
57
+ - db.example.com
58
+
59
+ # Example 5: Multiple services on the same domain with path-based routing
60
+ # API service on /api path
61
+ - name: api
62
+ port: 3001
63
+ path: /api
64
+ domains:
65
+ - muacle.com
66
+ docker:
67
+ image: myapp/api:latest
68
+ container: api-container
69
+ port: 3001
70
+ healthCheck:
71
+ path: /health
72
+ interval: 30
73
+
74
+ # UI service on root path
75
+ - name: ui
76
+ port: 3000
77
+ path: /
78
+ domains:
79
+ - muacle.com
80
+ docker:
81
+ image: myapp/ui:latest
82
+ container: ui-container
83
+ port: 3000
84
+ healthCheck:
85
+ path: /
86
+ interval: 30
87
+
88
+ nginx:
89
+ configPath: /etc/nginx/sites-available
90
+ reloadCommand: sudo nginx -t && sudo systemctl reload nginx
91
+
92
+ certbot:
93
+ email: admin@example.com
94
+ staging: false # Set to true for testing
95
+
96
+ deployment:
97
+ strategy: rolling # Options: rolling, blue-green
98
+ healthCheckTimeout: 30000 # milliseconds