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
@@ -0,0 +1,326 @@
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
+ * Generate Nginx server block configuration for a service
8
+ */
9
+ export function generateNginxConfig(
10
+ service: ServiceConfig,
11
+ withHttps: boolean,
12
+ portOverride?: number
13
+ ): string {
14
+ const serverNames = service.domains.join(' ')
15
+ // Use primary domain for upstream naming to ensure uniqueness
16
+ const primaryDomain = service.domains[0]
17
+ const domainSafe = primaryDomain.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
18
+ const upstreamName = `${domainSafe}_${service.name}`
19
+ const servicePath = service.path || '/'
20
+ const port = portOverride || service.port
21
+
22
+ let config = `# Nginx configuration for ${service.name}\n\n`
23
+
24
+ // Upstream configuration
25
+ config += `upstream ${upstreamName} {\n`
26
+ config += ` server localhost:${port} max_fails=3 fail_timeout=30s;\n`
27
+ config += ` keepalive 32;\n`
28
+ config += `}\n\n`
29
+
30
+ if (withHttps) {
31
+ // HTTP server - redirect to HTTPS
32
+ config += `server {\n`
33
+ config += ` listen 80;\n`
34
+ config += ` listen [::]:80;\n`
35
+ config += ` server_name ${serverNames};\n\n`
36
+ config += ` # Redirect all HTTP to HTTPS\n`
37
+ config += ` return 301 https://$server_name$request_uri;\n`
38
+ config += `}\n\n`
39
+
40
+ // HTTPS server
41
+ config += `server {\n`
42
+ config += ` listen 443 ssl http2;\n`
43
+ config += ` listen [::]:443 ssl http2;\n`
44
+ config += ` server_name ${serverNames};\n\n`
45
+
46
+ // SSL configuration
47
+ const primaryDomain = service.domains[0]
48
+ config += ` # SSL Configuration\n`
49
+ config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\n`
50
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\n`
51
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
52
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
53
+ config += ` ssl_prefer_server_ciphers on;\n\n`
54
+ } else {
55
+ // HTTP only server
56
+ config += `server {\n`
57
+ config += ` listen 80;\n`
58
+ config += ` listen [::]:80;\n`
59
+ config += ` server_name ${serverNames};\n\n`
60
+ }
61
+
62
+ // Logging
63
+ config += ` # Logging\n`
64
+ config += ` access_log /var/log/nginx/${service.name}_access.log;\n`
65
+ config += ` error_log /var/log/nginx/${service.name}_error.log;\n\n`
66
+
67
+ // Client settings
68
+ config += ` # Client settings\n`
69
+ config += ` client_max_body_size 100M;\n\n`
70
+
71
+ // Proxy settings
72
+ config += ` # Proxy settings\n`
73
+ config += ` location ${servicePath} {\n`
74
+ config += ` proxy_pass http://${upstreamName};\n`
75
+ config += ` proxy_http_version 1.1;\n`
76
+ config += ` proxy_set_header Upgrade $http_upgrade;\n`
77
+ config += ` proxy_set_header Connection 'upgrade';\n`
78
+ config += ` proxy_set_header Host $host;\n`
79
+ config += ` proxy_set_header X-Real-IP $remote_addr;\n`
80
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`
81
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`
82
+ config += ` proxy_cache_bypass $http_upgrade;\n`
83
+ config += ` proxy_connect_timeout 60s;\n`
84
+ config += ` proxy_send_timeout 60s;\n`
85
+ config += ` proxy_read_timeout 60s;\n`
86
+ config += ` }\n`
87
+
88
+ // Health check endpoint (if configured)
89
+ if (service.healthCheck) {
90
+ config += `\n # Health check endpoint\n`
91
+ config += ` location ${service.healthCheck.path} {\n`
92
+ config += ` proxy_pass http://${upstreamName};\n`
93
+ config += ` access_log off;\n`
94
+ config += ` }\n`
95
+ }
96
+
97
+ config += `}\n`
98
+
99
+ return config
100
+ }
101
+
102
+ /**
103
+ * Generate Nginx configuration for multiple services on the same domain
104
+ * Groups services by domain and creates location blocks for each service path
105
+ */
106
+ export function generateMultiServiceNginxConfig(
107
+ services: ServiceConfig[],
108
+ domain: string,
109
+ withHttps: boolean,
110
+ portOverrides?: Map<string, number>
111
+ ): string {
112
+ const upstreams: string[] = []
113
+ const locations: string[] = []
114
+ const healthChecks: string[] = []
115
+
116
+ // Sort services by path length (longest first) to ensure specific paths are matched before general ones
117
+ const sortedServices = [...services].sort((a, b) => {
118
+ const pathA = (a.path || '/').length
119
+ const pathB = (b.path || '/').length
120
+ return pathB - pathA
121
+ })
122
+
123
+ // Track upstream names to ensure uniqueness within the same domain config
124
+ const usedUpstreamNames = new Set<string>()
125
+
126
+ for (const service of sortedServices) {
127
+ // Create unique upstream name: domain_service_name to avoid conflicts
128
+ // Replace dots and special chars in domain for valid nginx upstream name
129
+ const domainSafe = domain.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')
130
+ let upstreamName = `${domainSafe}_${service.name}`
131
+
132
+ // Ensure uniqueness (in case same service name appears multiple times)
133
+ let counter = 1
134
+ const originalUpstreamName = upstreamName
135
+ while (usedUpstreamNames.has(upstreamName)) {
136
+ upstreamName = `${originalUpstreamName}_${counter}`
137
+ counter++
138
+ }
139
+ usedUpstreamNames.add(upstreamName)
140
+
141
+ const servicePath = service.path || '/'
142
+ const port = portOverrides?.get(service.name) || service.port
143
+
144
+ // Generate upstream for each service on the same domain
145
+ upstreams.push(`upstream ${upstreamName} {`)
146
+ upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`)
147
+ upstreams.push(` keepalive 32;`)
148
+ upstreams.push(`}`)
149
+
150
+ // Generate location block
151
+ if (servicePath === '/') {
152
+ // Root path - use exact match or default
153
+ locations.push(` # Service: ${service.name}`)
154
+ locations.push(` location / {`)
155
+ } else {
156
+ // Specific path - use prefix match
157
+ locations.push(` # Service: ${service.name}`)
158
+ locations.push(` location ${servicePath} {`)
159
+ }
160
+
161
+ locations.push(` proxy_pass http://${upstreamName};`)
162
+ locations.push(` proxy_http_version 1.1;`)
163
+ locations.push(` proxy_set_header Upgrade $http_upgrade;`)
164
+ locations.push(` proxy_set_header Connection 'upgrade';`)
165
+ locations.push(` proxy_set_header Host $host;`)
166
+ locations.push(` proxy_set_header X-Real-IP $remote_addr;`)
167
+ locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
168
+ locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
169
+ locations.push(` proxy_cache_bypass $http_upgrade;`)
170
+ locations.push(` proxy_connect_timeout 60s;`)
171
+ locations.push(` proxy_send_timeout 60s;`)
172
+ locations.push(` proxy_read_timeout 60s;`)
173
+ locations.push(` }`)
174
+
175
+ // Health check endpoint
176
+ if (service.healthCheck) {
177
+ healthChecks.push(` # Health check for ${service.name}`)
178
+ healthChecks.push(` location ${service.healthCheck.path} {`)
179
+ healthChecks.push(` proxy_pass http://${upstreamName};`)
180
+ healthChecks.push(` access_log off;`)
181
+ healthChecks.push(` }`)
182
+ }
183
+ }
184
+
185
+ let config = `# Nginx configuration for ${domain}\n`
186
+ config += `# Multiple services on the same domain\n\n`
187
+
188
+ // Add upstreams
189
+ config += upstreams.join('\n') + '\n\n'
190
+
191
+ if (withHttps) {
192
+ // HTTP server - redirect to HTTPS
193
+ config += `server {\n`
194
+ config += ` listen 80;\n`
195
+ config += ` listen [::]:80;\n`
196
+ config += ` server_name ${domain};\n\n`
197
+ config += ` # Redirect all HTTP to HTTPS\n`
198
+ config += ` return 301 https://$server_name$request_uri;\n`
199
+ config += `}\n\n`
200
+
201
+ // HTTPS server
202
+ config += `server {\n`
203
+ config += ` listen 443 ssl http2;\n`
204
+ config += ` listen [::]:443 ssl http2;\n`
205
+ config += ` server_name ${domain};\n\n`
206
+
207
+ // SSL configuration
208
+ config += ` # SSL Configuration\n`
209
+ config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;\n`
210
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;\n`
211
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`
212
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;\n`
213
+ config += ` ssl_prefer_server_ciphers on;\n\n`
214
+ } else {
215
+ // HTTP only server
216
+ config += `server {\n`
217
+ config += ` listen 80;\n`
218
+ config += ` listen [::]:80;\n`
219
+ config += ` server_name ${domain};\n\n`
220
+ }
221
+
222
+ // Logging
223
+ config += ` # Logging\n`
224
+ config += ` access_log /var/log/nginx/${domain}_access.log;\n`
225
+ config += ` error_log /var/log/nginx/${domain}_error.log;\n\n`
226
+
227
+ // Client settings
228
+ config += ` # Client settings\n`
229
+ config += ` client_max_body_size 100M;\n\n`
230
+
231
+ // Location blocks
232
+ config += ` # Service locations\n`
233
+ config += locations.join('\n') + '\n\n'
234
+
235
+ // Health check endpoints
236
+ if (healthChecks.length > 0) {
237
+ config += ` # Health check endpoints\n`
238
+ config += healthChecks.join('\n') + '\n'
239
+ }
240
+
241
+ config += `}\n`
242
+
243
+ return config
244
+ }
245
+
246
+ /**
247
+ * Check if an Nginx config file exists
248
+ */
249
+ export async function configExists(configName: string, configPath: string): Promise<boolean> {
250
+ const configFilePath = path.join(configPath, `${configName}.conf`)
251
+ return await fs.pathExists(configFilePath)
252
+ }
253
+
254
+ /**
255
+ * Write Nginx configuration file, deleting existing file if it exists and creating new one
256
+ */
257
+ export async function writeNginxConfig(
258
+ configName: string,
259
+ configPath: string,
260
+ configContent: string
261
+ ): Promise<boolean> {
262
+ const configFilePath = path.join(configPath, `${configName}.conf`)
263
+ const exists = await fs.pathExists(configFilePath)
264
+
265
+ // If config file exists, delete it first
266
+ if (exists) {
267
+ await fs.remove(configFilePath)
268
+ }
269
+
270
+ // Create new config file
271
+ await fs.writeFile(configFilePath, configContent)
272
+
273
+ return exists // Return true if file existed (was deleted and recreated)
274
+ }
275
+
276
+ /**
277
+ * Enable an Nginx site by creating a symbolic link
278
+ */
279
+ export async function enableSite(siteName: string, configPath: string): Promise<void> {
280
+ const availablePath = path.join(configPath, `${siteName}.conf`)
281
+ const enabledPath = availablePath.replace('sites-available', 'sites-enabled')
282
+
283
+ // Create sites-enabled directory if it doesn't exist
284
+ await fs.ensureDir(path.dirname(enabledPath))
285
+
286
+ // Remove existing symlink if present
287
+ if (await fs.pathExists(enabledPath)) {
288
+ await fs.remove(enabledPath)
289
+ }
290
+
291
+ // Create symlink
292
+ await execa('sudo', ['ln', '-sf', availablePath, enabledPath])
293
+ }
294
+
295
+ /**
296
+ * Test and reload Nginx configuration
297
+ */
298
+ export async function reloadNginx(reloadCommand: string): Promise<void> {
299
+ try {
300
+ // Test configuration first
301
+ await execa('sudo', ['nginx', '-t'])
302
+
303
+ // Reload Nginx
304
+ const parts = reloadCommand.split(' ')
305
+ if (parts.length > 0) {
306
+ // Simple execution of provided command
307
+ await execa(parts[0], parts.slice(1), { shell: true })
308
+ }
309
+ } catch (error) {
310
+ throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Disable an Nginx site
316
+ */
317
+ export async function disableSite(siteName: string, configPath: string): Promise<void> {
318
+ const enabledPath = path.join(
319
+ configPath.replace('sites-available', 'sites-enabled'),
320
+ `${siteName}.conf`
321
+ )
322
+
323
+ if (await fs.pathExists(enabledPath)) {
324
+ await fs.remove(enabledPath)
325
+ }
326
+ }
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
package/test ADDED
File without changes
package/todo.md ADDED
@@ -0,0 +1,6 @@
1
+ build tool deploy and run project in yml service config service port to nginx multiple domain name or sub domain and can connect docker port
2
+ ✔ Automatic Nginx reverse proxy setup
3
+ ✔ Automatic HTTPS with Certbot
4
+ ✔ Zero-downtime deploy
5
+ make it in cli https://www.npmjs.com/package/commander typescript to build create project and setup by yml nginx
6
+ in save cost in run vm
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { resolve } from 'path'
2
+ import type { Plugin } from 'vite'
3
+ import { defineConfig } from 'vite'
4
+
5
+ const addShebangPlugin = (): Plugin => ({
6
+ name: 'add-shebang',
7
+ generateBundle(_options, bundle) {
8
+ if (bundle['index.js']) {
9
+ const chunk = bundle['index.js']
10
+ if (chunk.type === 'chunk') {
11
+ chunk.code = '#!/usr/bin/env node\n' + chunk.code
12
+ }
13
+ }
14
+ },
15
+ })
16
+
17
+ export default defineConfig({
18
+ plugins: [addShebangPlugin()],
19
+ build: {
20
+ outDir: 'dist',
21
+ lib: {
22
+ entry: resolve(__dirname, 'src/index.ts'),
23
+ formats: ['es'],
24
+ fileName: 'index',
25
+ },
26
+ rollupOptions: {
27
+ external: (id) => {
28
+ // Externalize all node_modules and Node.js built-ins
29
+ return (
30
+ !id.startsWith('.') &&
31
+ !id.startsWith('/') &&
32
+ !resolve(__dirname, id).startsWith(__dirname + '/src')
33
+ )
34
+ },
35
+ output: {
36
+ format: 'es',
37
+ entryFileNames: '[name].js',
38
+ preserveModules: true,
39
+ preserveModulesRoot: 'src',
40
+ },
41
+ },
42
+ target: 'node18',
43
+ minify: false,
44
+ sourcemap: true,
45
+ },
46
+ })