suthep 0.1.0 → 0.2.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 (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
@@ -0,0 +1,89 @@
1
+ # Suthep Configuration Example
2
+ # Copy this file to suthep.yml and customize for your project
3
+
4
+ project:
5
+ name: my-app
6
+ version: 1.0.0
7
+
8
+ services:
9
+ # Example 1: Service without Docker (runs directly on host)
10
+ # Perfect for Node.js with PM2, Python with Gunicorn, etc.
11
+ - name: api
12
+ port: 3000
13
+ domains:
14
+ - api.example.com
15
+ - www.api.example.com
16
+ healthCheck:
17
+ path: /health
18
+ interval: 30
19
+ environment:
20
+ NODE_ENV: production
21
+ PORT: 3000
22
+
23
+ # Example 2: Service with Docker image
24
+ # Suthep will pull the image and create the container
25
+ - name: webapp
26
+ port: 8080
27
+ docker:
28
+ image: nginx:latest
29
+ container: webapp-container
30
+ port: 80
31
+ domains:
32
+ - example.com
33
+ - www.example.com
34
+ healthCheck:
35
+ path: /
36
+ interval: 30
37
+
38
+ # Example 3: Service connecting to existing Docker container
39
+ # Useful when containers are managed separately (e.g., docker-compose)
40
+ - name: database-proxy
41
+ port: 5432
42
+ docker:
43
+ container: postgres-container
44
+ port: 5432
45
+ domains:
46
+ - db.example.com
47
+
48
+ # Example 4: Path-based routing
49
+ # Multiple services on the same domain using different paths
50
+ - name: api-v2
51
+ port: 3001
52
+ path: /api
53
+ domains:
54
+ - myapp.com
55
+ docker:
56
+ image: myapp/api:latest
57
+ container: api-v2-container
58
+ port: 3001
59
+ healthCheck:
60
+ path: /health
61
+ interval: 30
62
+
63
+ - name: frontend
64
+ port: 3000
65
+ path: /
66
+ domains:
67
+ - myapp.com
68
+ docker:
69
+ image: myapp/frontend:latest
70
+ container: frontend-container
71
+ port: 3000
72
+ healthCheck:
73
+ path: /
74
+ interval: 30
75
+
76
+ # Nginx Configuration
77
+ nginx:
78
+ configPath: /etc/nginx/sites-available
79
+ reloadCommand: sudo nginx -t && sudo systemctl reload nginx
80
+
81
+ # Certbot Configuration (SSL/TLS certificates)
82
+ certbot:
83
+ email: admin@example.com
84
+ staging: false # Set to true for testing to avoid rate limits
85
+
86
+ # Deployment Configuration
87
+ deployment:
88
+ strategy: rolling # Options: rolling, blue-green
89
+ healthCheckTimeout: 30000 # milliseconds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suthep",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup",
6
6
  "main": "dist/index.js",
@@ -1,11 +1,23 @@
1
1
  import chalk from 'chalk'
2
2
  import fs from 'fs-extra'
3
- import path from 'path'
4
- import { requestCertificate } from '../utils/certbot'
3
+ import type { ServiceConfig } from '../types/config'
4
+ import { certificateExists, requestCertificate } from '../utils/certbot'
5
5
  import { loadConfig } from '../utils/config-loader'
6
6
  import { deployService, performHealthCheck } from '../utils/deployment'
7
- import { startDockerContainer } from '../utils/docker'
8
- import { enableSite, generateNginxConfig, reloadNginx } from '../utils/nginx'
7
+ import {
8
+ cleanupTempContainer,
9
+ startDockerContainer,
10
+ startDockerContainerZeroDowntime,
11
+ swapContainersForZeroDowntime,
12
+ type ZeroDowntimeContainerInfo,
13
+ } from '../utils/docker'
14
+ import {
15
+ enableSite,
16
+ generateMultiServiceNginxConfig,
17
+ generateNginxConfig,
18
+ reloadNginx,
19
+ writeNginxConfig,
20
+ } from '../utils/nginx'
9
21
 
10
22
  interface DeployOptions {
11
23
  file: string
@@ -28,7 +40,24 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
28
40
  console.log(chalk.green(`āœ… Configuration loaded for project: ${config.project.name}`))
29
41
  console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\n`))
30
42
 
31
- // Deploy each service
43
+ // Group services by domain
44
+ const domainToServices = new Map<string, ServiceConfig[]>()
45
+ const allDomains = new Set<string>()
46
+
47
+ for (const service of config.services) {
48
+ for (const domain of service.domains) {
49
+ allDomains.add(domain)
50
+ if (!domainToServices.has(domain)) {
51
+ domainToServices.set(domain, [])
52
+ }
53
+ domainToServices.get(domain)!.push(service)
54
+ }
55
+ }
56
+
57
+ // Deploy each service (Docker, health checks, etc.)
58
+ // Track zero-downtime info for services that need it
59
+ const serviceTempInfo = new Map<string, ZeroDowntimeContainerInfo | null>()
60
+
32
61
  for (const service of config.services) {
33
62
  console.log(chalk.cyan(`\nšŸ“¦ Deploying service: ${service.name}`))
34
63
 
@@ -36,62 +65,42 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
36
65
  // Start Docker container if configured
37
66
  if (service.docker) {
38
67
  console.log(chalk.dim(' 🐳 Managing Docker container...'))
39
- await startDockerContainer(service)
40
- }
41
-
42
- // Deploy the service
43
- await deployService(service, config.deployment)
44
68
 
45
- // Generate and configure Nginx
46
- if (options.nginx) {
47
- console.log(chalk.dim(' āš™ļø Configuring Nginx reverse proxy...'))
48
- const nginxConfigContent = generateNginxConfig(service, false)
49
- const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)
50
-
51
- await fs.writeFile(nginxConfigPath, nginxConfigContent)
52
- await enableSite(service.name, config.nginx.configPath)
53
-
54
- console.log(chalk.green(` āœ… Nginx configured for ${service.domains.join(', ')}`))
55
- }
69
+ // Use zero-downtime deployment if strategy is blue-green or rolling
70
+ if (
71
+ config.deployment.strategy === 'blue-green' ||
72
+ config.deployment.strategy === 'rolling'
73
+ ) {
74
+ const tempInfo = await startDockerContainerZeroDowntime(service)
75
+ serviceTempInfo.set(service.name, tempInfo)
56
76
 
57
- // Setup HTTPS with Certbot
58
- if (options.https && service.domains.length > 0) {
59
- console.log(chalk.dim(' šŸ” Setting up HTTPS certificates...'))
60
-
61
- for (const domain of service.domains) {
62
- try {
63
- await requestCertificate(domain, config.certbot.email, config.certbot.staging)
64
- console.log(chalk.green(` āœ… SSL certificate obtained for ${domain}`))
65
- } catch (error) {
77
+ if (tempInfo && tempInfo.oldContainerExists) {
66
78
  console.log(
67
- chalk.yellow(
68
- ` āš ļø Failed to obtain SSL for ${domain}: ${
69
- error instanceof Error ? error.message : error
70
- }`
79
+ chalk.cyan(
80
+ ` šŸ”„ Zero-downtime deployment: new container on port ${tempInfo.tempPort}`
71
81
  )
72
82
  )
73
83
  }
84
+ } else {
85
+ // Fallback to regular deployment
86
+ await startDockerContainer(service)
87
+ serviceTempInfo.set(service.name, null)
74
88
  }
75
-
76
- // Update Nginx config with HTTPS
77
- if (options.nginx) {
78
- const nginxConfigContent = generateNginxConfig(service, true)
79
- const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)
80
- await fs.writeFile(nginxConfigPath, nginxConfigContent)
81
- }
89
+ } else {
90
+ serviceTempInfo.set(service.name, null)
82
91
  }
83
92
 
84
- // Reload Nginx after all configurations
85
- if (options.nginx) {
86
- console.log(chalk.dim(' šŸ”„ Reloading Nginx...'))
87
- await reloadNginx(config.nginx.reloadCommand)
88
- }
93
+ // Deploy the service (with temp info for zero-downtime)
94
+ const tempInfo = serviceTempInfo.get(service.name) || null
95
+ await deployService(service, config.deployment, tempInfo)
89
96
 
90
- // Perform health check
97
+ // Perform health check on appropriate port
91
98
  if (service.healthCheck) {
92
99
  console.log(chalk.dim(` šŸ„ Performing health check...`))
100
+ const checkPort =
101
+ tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port
93
102
  const isHealthy = await performHealthCheck(
94
- `http://localhost:${service.port}${service.healthCheck.path}`,
103
+ `http://localhost:${checkPort}${service.healthCheck.path}`,
95
104
  config.deployment.healthCheckTimeout
96
105
  )
97
106
 
@@ -102,7 +111,7 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
102
111
  }
