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,378 @@
1
+ import { execa } from "execa";
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ function isRootDomain(domain) {
5
+ const domainWithoutWww = domain.startsWith("www.") ? domain.substring(4) : domain;
6
+ const partsWithoutWww = domainWithoutWww.split(".");
7
+ return partsWithoutWww.length === 2;
8
+ }
9
+ function getCanonicalDomain(domain, allDomains) {
10
+ if (isRootDomain(domain)) {
11
+ const domainWithoutWww = domain.startsWith("www.") ? domain.substring(4) : domain;
12
+ const wwwVersion = `www.${domainWithoutWww}`;
13
+ const nonWwwVersion = domainWithoutWww;
14
+ if (allDomains.has(wwwVersion) && allDomains.has(nonWwwVersion)) {
15
+ return wwwVersion;
16
+ }
17
+ }
18
+ return domain;
19
+ }
20
+ function normalizeDomains(domains) {
21
+ let canonical = domains[0];
22
+ const domainSet = new Set(domains);
23
+ for (const domain of domains) {
24
+ if (isRootDomain(domain)) {
25
+ const domainWithoutWww = domain.startsWith("www.") ? domain.substring(4) : domain;
26
+ const wwwVersion = `www.${domainWithoutWww}`;
27
+ const nonWwwVersion = domainWithoutWww;
28
+ if (domainSet.has(wwwVersion) && domainSet.has(nonWwwVersion)) {
29
+ canonical = wwwVersion;
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ const sortedDomains = [...domains];
35
+ if (canonical.startsWith("www.")) {
36
+ const nonWww = canonical.substring(4);
37
+ const wwwIndex = sortedDomains.indexOf(canonical);
38
+ const nonWwwIndex = sortedDomains.indexOf(nonWww);
39
+ if (wwwIndex !== -1 && nonWwwIndex !== -1 && wwwIndex > nonWwwIndex) {
40
+ [sortedDomains[wwwIndex], sortedDomains[nonWwwIndex]] = [
41
+ sortedDomains[nonWwwIndex],
42
+ sortedDomains[wwwIndex]
43
+ ];
44
+ }
45
+ }
46
+ const serverNames = sortedDomains.join(" ");
47
+ return {
48
+ canonical,
49
+ serverNames
50
+ };
51
+ }
52
+ function generateNginxConfig(service, withHttps, portOverride) {
53
+ const { canonical, serverNames } = normalizeDomains(service.domains);
54
+ const domainSafe = canonical.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
55
+ const upstreamName = `${domainSafe}_${service.name}`;
56
+ const servicePath = service.path || "/";
57
+ const port = portOverride || service.port;
58
+ let config = `# Nginx configuration for ${service.name}
59
+
60
+ `;
61
+ config += `upstream ${upstreamName} {
62
+ `;
63
+ config += ` server localhost:${port} max_fails=3 fail_timeout=30s;
64
+ `;
65
+ config += ` keepalive 32;
66
+ `;
67
+ config += `}
68
+
69
+ `;
70
+ if (withHttps) {
71
+ config += `server {
72
+ `;
73
+ config += ` listen 80;
74
+ `;
75
+ config += ` listen [::]:80;
76
+ `;
77
+ config += ` server_name ${serverNames};
78
+
79
+ `;
80
+ config += ` # Redirect all HTTP to HTTPS
81
+ `;
82
+ config += ` return 301 https://$server_name$request_uri;
83
+ `;
84
+ config += `}
85
+
86
+ `;
87
+ config += `server {
88
+ `;
89
+ config += ` listen 443 ssl http2;
90
+ `;
91
+ config += ` listen [::]:443 ssl http2;
92
+ `;
93
+ config += ` server_name ${serverNames};
94
+
95
+ `;
96
+ config += ` # SSL Configuration
97
+ `;
98
+ config += ` ssl_certificate /etc/letsencrypt/live/${canonical}/fullchain.pem;
99
+ `;
100
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${canonical}/privkey.pem;
101
+ `;
102
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;
103
+ `;
104
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;
105
+ `;
106
+ config += ` ssl_prefer_server_ciphers on;
107
+
108
+ `;
109
+ } else {
110
+ config += `server {
111
+ `;
112
+ config += ` listen 80;
113
+ `;
114
+ config += ` listen [::]:80;
115
+ `;
116
+ config += ` server_name ${serverNames};
117
+
118
+ `;
119
+ }
120
+ config += ` # Logging
121
+ `;
122
+ config += ` access_log /var/log/nginx/${service.name}_access.log;
123
+ `;
124
+ config += ` error_log /var/log/nginx/${service.name}_error.log;
125
+
126
+ `;
127
+ config += ` # Client settings
128
+ `;
129
+ config += ` client_max_body_size 100M;
130
+
131
+ `;
132
+ config += ` # Proxy settings
133
+ `;
134
+ config += ` location ${servicePath} {
135
+ `;
136
+ config += ` proxy_pass http://${upstreamName};
137
+ `;
138
+ config += ` proxy_http_version 1.1;
139
+ `;
140
+ config += ` proxy_set_header Upgrade $http_upgrade;
141
+ `;
142
+ config += ` proxy_set_header Connection 'upgrade';
143
+ `;
144
+ config += ` proxy_set_header Host $host;
145
+ `;
146
+ config += ` proxy_set_header X-Real-IP $remote_addr;
147
+ `;
148
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
149
+ `;
150
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;
151
+ `;
152
+ config += ` proxy_cache_bypass $http_upgrade;
153
+ `;
154
+ config += ` proxy_connect_timeout 60s;
155
+ `;
156
+ config += ` proxy_send_timeout 60s;
157
+ `;
158
+ config += ` proxy_read_timeout 60s;
159
+ `;
160
+ config += ` }
161
+ `;
162
+ if (service.healthCheck) {
163
+ config += `
164
+ # Health check endpoint
165
+ `;
166
+ config += ` location ${service.healthCheck.path} {
167
+ `;
168
+ config += ` proxy_pass http://${upstreamName};
169
+ `;
170
+ config += ` access_log off;
171
+ `;
172
+ config += ` }
173
+ `;
174
+ }
175
+ config += `}
176
+ `;
177
+ return config;
178
+ }
179
+ function generateMultiServiceNginxConfig(services, domain, withHttps, portOverrides) {
180
+ const upstreams = [];
181
+ const locations = [];
182
+ const healthChecks = [];
183
+ const allServiceDomains = /* @__PURE__ */ new Set();
184
+ for (const service of services) {
185
+ for (const d of service.domains) {
186
+ allServiceDomains.add(d);
187
+ }
188
+ }
189
+ let canonicalDomain = domain;
190
+ let serverNames = domain;
191
+ if (isRootDomain(domain)) {
192
+ const domainWithoutWww = domain.startsWith("www.") ? domain.substring(4) : domain;
193
+ const wwwVersion = `www.${domainWithoutWww}`;
194
+ const nonWwwVersion = domainWithoutWww;
195
+ if (allServiceDomains.has(wwwVersion) && allServiceDomains.has(nonWwwVersion)) {
196
+ canonicalDomain = wwwVersion;
197
+ serverNames = `${wwwVersion} ${nonWwwVersion}`;
198
+ }
199
+ }
200
+ const sortedServices = [...services].sort((a, b) => {
201
+ const pathA = (a.path || "/").length;
202
+ const pathB = (b.path || "/").length;
203
+ return pathB - pathA;
204
+ });
205
+ const portToUpstreamName = /* @__PURE__ */ new Map();
206
+ const domainSafe = canonicalDomain.replace(/\./g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
207
+ for (const service of sortedServices) {
208
+ const port = portOverrides?.get(service.name) || service.port;
209
+ if (!portToUpstreamName.has(port)) {
210
+ const upstreamName = `${domainSafe}_port_${port}`;
211
+ portToUpstreamName.set(port, upstreamName);
212
+ upstreams.push(`upstream ${upstreamName} {`);
213
+ upstreams.push(` server localhost:${port} max_fails=3 fail_timeout=30s;`);
214
+ upstreams.push(` keepalive 32;`);
215
+ upstreams.push(`}`);
216
+ }
217
+ }
218
+ for (const service of sortedServices) {
219
+ const servicePath = service.path || "/";
220
+ const port = portOverrides?.get(service.name) || service.port;
221
+ const upstreamName = portToUpstreamName.get(port);
222
+ if (servicePath === "/") {
223
+ locations.push(` # Service: ${service.name}`);
224
+ locations.push(` location / {`);
225
+ } else {
226
+ locations.push(` # Service: ${service.name}`);
227
+ locations.push(` location ${servicePath} {`);
228
+ }
229
+ locations.push(` proxy_pass http://${upstreamName};`);
230
+ locations.push(` proxy_http_version 1.1;`);
231
+ locations.push(` proxy_set_header Upgrade $http_upgrade;`);
232
+ locations.push(` proxy_set_header Connection 'upgrade';`);
233
+ locations.push(` proxy_set_header Host $host;`);
234
+ locations.push(` proxy_set_header X-Real-IP $remote_addr;`);
235
+ locations.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
236
+ locations.push(` proxy_set_header X-Forwarded-Proto $scheme;`);
237
+ locations.push(` proxy_cache_bypass $http_upgrade;`);
238
+ locations.push(` proxy_connect_timeout 60s;`);
239
+ locations.push(` proxy_send_timeout 60s;`);
240
+ locations.push(` proxy_read_timeout 60s;`);
241
+ locations.push(` }`);
242
+ if (service.healthCheck) {
243
+ healthChecks.push(` # Health check for ${service.name}`);
244
+ healthChecks.push(` location ${service.healthCheck.path} {`);
245
+ healthChecks.push(` proxy_pass http://${upstreamName};`);
246
+ healthChecks.push(` access_log off;`);
247
+ healthChecks.push(` }`);
248
+ }
249
+ }
250
+ let config = `# Nginx configuration for ${canonicalDomain}
251
+ `;
252
+ config += `# Multiple services on the same domain
253
+
254
+ `;
255
+ config += upstreams.join("\n") + "\n\n";
256
+ if (withHttps) {
257
+ config += `server {
258
+ `;
259
+ config += ` listen 80;
260
+ `;
261
+ config += ` listen [::]:80;
262
+ `;
263
+ config += ` server_name ${serverNames};
264
+
265
+ `;
266
+ config += ` # Redirect all HTTP to HTTPS
267
+ `;
268
+ config += ` return 301 https://$server_name$request_uri;
269
+ `;
270
+ config += `}
271
+
272
+ `;
273
+ config += `server {
274
+ `;
275
+ config += ` listen 443 ssl http2;
276
+ `;
277
+ config += ` listen [::]:443 ssl http2;
278
+ `;
279
+ config += ` server_name ${serverNames};
280
+
281
+ `;
282
+ config += ` # SSL Configuration
283
+ `;
284
+ config += ` ssl_certificate /etc/letsencrypt/live/${canonicalDomain}/fullchain.pem;
285
+ `;
286
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${canonicalDomain}/privkey.pem;
287
+ `;
288
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;
289
+ `;
290
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;
291
+ `;
292
+ config += ` ssl_prefer_server_ciphers on;
293
+
294
+ `;
295
+ } else {
296
+ config += `server {
297
+ `;
298
+ config += ` listen 80;
299
+ `;
300
+ config += ` listen [::]:80;
301
+ `;
302
+ config += ` server_name ${serverNames};
303
+
304
+ `;
305
+ }
306
+ config += ` # Logging
307
+ `;
308
+ config += ` access_log /var/log/nginx/${canonicalDomain}_access.log;
309
+ `;
310
+ config += ` error_log /var/log/nginx/${canonicalDomain}_error.log;
311
+
312
+ `;
313
+ config += ` # Client settings
314
+ `;
315
+ config += ` client_max_body_size 100M;
316
+
317
+ `;
318
+ config += ` # Service locations
319
+ `;
320
+ config += locations.join("\n") + "\n\n";
321
+ if (healthChecks.length > 0) {
322
+ config += ` # Health check endpoints
323
+ `;
324
+ config += healthChecks.join("\n") + "\n";
325
+ }
326
+ config += `}
327
+ `;
328
+ return config;
329
+ }
330
+ async function writeNginxConfig(configName, configPath, configContent) {
331
+ const configFilePath = path.join(configPath, `${configName}.conf`);
332
+ const exists = await fs.pathExists(configFilePath);
333
+ if (exists) {
334
+ await fs.remove(configFilePath);
335
+ }
336
+ await fs.writeFile(configFilePath, configContent);
337
+ return exists;
338
+ }
339
+ async function enableSite(siteName, configPath) {
340
+ const availablePath = path.join(configPath, `${siteName}.conf`);
341
+ const enabledPath = availablePath.replace("sites-available", "sites-enabled");
342
+ await fs.ensureDir(path.dirname(enabledPath));
343
+ if (await fs.pathExists(enabledPath)) {
344
+ await fs.remove(enabledPath);
345
+ }
346
+ await execa("sudo", ["ln", "-sf", availablePath, enabledPath]);
347
+ }
348
+ async function reloadNginx(reloadCommand) {
349
+ try {
350
+ await execa("sudo", ["nginx", "-t"]);
351
+ const parts = reloadCommand.split(" ");
352
+ if (parts.length > 0) {
353
+ await execa(parts[0], parts.slice(1), { shell: true });
354
+ }
355
+ } catch (error) {
356
+ throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`);
357
+ }
358
+ }
359
+ async function disableSite(siteName, configPath) {
360
+ const enabledPath = path.join(
361
+ configPath.replace("sites-available", "sites-enabled"),
362
+ `${siteName}.conf`
363
+ );
364
+ if (await fs.pathExists(enabledPath)) {
365
+ await fs.remove(enabledPath);
366
+ }
367
+ }
368
+ export {
369
+ disableSite,
370
+ enableSite,
371
+ generateMultiServiceNginxConfig,
372
+ generateNginxConfig,
373
+ getCanonicalDomain,
374
+ isRootDomain,
375
+ reloadNginx,
376
+ writeNginxConfig
377
+ };
378
+ //# 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 * Check if a domain is a root domain (no subdomain)\n * Root domain: example.com (2 parts)\n * Subdomain: dev.example.com (3+ parts)\n */\nexport function isRootDomain(domain: string): boolean {\n // Remove 'www.' prefix if present for checking\n const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain\n const partsWithoutWww = domainWithoutWww.split('.')\n // Root domain has exactly 2 parts (domain + TLD)\n return partsWithoutWww.length === 2\n}\n\n/**\n * Get canonical domain for a given domain\n * For root domains with both www and non-www, returns www version\n * Otherwise returns the domain itself\n */\nexport function getCanonicalDomain(domain: string, allDomains: Set<string>): string {\n if (isRootDomain(domain)) {\n const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain\n const wwwVersion = `www.${domainWithoutWww}`\n const nonWwwVersion = domainWithoutWww\n\n // If we have both www and non-www versions, prefer www\n if (allDomains.has(wwwVersion) && allDomains.has(nonWwwVersion)) {\n return wwwVersion\n }\n }\n return domain\n}\n\n/**\n * Normalize domain list to handle www/non-www variants\n * For root domains: combine www and non-www in same server_name\n * For subdomains: keep as configured\n * Returns canonical domain (for SSL certs) and combined server names\n */\nfunction normalizeDomains(domains: string[]): {\n canonical: string\n serverNames: string\n} {\n // Use first domain as canonical (for SSL certificates)\n let canonical: string = domains[0]\n\n // For root domains with both www and non-www, prefer www as canonical\n const domainSet = new Set(domains)\n for (const domain of domains) {\n if (isRootDomain(domain)) {\n const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain\n const wwwVersion = `www.${domainWithoutWww}`\n const nonWwwVersion = domainWithoutWww\n\n // If we have both www and non-www versions of a root domain\n if (domainSet.has(wwwVersion) && domainSet.has(nonWwwVersion)) {\n // Prefer www for root domains (for SSL cert path)\n canonical = wwwVersion\n break\n }\n }\n }\n\n // Combine all domains in server_name\n // For root domains with both www and non-www, ensure www comes first\n const sortedDomains = [...domains]\n if (canonical.startsWith('www.')) {\n const nonWww = canonical.substring(4)\n const wwwIndex = sortedDomains.indexOf(canonical)\n const nonWwwIndex = sortedDomains.indexOf(nonWww)\n if (wwwIndex !== -1 && nonWwwIndex !== -1 && wwwIndex > nonWwwIndex) {\n // Swap to put www first\n ;[sortedDomains[wwwIndex], sortedDomains[nonWwwIndex]] = [\n sortedDomains[nonWwwIndex],\n sortedDomains[wwwIndex],\n ]\n }\n }\n const serverNames = sortedDomains.join(' ')\n\n return {\n canonical,\n serverNames,\n }\n}\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 // Normalize domains - combine www and root domains in same server_name\n const { canonical, serverNames } = normalizeDomains(service.domains)\n\n // Use canonical domain for upstream naming and SSL certificates\n const domainSafe = canonical.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 config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${canonical}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${canonical}/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 * Combines upstreams when multiple services share the same port\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 // Collect all domains from services to check for www/non-www variants\n const allServiceDomains = new Set<string>()\n for (const service of services) {\n for (const d of service.domains) {\n allServiceDomains.add(d)\n }\n }\n\n // Determine canonical domain (for SSL certs) and combined server names\n // For root domains: combine www and non-www in same server_name\n // For subdomains: use as-is\n let canonicalDomain = domain\n let serverNames = domain\n\n if (isRootDomain(domain)) {\n const domainWithoutWww = domain.startsWith('www.') ? domain.substring(4) : domain\n const wwwVersion = `www.${domainWithoutWww}`\n const nonWwwVersion = domainWithoutWww\n\n if (allServiceDomains.has(wwwVersion) && allServiceDomains.has(nonWwwVersion)) {\n // Prefer www for root domains (for SSL cert path)\n canonicalDomain = wwwVersion\n // Combine both in server_name (www first)\n serverNames = `${wwwVersion} ${nonWwwVersion}`\n }\n }\n // Subdomains are left as-is\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 // Map port to upstream name - combine services with same port into one upstream\n const portToUpstreamName = new Map<number, string>()\n const domainSafe = canonicalDomain.replace(/\\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')\n\n // First pass: create upstreams grouped by port\n for (const service of sortedServices) {\n const port = portOverrides?.get(service.name) || service.port\n\n // If we haven't seen this port before, create a new upstream\n if (!portToUpstreamName.has(port)) {\n // Use domain_port format for upstream name to ensure uniqueness\n // Since ports are unique within a domain, this format ensures no conflicts\n const upstreamName = `${domainSafe}_port_${port}`\n\n portToUpstreamName.set(port, upstreamName)\n\n // Generate upstream block\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 }\n\n // Second pass: create location blocks for each service, using the shared upstream\n for (const service of sortedServices) {\n const servicePath = service.path || '/'\n const port = portOverrides?.get(service.name) || service.port\n const upstreamName = portToUpstreamName.get(port)!\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 ${canonicalDomain}\\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 ${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 config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${canonicalDomain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${canonicalDomain}/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/${canonicalDomain}_access.log;\\n`\n config += ` error_log /var/log/nginx/${canonicalDomain}_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":[],"mappings":"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"}
package/docs/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Suthep Documentation
2
+
3
+ Welcome to the Suthep documentation! Choose your preferred language:
4
+
5
+ ## Available Languages
6
+
7
+ - 🇬🇧 [English Documentation](./english/README.md) - English user guide
8
+ - 🇹🇭 [Thai Documentation](./thai/README.md) - คู่มือผู้ใช้ภาษาไทย
9
+
10
+ ## Documentation Structure
11
+
12
+ Both language versions include:
13
+
14
+ 1. **Introduction** - Overview of Suthep and its features
15
+ 2. **Installation** - Step-by-step installation guide
16
+ 3. **Quick Start** - Get started in minutes
17
+ 4. **Configuration Guide** - Complete configuration reference
18
+ 5. **Commands Reference** - All available commands
19
+ 6. **Examples** - Real-world deployment examples
20
+ 7. **Troubleshooting** - Common issues and solutions
21
+ 8. **Advanced Topics** - Advanced configuration and optimization
22
+
23
+ ## Quick Links
24
+
25
+ ### English
26
+ - [Start Here](./english/README.md)
27
+ - [Quick Start Guide](./english/03-quick-start.md)
28
+ - [Configuration Reference](./english/04-configuration.md)
29
+
30
+ ### Thai
31
+ - [เริ่มต้นที่นี่](./thai/README.md)
32
+ - [คู่มือเริ่มต้นใช้งาน](./thai/03-quick-start.md)
33
+ - [คู่มือการตั้งค่า](./thai/04-configuration.md)
34
+
35
+ ---
36
+
37
+ For the main project README, see [../README.md](../README.md)
38
+
@@ -0,0 +1,84 @@
1
+ # Introduction to Suthep
2
+
3
+ ## What is Suthep?
4
+
5
+ Suthep is a command-line tool designed to simplify the deployment of web applications and services. It automates the complex process of setting up reverse proxies, SSL certificates, and managing deployments with zero downtime.
6
+
7
+ ## Why Use Suthep?
8
+
9
+ ### Simplified Deployment
10
+
11
+ Traditional deployment processes require manual configuration of:
12
+ - Nginx reverse proxy rules
13
+ - SSL certificate management
14
+ - Docker container orchestration
15
+ - Health check monitoring
16
+ - Zero-downtime deployment strategies
17
+
18
+ Suthep handles all of this automatically with a simple YAML configuration file.
19
+
20
+ ### Key Benefits
21
+
22
+ 1. **Time Savings** - Deploy in minutes instead of hours
23
+ 2. **Reduced Errors** - Automated configuration reduces human error
24
+ 3. **Zero Downtime** - Rolling deployments ensure continuous service availability
25
+ 4. **Easy Management** - Simple commands to deploy, update, and manage services
26
+ 5. **Cost Effective** - Run multiple services on a single server efficiently
27
+
28
+ ## How It Works
29
+
30
+ Suthep follows a simple workflow:
31
+
32
+ 1. **Configure** - Create a `suthep.yml` configuration file
33
+ 2. **Setup** - Install prerequisites (Nginx, Certbot) with `suthep setup`
34
+ 3. **Deploy** - Deploy your services with `suthep deploy`
35
+
36
+ Behind the scenes, Suthep:
37
+ - Generates Nginx configuration files
38
+ - Obtains SSL certificates from Let's Encrypt
39
+ - Manages Docker containers
40
+ - Performs health checks
41
+ - Handles zero-downtime deployments
42
+
43
+ ## Use Cases
44
+
45
+ Suthep is ideal for:
46
+
47
+ - **Small to Medium Applications** - Deploy multiple services on a single server
48
+ - **Microservices** - Manage multiple services with different domains
49
+ - **Docker Applications** - Deploy containerized applications easily
50
+ - **API Services** - Set up reverse proxies for API endpoints
51
+ - **Web Applications** - Deploy web apps with automatic HTTPS
52
+
53
+ ## What You'll Learn
54
+
55
+ In this guide, you'll learn:
56
+
57
+ - How to install and set up Suthep
58
+ - How to create and configure deployment files
59
+ - How to use all available commands
60
+ - How to deploy different types of services
61
+ - How to troubleshoot common issues
62
+ - Advanced configuration options
63
+
64
+ ## Prerequisites
65
+
66
+ Before using Suthep, you should have:
67
+
68
+ - **Node.js 16+** installed
69
+ - **sudo/administrator access** on your server
70
+ - **Domain names** pointing to your server (for HTTPS)
71
+ - **Basic knowledge** of YAML configuration files
72
+ - **Docker** (optional, only if deploying Docker containers)
73
+
74
+ ## Next Steps
75
+
76
+ Ready to get started? Continue to:
77
+
78
+ - [Installation Guide](./02-installation.md) - Install Suthep on your system
79
+ - [Quick Start Guide](./03-quick-start.md) - Deploy your first service
80
+
81
+ ---
82
+
83
+ **Previous:** [README](./README.md) | **Next:** [Installation →](./02-installation.md)
84
+