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,303 @@
1
+ import { execa } from "execa";
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ function generateNginxConfig(service, withHttps, portOverride) {
5
+ const serverNames = service.domains.join(" ");
6
+ const primaryDomain = service.domains[0];
7
+ const domainSafe = primaryDomain.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
8
+ const upstreamName = `${domainSafe}_${service.name}`;
9
+ const servicePath = service.path || "/";
10
+ const port = portOverride || service.port;
11
+ let config = `# Nginx configuration for ${service.name}
12
+
13
+ `;
14
+ config += `upstream ${upstreamName} {
15
+ `;
16
+ config += ` server localhost:${port} max_fails=3 fail_timeout=30s;
17
+ `;
18
+ config += ` keepalive 32;
19
+ `;
20
+ config += `}
21
+
22
+ `;
23
+ if (withHttps) {
24
+ config += `server {
25
+ `;
26
+ config += ` listen 80;
27
+ `;
28
+ config += ` listen [::]:80;
29
+ `;
30
+ config += ` server_name ${serverNames};
31
+
32
+ `;
33
+ config += ` # Redirect all HTTP to HTTPS
34
+ `;
35
+ config += ` return 301 https://$server_name$request_uri;
36
+ `;
37
+ config += `}
38
+
39
+ `;
40
+ config += `server {
41
+ `;
42
+ config += ` listen 443 ssl http2;
43
+ `;
44
+ config += ` listen [::]:443 ssl http2;
45
+ `;
46
+ config += ` server_name ${serverNames};
47
+
48
+ `;
49
+ const primaryDomain2 = service.domains[0];
50
+ config += ` # SSL Configuration
51
+ `;
52
+ config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain2}/fullchain.pem;
53
+ `;
54
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain2}/privkey.pem;
55
+ `;
56
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;
57
+ `;
58
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;
59
+ `;
60
+ config += ` ssl_prefer_server_ciphers on;
61
+
62
+ `;
63
+ } else {
64
+ config += `server {
65
+ `;
66
+ config += ` listen 80;
67
+ `;
68
+ config += ` listen [::]:80;
69
+ `;
70
+ config += ` server_name ${serverNames};
71
+
72
+ `;
73
+ }
74
+ config += ` # Logging
75
+ `;
76
+ config += ` access_log /var/log/nginx/${service.name}_access.log;
77
+ `;
78
+ config += ` error_log /var/log/nginx/${service.name}_error.log;
79
+
80
+ `;
81
+ config += ` # Client settings
82
+ `;
83
+ config += ` client_max_body_size 100M;
84
+
85
+ `;
86
+ config += ` # Proxy settings
87
+ `;
88
+ config += ` location ${servicePath} {
89
+ `;
90
+ config += ` proxy_pass http://${upstreamName};
91
+ `;
92
+ config += ` proxy_http_version 1.1;
93
+ `;
94
+ config += ` proxy_set_header Upgrade $http_upgrade;
95
+ `;
96
+ config += ` proxy_set_header Connection 'upgrade';
97
+ `;
98
+ config += ` proxy_set_header Host $host;
99
+ `;
100
+ config += ` proxy_set_header X-Real-IP $remote_addr;
101
+ `;
102
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
103
+ `;
104
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;
105
+ `;
106
+ config += ` proxy_cache_bypass $http_upgrade;
107
+ `;
108
+ config += ` proxy_connect_timeout 60s;
109
+ `;
110
+ config += ` proxy_send_timeout 60s;
111
+ `;
112
+ config += ` proxy_read_timeout 60s;
113
+ `;
114
+ config += ` }
115
+ `;
116
+ if (service.healthCheck) {
117
+ config += `
118
+ # Health check endpoint
119
+ `;
120
+ config += ` location ${service.healthCheck.path} {
121
+ `;
122
+ config += ` proxy_pass http://${upstreamName};
123
+ `;
124
+ config += ` access_log off;
125
+ `;
126
+ config += ` }
127
+ `;
128
+ }
129
+ config += `}
130
+ `;
131
+ return config;
132
+ }
133
+ function generateMultiServiceNginxConfig(services, domain, withHttps, portOverrides) {
134
+ const upstreams = [];
135
+ const locations = [];
136
+ const healthChecks = [];
137
+ const sortedServices = [...services].sort((a, b) => {
138
+ const pathA = (a.path || "/").length;
139
+ const pathB = (b.path || "/").length;
140
+ return pathB - pathA;
141
+ });
142
+ const usedUpstreamNames = /* @__PURE__ */ new Set();
143
+ for (const service of sortedServices) {
144
+ const domainSafe = domain.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
145
+ let upstreamName = `${domainSafe}_${service.name}`;
146
+ let counter = 1;
147
+ const originalUpstreamName = upstreamName;
148
+ while (usedUpstreamNames.has(upstreamName)) {
149
+ upstreamName = `${originalUpstreamName}_${counter}`;
150
+ counter++;
151
+ }
152
+ usedUpstreamNames.add(upstreamName);
153
+ const servicePath = service.path || "/";
154
+ const port = portOverrides?.get(service.name) || service.port;
155
+ upstreams.push(`upstream ${upstreamName} {`);
156
+ upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`);
157
+ upstreams.push(` keepalive 32;`);
158
+ upstreams.push(`}`);
159
+ if (servicePath === "/") {
160
+ locations.push(` # Service: ${service.name}`);
161
+ locations.push(` location / {`);
162
+ } else {
163
+ locations.push(` # Service: ${service.name}`);
164
+ locations.push(` location ${servicePath} {`);
165
+ }
166
+ locations.push(` proxy_pass http://${upstreamName};`);
167
+ locations.push(` proxy_http_version 1.1;`);
168
+ locations.push(` proxy_set_header Upgrade $http_upgrade;`);
169
+ locations.push(` proxy_set_header Connection 'upgrade';`);
170
+ locations.push(` proxy_set_header Host $host;`);
171
+ locations.push(` proxy_set_header X-Real-IP $remote_addr;`);
172
+ locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
173
+ locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`);
174
+ locations.push(` proxy_cache_bypass $http_upgrade;`);
175
+ locations.push(` proxy_connect_timeout 60s;`);
176
+ locations.push(` proxy_send_timeout 60s;`);
177
+ locations.push(` proxy_read_timeout 60s;`);
178
+ locations.push(` }`);
179
+ if (service.healthCheck) {
180
+ healthChecks.push(` # Health check for ${service.name}`);
181
+ healthChecks.push(` location ${service.healthCheck.path} {`);
182
+ healthChecks.push(` proxy_pass http://${upstreamName};`);
183
+ healthChecks.push(` access_log off;`);
184
+ healthChecks.push(` }`);
185
+ }
186
+ }
187
+ let config = `# Nginx configuration for ${domain}
188
+ `;
189
+ config += `# Multiple services on the same domain
190
+
191
+ `;
192
+ config += upstreams.join("\n") + "\n\n";
193
+ if (withHttps) {
194
+ config += `server {
195
+ `;
196
+ config += ` listen 80;
197
+ `;
198
+ config += ` listen [::]:80;
199
+ `;
200
+ config += ` server_name ${domain};
201
+
202
+ `;
203
+ config += ` # Redirect all HTTP to HTTPS
204
+ `;
205
+ config += ` return 301 https://$server_name$request_uri;
206
+ `;
207
+ config += `}
208
+
209
+ `;
210
+ config += `server {
211
+ `;
212
+ config += ` listen 443 ssl http2;
213
+ `;
214
+ config += ` listen [::]:443 ssl http2;
215
+ `;
216
+ config += ` server_name ${domain};
217
+
218
+ `;
219
+ config += ` # SSL Configuration
220
+ `;
221
+ config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
222
+ `;
223
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
224
+ `;
225
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;
226
+ `;
227
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;
228
+ `;
229
+ config += ` ssl_prefer_server_ciphers on;
230
+
231
+ `;
232
+ } else {
233
+ config += `server {
234
+ `;
235
+ config += ` listen 80;
236
+ `;
237
+ config += ` listen [::]:80;
238
+ `;
239
+ config += ` server_name ${domain};
240
+
241
+ `;
242
+ }
243
+ config += ` # Logging
244
+ `;
245
+ config += ` access_log /var/log/nginx/${domain}_access.log;
246
+ `;
247
+ config += ` error_log /var/log/nginx/${domain}_error.log;
248
+
249
+ `;
250
+ config += ` # Client settings
251
+ `;
252
+ config += ` client_max_body_size 100M;
253
+
254
+ `;
255
+ config += ` # Service locations
256
+ `;
257
+ config += locations.join("\n") + "\n\n";
258
+ if (healthChecks.length > 0) {
259
+ config += ` # Health check endpoints
260
+ `;
261
+ config += healthChecks.join("\n") + "\n";
262
+ }
263
+ config += `}
264
+ `;
265
+ return config;
266
+ }
267
+ async function writeNginxConfig(configName, configPath, configContent) {
268
+ const configFilePath = path.join(configPath, `${configName}.conf`);
269
+ const exists = await fs.pathExists(configFilePath);
270
+ if (exists) {
271
+ await fs.remove(configFilePath);
272
+ }
273
+ await fs.writeFile(configFilePath, configContent);
274
+ return exists;
275
+ }
276
+ async function enableSite(siteName, configPath) {
277
+ const availablePath = path.join(configPath, `${siteName}.conf`);
278
+ const enabledPath = availablePath.replace("sites-available", "sites-enabled");
279
+ await fs.ensureDir(path.dirname(enabledPath));
280
+ if (await fs.pathExists(enabledPath)) {
281
+ await fs.remove(enabledPath);
282
+ }
283
+ await execa("sudo", ["ln", "-sf", availablePath, enabledPath]);
284
+ }
285
+ async function reloadNginx(reloadCommand) {
286
+ try {
287
+ await execa("sudo", ["nginx", "-t"]);
288
+ const parts = reloadCommand.split(" ");
289
+ if (parts.length > 0) {
290
+ await execa(parts[0], parts.slice(1), { shell: true });
291
+ }
292
+ } catch (error) {
293
+ throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`);
294
+ }
295
+ }
296
+ export {
297
+ enableSite,
298
+ generateMultiServiceNginxConfig,
299
+ generateNginxConfig,
300
+ reloadNginx,
301
+ writeNginxConfig
302
+ };
303
+ //# sourceMappingURL=nginx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nginx.js","sources":["../../src/utils/nginx.ts"],"sourcesContent":["import { execa } from 'execa'\nimport fs from 'fs-extra'\nimport path from 'path'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Generate Nginx server block configuration for a service\n */\nexport function generateNginxConfig(\n service: ServiceConfig,\n withHttps: boolean,\n portOverride?: number\n): string {\n const serverNames = service.domains.join(' ')\n // Use primary domain for upstream naming to ensure uniqueness\n const primaryDomain = service.domains[0]\n const domainSafe = primaryDomain.replace(/\\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')\n const upstreamName = `${domainSafe}_${service.name}`\n const servicePath = service.path || '/'\n const port = portOverride || service.port\n\n let config = `# Nginx configuration for ${service.name}\\n\\n`\n\n // Upstream configuration\n config += `upstream ${upstreamName} {\\n`\n config += ` server localhost:${port} max_fails=3 fail_timeout=30s;\\n`\n config += ` keepalive 32;\\n`\n config += `}\\n\\n`\n\n if (withHttps) {\n // HTTP server - redirect to HTTPS\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n config += ` # Redirect all HTTP to HTTPS\\n`\n config += ` return 301 https://$server_name$request_uri;\\n`\n config += `}\\n\\n`\n\n // HTTPS server\n config += `server {\\n`\n config += ` listen 443 ssl http2;\\n`\n config += ` listen [::]:443 ssl http2;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n\n // SSL configuration\n const primaryDomain = service.domains[0]\n config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\\n`\n config += ` ssl_protocols TLSv1.2 TLSv1.3;\\n`\n config += ` ssl_ciphers HIGH:!aNULL:!MD5;\\n`\n config += ` ssl_prefer_server_ciphers on;\\n\\n`\n } else {\n // HTTP only server\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n }\n\n // Logging\n config += ` # Logging\\n`\n config += ` access_log /var/log/nginx/${service.name}_access.log;\\n`\n config += ` error_log /var/log/nginx/${service.name}_error.log;\\n\\n`\n\n // Client settings\n config += ` # Client settings\\n`\n config += ` client_max_body_size 100M;\\n\\n`\n\n // Proxy settings\n config += ` # Proxy settings\\n`\n config += ` location ${servicePath} {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` proxy_http_version 1.1;\\n`\n config += ` proxy_set_header Upgrade $http_upgrade;\\n`\n config += ` proxy_set_header Connection 'upgrade';\\n`\n config += ` proxy_set_header Host $host;\\n`\n config += ` proxy_set_header X-Real-IP $remote_addr;\\n`\n config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\\n`\n config += ` proxy_set_header X-Forwarded-Proto $scheme;\\n`\n config += ` proxy_cache_bypass $http_upgrade;\\n`\n config += ` proxy_connect_timeout 60s;\\n`\n config += ` proxy_send_timeout 60s;\\n`\n config += ` proxy_read_timeout 60s;\\n`\n config += ` }\\n`\n\n // Health check endpoint (if configured)\n if (service.healthCheck) {\n config += `\\n # Health check endpoint\\n`\n config += ` location ${service.healthCheck.path} {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` access_log off;\\n`\n config += ` }\\n`\n }\n\n config += `}\\n`\n\n return config\n}\n\n/**\n * Generate Nginx configuration for multiple services on the same domain\n * Groups services by domain and creates location blocks for each service path\n */\nexport function generateMultiServiceNginxConfig(\n services: ServiceConfig[],\n domain: string,\n withHttps: boolean,\n portOverrides?: Map<string, number>\n): string {\n const upstreams: string[] = []\n const locations: string[] = []\n const healthChecks: string[] = []\n\n // Sort services by path length (longest first) to ensure specific paths are matched before general ones\n const sortedServices = [...services].sort((a, b) => {\n const pathA = (a.path || '/').length\n const pathB = (b.path || '/').length\n return pathB - pathA\n })\n\n // Track upstream names to ensure uniqueness within the same domain config\n const usedUpstreamNames = new Set<string>()\n\n for (const service of sortedServices) {\n // Create unique upstream name: domain_service_name to avoid conflicts\n // Replace dots and special chars in domain for valid nginx upstream name\n const domainSafe = domain.replace(/\\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')\n let upstreamName = `${domainSafe}_${service.name}`\n\n // Ensure uniqueness (in case same service name appears multiple times)\n let counter = 1\n const originalUpstreamName = upstreamName\n while (usedUpstreamNames.has(upstreamName)) {\n upstreamName = `${originalUpstreamName}_${counter}`\n counter++\n }\n usedUpstreamNames.add(upstreamName)\n\n const servicePath = service.path || '/'\n const port = portOverrides?.get(service.name) || service.port\n\n // Generate upstream for each service on the same domain\n upstreams.push(`upstream ${upstreamName} {`)\n upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`)\n upstreams.push(` keepalive 32;`)\n upstreams.push(`}`)\n\n // Generate location block\n if (servicePath === '/') {\n // Root path - use exact match or default\n locations.push(` # Service: ${service.name}`)\n locations.push(` location / {`)\n } else {\n // Specific path - use prefix match\n locations.push(` # Service: ${service.name}`)\n locations.push(` location ${servicePath} {`)\n }\n\n locations.push(` proxy_pass http://${upstreamName};`)\n locations.push(` proxy_http_version 1.1;`)\n locations.push(` proxy_set_header Upgrade $http_upgrade;`)\n locations.push(` proxy_set_header Connection 'upgrade';`)\n locations.push(` proxy_set_header Host $host;`)\n locations.push(` proxy_set_header X-Real-IP $remote_addr;`)\n locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)\n locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`)\n locations.push(` proxy_cache_bypass $http_upgrade;`)\n locations.push(` proxy_connect_timeout 60s;`)\n locations.push(` proxy_send_timeout 60s;`)\n locations.push(` proxy_read_timeout 60s;`)\n locations.push(` }`)\n\n // Health check endpoint\n if (service.healthCheck) {\n healthChecks.push(` # Health check for ${service.name}`)\n healthChecks.push(` location ${service.healthCheck.path} {`)\n healthChecks.push(` proxy_pass http://${upstreamName};`)\n healthChecks.push(` access_log off;`)\n healthChecks.push(` }`)\n }\n }\n\n let config = `# Nginx configuration for ${domain}\\n`\n config += `# Multiple services on the same domain\\n\\n`\n\n // Add upstreams\n config += upstreams.join('\\n') + '\\n\\n'\n\n if (withHttps) {\n // HTTP server - redirect to HTTPS\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${domain};\\n\\n`\n config += ` # Redirect all HTTP to HTTPS\\n`\n config += ` return 301 https://$server_name$request_uri;\\n`\n config += `}\\n\\n`\n\n // HTTPS server\n config += `server {\\n`\n config += ` listen 443 ssl http2;\\n`\n config += ` listen [::]:443 ssl http2;\\n`\n config += ` server_name ${domain};\\n\\n`\n\n // SSL configuration\n config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;\\n`\n config += ` ssl_protocols TLSv1.2 TLSv1.3;\\n`\n config += ` ssl_ciphers HIGH:!aNULL:!MD5;\\n`\n config += ` ssl_prefer_server_ciphers on;\\n\\n`\n } else {\n // HTTP only server\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${domain};\\n\\n`\n }\n\n // Logging\n config += ` # Logging\\n`\n config += ` access_log /var/log/nginx/${domain}_access.log;\\n`\n config += ` error_log /var/log/nginx/${domain}_error.log;\\n\\n`\n\n // Client settings\n config += ` # Client settings\\n`\n config += ` client_max_body_size 100M;\\n\\n`\n\n // Location blocks\n config += ` # Service locations\\n`\n config += locations.join('\\n') + '\\n\\n'\n\n // Health check endpoints\n if (healthChecks.length > 0) {\n config += ` # Health check endpoints\\n`\n config += healthChecks.join('\\n') + '\\n'\n }\n\n config += `}\\n`\n\n return config\n}\n\n/**\n * Check if an Nginx config file exists\n */\nexport async function configExists(configName: string, configPath: string): Promise<boolean> {\n const configFilePath = path.join(configPath, `${configName}.conf`)\n return await fs.pathExists(configFilePath)\n}\n\n/**\n * Write Nginx configuration file, deleting existing file if it exists and creating new one\n */\nexport async function writeNginxConfig(\n configName: string,\n configPath: string,\n configContent: string\n): Promise<boolean> {\n const configFilePath = path.join(configPath, `${configName}.conf`)\n const exists = await fs.pathExists(configFilePath)\n\n // If config file exists, delete it first\n if (exists) {\n await fs.remove(configFilePath)\n }\n\n // Create new config file\n await fs.writeFile(configFilePath, configContent)\n\n return exists // Return true if file existed (was deleted and recreated)\n}\n\n/**\n * Enable an Nginx site by creating a symbolic link\n */\nexport async function enableSite(siteName: string, configPath: string): Promise<void> {\n const availablePath = path.join(configPath, `${siteName}.conf`)\n const enabledPath = availablePath.replace('sites-available', 'sites-enabled')\n\n // Create sites-enabled directory if it doesn't exist\n await fs.ensureDir(path.dirname(enabledPath))\n\n // Remove existing symlink if present\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n\n // Create symlink\n await execa('sudo', ['ln', '-sf', availablePath, enabledPath])\n}\n\n/**\n * Test and reload Nginx configuration\n */\nexport async function reloadNginx(reloadCommand: string): Promise<void> {\n try {\n // Test configuration first\n await execa('sudo', ['nginx', '-t'])\n\n // Reload Nginx\n const parts = reloadCommand.split(' ')\n if (parts.length > 0) {\n // Simple execution of provided command\n await execa(parts[0], parts.slice(1), { shell: true })\n }\n } catch (error) {\n throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)\n }\n}\n\n/**\n * Disable an Nginx site\n */\nexport async function disableSite(siteName: string, configPath: string): Promise<void> {\n const enabledPath = path.join(\n configPath.replace('sites-available', 'sites-enabled'),\n `${siteName}.conf`\n )\n\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n}\n"],"names":["primaryDomain"],"mappings":";;;AAQO,SAAS,oBACd,SACA,WACA,cACQ;AACR,QAAM,cAAc,QAAQ,QAAQ,KAAK,GAAG;AAE5C,QAAM,gBAAgB,QAAQ,QAAQ,CAAC;AACvC,QAAM,aAAa,cAAc,QAAQ,OAAO,GAAG,EAAE,QAAQ,kBAAkB,GAAG;AAClF,QAAM,eAAe,GAAG,UAAU,IAAI,QAAQ,IAAI;AAClD,QAAM,cAAc,QAAQ,QAAQ;AACpC,QAAM,OAAO,gBAAgB,QAAQ;AAErC,MAAI,SAAS,6BAA6B,QAAQ,IAAI;AAAA;AAAA;AAGtD,YAAU,YAAY,YAAY;AAAA;AAClC,YAAU,wBAAwB,IAAI;AAAA;AACtC,YAAU;AAAA;AACV,YAAU;AAAA;AAAA;AAEV,MAAI,WAAW;AAEb,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AACxC,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAGV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AAGxC,UAAMA,iBAAgB,QAAQ,QAAQ,CAAC;AACvC,cAAU;AAAA;AACV,cAAU,6CAA6CA,cAAa;AAAA;AACpE,cAAU,iDAAiDA,cAAa;AAAA;AACxE,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAAA,EACZ,OAAO;AAEL,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AAAA,EAC1C;AAGA,YAAU;AAAA;AACV,YAAU,iCAAiC,QAAQ,IAAI;AAAA;AACvD,YAAU,gCAAgC,QAAQ,IAAI;AAAA;AAAA;AAGtD,YAAU;AAAA;AACV,YAAU;AAAA;AAAA;AAGV,YAAU;AAAA;AACV,YAAU,gBAAgB,WAAW;AAAA;AACrC,YAAU,6BAA6B,YAAY;AAAA;AACnD,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AAGV,MAAI,QAAQ,aAAa;AACvB,cAAU;AAAA;AAAA;AACV,cAAU,gBAAgB,QAAQ,YAAY,IAAI;AAAA;AAClD,cAAU,6BAA6B,YAAY;AAAA;AACnD,cAAU;AAAA;AACV,cAAU;AAAA;AAAA,EACZ;AAEA,YAAU;AAAA;AAEV,SAAO;AACT;AAMO,SAAS,gCACd,UACA,QACA,WACA,eACQ;AACR,QAAM,YAAsB,CAAA;AAC5B,QAAM,YAAsB,CAAA;AAC5B,QAAM,eAAyB,CAAA;AAG/B,QAAM,iBAAiB,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM;AAClD,UAAM,SAAS,EAAE,QAAQ,KAAK;AAC9B,UAAM,SAAS,EAAE,QAAQ,KAAK;AAC9B,WAAO,QAAQ;AAAA,EACjB,CAAC;AAGD,QAAM,wCAAwB,IAAA;AAE9B,aAAW,WAAW,gBAAgB;AAGpC,UAAM,aAAa,OAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,kBAAkB,GAAG;AAC3E,QAAI,eAAe,GAAG,UAAU,IAAI,QAAQ,IAAI;AAGhD,QAAI,UAAU;AACd,UAAM,uBAAuB;AAC7B,WAAO,kBAAkB,IAAI,YAAY,GAAG;AAC1C,qBAAe,GAAG,oBAAoB,IAAI,OAAO;AACjD;AAAA,IACF;AACA,sBAAkB,IAAI,YAAY;AAElC,UAAM,cAAc,QAAQ,QAAQ;AACpC,UAAM,OAAO,eAAe,IAAI,QAAQ,IAAI,KAAK,QAAQ;AAGzD,cAAU,KAAK,YAAY,YAAY,IAAI;AAC3C,cAAU,KAAK,wBAAwB,IAAI,gCAAgC;AAC3E,cAAU,KAAK,mBAAmB;AAClC,cAAU,KAAK,GAAG;AAGlB,QAAI,gBAAgB,KAAK;AAEvB,gBAAU,KAAK,kBAAkB,QAAQ,IAAI,EAAE;AAC/C,gBAAU,KAAK,kBAAkB;AAAA,IACnC,OAAO;AAEL,gBAAU,KAAK,kBAAkB,QAAQ,IAAI,EAAE;AAC/C,gBAAU,KAAK,gBAAgB,WAAW,IAAI;AAAA,IAChD;AAEA,cAAU,KAAK,6BAA6B,YAAY,GAAG;AAC3D,cAAU,KAAK,iCAAiC;AAChD,cAAU,KAAK,iDAAiD;AAChE,cAAU,KAAK,gDAAgD;AAC/D,cAAU,KAAK,sCAAsC;AACrD,cAAU,KAAK,kDAAkD;AACjE,cAAU,KAAK,sEAAsE;AACrF,cAAU,KAAK,qDAAqD;AACpE,cAAU,KAAK,2CAA2C;AAC1D,cAAU,KAAK,oCAAoC;AACnD,cAAU,KAAK,iCAAiC;AAChD,cAAU,KAAK,iCAAiC;AAChD,cAAU,KAAK,OAAO;AAGtB,QAAI,QAAQ,aAAa;AACvB,mBAAa,KAAK,0BAA0B,QAAQ,IAAI,EAAE;AAC1D,mBAAa,KAAK,gBAAgB,QAAQ,YAAY,IAAI,IAAI;AAC9D,mBAAa,KAAK,6BAA6B,YAAY,GAAG;AAC9D,mBAAa,KAAK,yBAAyB;AAC3C,mBAAa,KAAK,OAAO;AAAA,IAC3B;AAAA,EACF;AAEA,MAAI,SAAS,6BAA6B,MAAM;AAAA;AAChD,YAAU;AAAA;AAAA;AAGV,YAAU,UAAU,KAAK,IAAI,IAAI;AAEjC,MAAI,WAAW;AAEb,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,MAAM;AAAA;AAAA;AACnC,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAGV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,MAAM;AAAA;AAAA;AAGnC,cAAU;AAAA;AACV,cAAU,6CAA6C,MAAM;AAAA;AAC7D,cAAU,iDAAiD,MAAM;AAAA;AACjE,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAAA,EACZ,OAAO;AAEL,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,MAAM;AAAA;AAAA;AAAA,EACrC;AAGA,YAAU;AAAA;AACV,YAAU,iCAAiC,MAAM;AAAA;AACjD,YAAU,gCAAgC,MAAM;AAAA;AAAA;AAGhD,YAAU;AAAA;AACV,YAAU;AAAA;AAAA;AAGV,YAAU;AAAA;AACV,YAAU,UAAU,KAAK,IAAI,IAAI;AAGjC,MAAI,aAAa,SAAS,GAAG;AAC3B,cAAU;AAAA;AACV,cAAU,aAAa,KAAK,IAAI,IAAI;AAAA,EACtC;AAEA,YAAU;AAAA;AAEV,SAAO;AACT;AAaA,eAAsB,iBACpB,YACA,YACA,eACkB;AAClB,QAAM,iBAAiB,KAAK,KAAK,YAAY,GAAG,UAAU,OAAO;AACjE,QAAM,SAAS,MAAM,GAAG,WAAW,cAAc;AAGjD,MAAI,QAAQ;AACV,UAAM,GAAG,OAAO,cAAc;AAAA,EAChC;AAGA,QAAM,GAAG,UAAU,gBAAgB,aAAa;AAEhD,SAAO;AACT;AAKA,eAAsB,WAAW,UAAkB,YAAmC;AACpF,QAAM,gBAAgB,KAAK,KAAK,YAAY,GAAG,QAAQ,OAAO;AAC9D,QAAM,cAAc,cAAc,QAAQ,mBAAmB,eAAe;AAG5E,QAAM,GAAG,UAAU,KAAK,QAAQ,WAAW,CAAC;AAG5C,MAAI,MAAM,GAAG,WAAW,WAAW,GAAG;AACpC,UAAM,GAAG,OAAO,WAAW;AAAA,EAC7B;AAGA,QAAM,MAAM,QAAQ,CAAC,MAAM,OAAO,eAAe,WAAW,CAAC;AAC/D;AAKA,eAAsB,YAAY,eAAsC;AACtE,MAAI;AAEF,UAAM,MAAM,QAAQ,CAAC,SAAS,IAAI,CAAC;AAGnC,UAAM,QAAQ,cAAc,MAAM,GAAG;AACrC,QAAI,MAAM,SAAS,GAAG;AAEpB,YAAM,MAAM,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,OAAO,MAAM;AAAA,IACvD;AAAA,EACF,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,KAAK,EAAE;AAAA,EAC7F;AACF;"}
package/docs/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Suthep Documentation
2
+
3
+ Welcome to the Suthep documentation! This guide will help you understand and use Suthep, a powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
4
+
5
+ ## Choose Your Language / เลือกภาษาของคุณ
6
+
7
+ - 🇺🇸 [English Documentation](./en/) - English documentation
8
+ - 🇹🇭 [เอกสารภาษาไทย](./th/) - Thai documentation
9
+
10
+ ## Quick Navigation
11
+
12
+ ### English / ภาษาอังกฤษ
13
+
14
+ - [Getting Started](./en/getting-started.md) - Installation and quick start guide
15
+ - [Configuration Reference](./en/configuration.md) - Complete configuration file reference
16
+ - [Commands Reference](./en/commands.md) - Detailed command documentation
17
+ - [Examples](./en/examples.md) - Real-world deployment examples
18
+ - [Troubleshooting](./en/troubleshooting.md) - Common issues and solutions
19
+ - [Developer Guide](./en/developer-guide.md) - Complete guide for developers
20
+
21
+ ### ไทย / Thai
22
+
23
+ - [เริ่มต้นใช้งาน](./th/getting-started.md) - คู่มือการติดตั้งและเริ่มต้นใช้งาน
24
+ - [คู่มือการตั้งค่า](./th/configuration.md) - คู่มืออ้างอิงไฟล์ configuration ฉบับสมบูรณ์
25
+ - [คู่มือคำสั่ง](./th/commands.md) - เอกสารคำสั่งแบบละเอียด
26
+
27
+ ## What is Suthep?
28
+
29
+ Suthep is a deployment automation tool that simplifies the process of deploying web services with:
30
+
31
+ - ✅ **Automatic Nginx reverse proxy setup** - No manual Nginx configuration needed
32
+ - ✅ **Automatic HTTPS with Certbot** - Free SSL certificates from Let's Encrypt
33
+ - ✅ **Zero-downtime deployment** - Rolling and blue-green deployment strategies
34
+ - ✅ **Docker container support** - Deploy containerized applications easily
35
+ - ✅ **Multiple domain/subdomain support** - One service, multiple domains
36
+ - ✅ **Health check integration** - Ensure services are healthy before going live
37
+ - ✅ **YAML-based configuration** - Simple, declarative configuration
38
+
39
+ ## Quick Start
40
+
41
+ ```bash
42
+ # Install dependencies
43
+ npm install
44
+ npm link
45
+
46
+ # Initialize configuration
47
+ suthep init
48
+
49
+ # Setup prerequisites
50
+ suthep setup
51
+
52
+ # Deploy your services
53
+ suthep deploy
54
+ ```
55
+
56
+ ## Requirements
57
+
58
+ - Node.js 16+
59
+ - Nginx (installed via `suthep setup`)
60
+ - Certbot (installed via `suthep setup`)
61
+ - Docker (optional, for Docker-based services)
62
+ - sudo access (for Nginx and Certbot operations)
63
+
64
+ ## Documentation Structure
65
+
66
+ ```
67
+ docs/
68
+ ├── README.md (this file)
69
+ ├── en/ # English documentation
70
+ │ ├── README.md
71
+ │ ├── getting-started.md
72
+ │ ├── configuration.md
73
+ │ ├── commands.md
74
+ │ └── ...
75
+ └── th/ # Thai documentation
76
+ ├── README.md
77
+ ├── getting-started.md
78
+ ├── configuration.md
79
+ └── commands.md
80
+ ```
81
+
82
+ ## Contributing Translations
83
+
84
+ We welcome contributions to translate documentation into more languages!
85
+
86
+ To contribute:
87
+ 1. Create a new language folder (e.g., `ja/` for Japanese)
88
+ 2. Translate the documentation files
89
+ 3. Update this README with links to the new language
90
+ 4. Submit a pull request
91
+
92
+ ## License
93
+
94
+ [MIT](../LICENSE)
95
+
@@ -0,0 +1,211 @@
1
+ # Translations Guide
2
+
3
+ This document describes the translation structure and how to contribute translations for the Suthep documentation.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ docs/
9
+ ├── README.md # Main documentation index with language selection
10
+ ├── TRANSLATIONS.md # This file - translation guide
11
+ ├── en/ # English documentation (default)
12
+ │ ├── README.md
13
+ │ ├── getting-started.md
14
+ │ ├── configuration.md
15
+ │ ├── commands.md
16
+ │ ├── port-binding.md
17
+ │ ├── docker-ports-config.md
18
+ │ ├── examples.md
19
+ │ ├── troubleshooting.md
20
+ │ ├── architecture.md
21
+ │ ├── developer-guide.md
22
+ │ └── api-reference.md
23
+ └── th/ # Thai documentation
24
+ ├── README.md
25
+ ├── getting-started.md
26
+ ├── configuration.md
27
+ └── commands.md
28
+ ```
29
+
30
+ ## Translation Status
31
+
32
+ | Document | English | Thai | Status |
33
+ |----------|---------|------|--------|
34
+ | README | ✅ | ✅ | Complete |
35
+ | Getting Started | ✅ | ✅ | Complete |
36
+ | Configuration | ✅ | ✅ | Complete |
37
+ | Commands | ✅ | ✅ | Complete |
38
+ | Port Binding | ✅ | ⏳ | English only |
39
+ | Docker Ports Config | ✅ | ⏳ | English only |
40
+ | Examples | ✅ | ⏳ | English only |
41
+ | Troubleshooting | ✅ | ⏳ | English only |
42
+ | Architecture | ✅ | ⏳ | English only |
43
+ | Developer Guide | ✅ | ⏳ | English only |
44
+ | API Reference | ✅ | ⏳ | English only |
45
+
46
+ **Legend:**
47
+ - ✅ Available
48
+ - ⏳ Coming soon / In progress
49
+ - ❌ Not started
50
+
51
+ ## Adding a New Translation
52
+
53
+ ### Step 1: Create Language Folder
54
+
55
+ Create a new folder for your language using the ISO 639-1 language code:
56
+
57
+ ```bash
58
+ mkdir docs/ja # For Japanese
59
+ mkdir docs/es # For Spanish
60
+ mkdir docs/fr # For French
61
+ ```
62
+
63
+ ### Step 2: Create README.md
64
+
65
+ Create a `README.md` file in the new language folder that:
66
+ - Welcomes users in the target language
67
+ - Lists all available documents
68
+ - Links to English versions for untranslated documents
69
+ - Links back to the main documentation index
70
+
71
+ Example structure:
72
+
73
+ ```markdown
74
+ # Suthep Documentation
75
+
76
+ [Welcome message in target language]
77
+
78
+ ## Table of Contents
79
+
80
+ 1. [Getting Started](./getting-started.md)
81
+ 2. [Configuration](./configuration.md)
82
+ ...
83
+
84
+ ## Other Languages
85
+
86
+ - [English](../en/README.md)
87
+ - [Your Language](./README.md) (current)
88
+ ```
89
+
90
+ ### Step 3: Translate Documents
91
+
92
+ Translate documents in priority order:
93
+
94
+ 1. **README.md** - Main index
95
+ 2. **getting-started.md** - Most important for new users
96
+ 3. **configuration.md** - Essential reference
97
+ 4. **commands.md** - User-facing documentation
98
+ 5. Other documents as needed
99
+
100
+ ### Step 4: Update Links
101
+
102
+ - Update internal links to use relative paths within the language folder
103
+ - Link to English versions (`../en/filename.md`) for untranslated documents
104
+ - Update the main `docs/README.md` to include your language
105
+
106
+ ### Step 5: Update Translation Status
107
+
108
+ Update this file (`TRANSLATIONS.md`) to reflect the new translation status.
109
+
110
+ ## Translation Guidelines
111
+
112
+ ### File Naming
113
+
114
+ - Use the same filenames as English versions
115
+ - Keep file extensions: `.md`
116
+ - No language suffix needed (language is determined by folder)
117
+
118
+ ### Link Structure
119
+
120
+ - **Within same language**: `./filename.md`
121
+ - **To English version**: `../en/filename.md`
122
+ - **To main index**: `../README.md`
123
+ - **To other languages**: `../lang-code/README.md`
124
+
125
+ ### Code Examples
126
+
127
+ - Keep code examples in English (they're universal)
128
+ - Translate comments in code examples if appropriate
129
+ - Keep command-line examples in English
130
+
131
+ ### Technical Terms
132
+
133
+ - Keep technical terms in English when there's no common translation
134
+ - Use English terms for: Docker, Nginx, Certbot, SSL, HTTPS, etc.
135
+ - Provide explanations in the target language
136
+
137
+ ### Formatting
138
+
139
+ - Maintain the same Markdown structure
140
+ - Keep the same heading hierarchy
141
+ - Preserve code blocks and syntax highlighting
142
+ - Maintain table structures
143
+
144
+ ## Website Integration
145
+
146
+ This structure is designed to work with documentation websites:
147
+
148
+ ### Static Site Generators
149
+
150
+ Works well with:
151
+ - **Docusaurus** - Multi-language support built-in
152
+ - **VitePress** - Supports i18n
153
+ - **MkDocs** - With i18n plugin
154
+ - **GitBook** - Multi-language support
155
+
156
+ ### URL Structure
157
+
158
+ For websites, URLs would be:
159
+ - `/en/getting-started` - English
160
+ - `/th/getting-started` - Thai
161
+ - `/` - Language selection
162
+
163
+ ### Configuration Example (Docusaurus)
164
+
165
+ ```javascript
166
+ // docusaurus.config.js
167
+ module.exports = {
168
+ i18n: {
169
+ defaultLocale: 'en',
170
+ locales: ['en', 'th'],
171
+ localeConfigs: {
172
+ en: {
173
+ label: 'English',
174
+ },
175
+ th: {
176
+ label: 'ไทย',
177
+ },
178
+ },
179
+ },
180
+ };
181
+ ```
182
+
183
+ ## Contributing
184
+
185
+ 1. **Fork the repository**
186
+ 2. **Create a language folder** (if new language)
187
+ 3. **Translate documents** following the guidelines
188
+ 4. **Update this file** with translation status
189
+ 5. **Submit a pull request**
190
+
191
+ ## Quality Checklist
192
+
193
+ Before submitting a translation:
194
+
195
+ - [ ] All internal links work correctly
196
+ - [ ] Code examples are preserved
197
+ - [ ] Technical terms are handled appropriately
198
+ - [ ] Formatting matches English version
199
+ - [ ] README.md includes language selection
200
+ - [ ] Translation status is updated
201
+ - [ ] Main README.md is updated
202
+
203
+ ## Questions?
204
+
205
+ If you have questions about translations:
206
+ - Open an issue on GitHub
207
+ - Check existing translations for examples
208
+ - Review the English documentation for context
209
+
210
+ Thank you for contributing to Suthep documentation! 🎉
211
+