103
112
  }
104
113
 
105
- console.log(chalk.green.bold(`\n✨ Service ${service.name} deployed successfully!`))
114
+ console.log(chalk.green.bold(`✨ Service ${service.name} deployed successfully!`))
106
115
  } catch (error) {
107
116
  console.error(
108
117
  chalk.red(`\nāŒ Failed to deploy service ${service.name}:`),
@@ -112,6 +121,267 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
112
121
  }
113
122
  }
114
123
 
124
+ // Helper function to generate nginx configs with optional port overrides
125
+ const generateNginxConfigsForDomain = (
126
+ domain: string,
127
+ withHttps: boolean,
128
+ portOverrides?: Map<string, number>
129
+ ): string => {
130
+ const servicesForDomain = domainToServices.get(domain)!
131
+ if (servicesForDomain.length === 1) {
132
+ const service = servicesForDomain[0]
133
+ const portOverride = portOverrides?.get(service.name)
134
+ return generateNginxConfig(service, withHttps, portOverride)
135
+ } else {
136
+ return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides)
137
+ }
138
+ }
139
+
140
+ // Check if we need zero-downtime nginx updates (any service has temp container)
141
+ const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(
142
+ (info) => info !== null && info.oldContainerExists
143
+ )
144
+
145
+ // Configure Nginx per domain
146
+ if (options.nginx) {
147
+ // If zero-downtime, first update nginx to point to temp ports
148
+ if (needsZeroDowntimeNginx) {
149
+ console.log(chalk.cyan(`\nāš™ļø Updating Nginx for zero-downtime deployment...`))
150
+
151
+ // Build port override map for temp ports
152
+ const tempPortOverrides = new Map<string, number>()
153
+ for (const service of config.services) {
154
+ const tempInfo = serviceTempInfo.get(service.name)
155
+ if (tempInfo && tempInfo.oldContainerExists) {
156
+ tempPortOverrides.set(service.name, tempInfo.tempPort)
157
+ }
158
+ }
159
+
160
+ for (const domain of allDomains) {
161
+ const configName = domain.replace(/\./g, '_')
162
+ try {
163
+ const nginxConfigContent = generateNginxConfigsForDomain(
164
+ domain,
165
+ false,
166
+ tempPortOverrides
167
+ )
168
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
169
+ await enableSite(configName, config.nginx.configPath)
170
+ console.log(chalk.green(` āœ… Nginx updated for ${domain} (temporary ports)`))
171
+ } catch (error) {
172
+ console.error(
173
+ chalk.red(` āŒ Failed to update Nginx for ${domain}:`),
174
+ error instanceof Error ? error.message : error
175
+ )
176
+ throw error
177
+ }
178
+ }
179
+
180
+ // Reload nginx to switch to temp ports (graceful reload, no connection drops)
181
+ console.log(chalk.cyan(`\nšŸ”„ Reloading Nginx to switch to new containers...`))
182
+ await reloadNginx(config.nginx.reloadCommand)
183
+ console.log(chalk.green(` āœ… Nginx reloaded, traffic now routed to new containers`))
184
+
185
+ // Now swap containers (stop old, promote new)
186
+ console.log(chalk.cyan(`\nšŸ”„ Swapping containers for zero-downtime...`))
187
+ for (const service of config.services) {
188
+ const tempInfo = serviceTempInfo.get(service.name)
189
+ if (tempInfo && tempInfo.oldContainerExists && service.docker) {
190
+ try {
191
+ await swapContainersForZeroDowntime(service, tempInfo)
192
+ console.log(chalk.green(` āœ… Container swapped for ${service.name}`))
193
+ } catch (error) {
194
+ console.error(
195
+ chalk.red(` āŒ Failed to swap container for ${service.name}:`),
196
+ error instanceof Error ? error.message : error
197
+ )
198
+ throw error
199
+ }
200
+ }
201
+ }
202
+
203
+ // Update nginx back to original ports (before stopping temp containers)
204
+ console.log(chalk.cyan(`\nāš™ļø Updating Nginx back to production ports...`))
205
+ for (const domain of allDomains) {
206
+ const configName = domain.replace(/\./g, '_')
207
+ try {
208
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false)
209
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
210
+ await enableSite(configName, config.nginx.configPath)
211
+ console.log(chalk.green(` āœ… Nginx updated for ${domain} (production ports)`))
212
+ } catch (error) {
213
+ console.error(
214
+ chalk.red(` āŒ Failed to update Nginx for ${domain}:`),
215
+ error instanceof Error ? error.message : error
216
+ )
217
+ throw error
218
+ }
219
+ }
220
+
221
+ // Reload nginx to switch to production ports (graceful reload)
222
+ console.log(chalk.cyan(`\nšŸ”„ Reloading Nginx to switch to production ports...`))
223
+ await reloadNginx(config.nginx.reloadCommand)
224
+ console.log(chalk.green(` āœ… Nginx reloaded, traffic now routed to production containers`))
225
+
226
+ // Clean up temp containers (nginx already pointing to production, so safe to remove)
227
+ console.log(chalk.cyan(`\n🧹 Cleaning up temporary containers...`))
228
+ for (const service of config.services) {
229
+ const tempInfo = serviceTempInfo.get(service.name)
230
+ if (tempInfo && tempInfo.oldContainerExists) {
231
+ await cleanupTempContainer(tempInfo.tempContainerName)
232
+ }
233
+ }
234
+ } else {
235
+ // Regular nginx configuration (no zero-downtime needed)
236
+ console.log(chalk.cyan(`\nāš™ļø Configuring Nginx reverse proxy...`))
237
+
238
+ for (const domain of allDomains) {
239
+ const servicesForDomain = domainToServices.get(domain)!
240
+ const configName = domain.replace(/\./g, '_')
241
+
242
+ try {
243
+ // Log domain and services configuration
244
+ if (servicesForDomain.length > 1) {
245
+ console.log(
246
+ chalk.cyan(
247
+ ` šŸ“‹ Configuring ${domain} with ${
248
+ servicesForDomain.length
249
+ } services: ${servicesForDomain.map((s) => s.name).join(', ')}`
250
+ )
251
+ )
252
+ console.log(
253
+ chalk.dim(
254
+ ` All services will share the same nginx config file: ${configName}.conf`
255
+ )
256
+ )
257
+ }
258
+
259
+ // Generate Nginx config
260
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, false)
261
+
262
+ // Check if config file already exists and write/override it
263
+ const wasOverridden = await writeNginxConfig(
264
+ configName,
265
+ config.nginx.configPath,
266
+ nginxConfigContent
267
+ )
268
+
269
+ if (wasOverridden) {
270
+ console.log(
271
+ chalk.yellow(
272
+ ` šŸ”„ Nginx config "${configName}.conf" already exists, deleting and recreating with new configuration...`
273
+ )
274
+ )
275
+ }
276
+
277
+ await enableSite(configName, config.nginx.configPath)
278
+
279
+ console.log(chalk.green(` āœ… Nginx configured for ${domain}`))
280
+ } catch (error) {
281
+ console.error(
282
+ chalk.red(` āŒ Failed to configure Nginx for ${domain}:`),
283
+ error instanceof Error ? error.message : error
284
+ )
285
+ throw error
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ // Setup HTTPS with Certbot (per domain, not per service)
292
+ if (options.https && allDomains.size > 0) {
293
+ console.log(chalk.cyan(`\nšŸ” Setting up HTTPS certificates...`))
294
+
295
+ for (const domain of allDomains) {
296
+ try {
297
+ // Check if certificate already exists
298
+ const exists = await certificateExists(domain)
299
+ if (exists) {
300
+ console.log(
301
+ chalk.green(
302
+ ` āœ… SSL certificate already exists for ${domain}, skipping certificate creation`
303
+ )
304
+ )
305
+ console.log(
306
+ chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)
307
+ )
308
+ } else {
309
+ // Request new certificate
310
+ console.log(chalk.cyan(` šŸ“œ Requesting SSL certificate for ${domain}...`))
311
+ try {
312
+ await requestCertificate(domain, config.certbot.email, config.certbot.staging)
313
+ console.log(chalk.green(` āœ… SSL certificate obtained for ${domain}`))
314
+ } catch (error: any) {
315
+ // Check if error is because certificate already exists (race condition or check missed it)
316
+ const errorMessage = error?.message || String(error) || ''
317
+ if (
318
+ errorMessage.includes('already exists') ||
319
+ errorMessage.includes('Skipping certificate creation')
320
+ ) {
321
+ console.log(
322
+ chalk.green(
323
+ ` āœ… SSL certificate already exists for ${domain} (detected during request), skipping...`
324
+ )
325
+ )
326
+ } else {
327
+ throw error // Re-throw if it's a different error
328
+ }
329
+ }
330
+ }
331
+ } catch (error) {
332
+ console.log(
333
+ chalk.yellow(
334
+ ` āš ļø Failed to obtain SSL for ${domain}: ${
335
+ error instanceof Error ? error.message : error
336
+ }`
337
+ )
338
+ )
339
+ }
340
+ }
341
+
342
+ // Update Nginx configs with HTTPS
343
+ if (options.nginx) {
344
+ console.log(chalk.cyan(`\nšŸ”„ Updating Nginx configs with HTTPS...`))
345
+ for (const domain of allDomains) {
346
+ const configName = domain.replace(/\./g, '_')
347
+
348
+ try {
349
+ const nginxConfigContent = generateNginxConfigsForDomain(domain, true)
350
+ const wasOverridden = await writeNginxConfig(
351
+ configName,
352
+ config.nginx.configPath,
353
+ nginxConfigContent
354
+ )
355
+
356
+ if (wasOverridden) {
357
+ console.log(
358
+ chalk.yellow(
359
+ ` šŸ”„ Nginx config "${configName}.conf" already exists, deleting and recreating with new HTTPS configuration...`
360
+ )
361
+ )
362
+ }
363
+ console.log(chalk.green(` āœ… HTTPS config updated for ${domain}`))
364
+ } catch (error) {
365
+ console.error(
366
+ chalk.red(` āŒ Failed to update HTTPS config for ${domain}:`),
367
+ error instanceof Error ? error.message : error
368
+ )
369
+ throw error
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // Final reload Nginx after all configurations (only if we didn't already reload for zero-downtime)
376
+ if (options.nginx && !needsZeroDowntimeNginx) {
377
+ console.log(chalk.cyan(`\nšŸ”„ Reloading Nginx...`))
378
+ await reloadNginx(config.nginx.reloadCommand)
379
+ } else if (options.nginx && needsZeroDowntimeNginx) {
380
+ // Final reload after HTTPS update
381
+ console.log(chalk.cyan(`\nšŸ”„ Final Nginx reload with HTTPS...`))
382
+ await reloadNginx(config.nginx.reloadCommand)
383
+ }
384
+
115
385
  console.log(chalk.green.bold('\nšŸŽ‰ All services deployed successfully!\n'))
116
386
 
117
387
  // Print service URLs
@@ -119,7 +389,9 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
119
389
  for (const service of config.services) {
120
390
  for (const domain of service.domains) {
121
391
  const protocol = options.https ? 'https' : 'http'
122
- console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}`))
392
+ const servicePath = service.path || '/'
393
+ const fullPath = servicePath === '/' ? '' : servicePath
394
+ console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))
123
395
  }
124
396
  }
125
397
  console.log()