suthep 0.1.0-beta.1 โ†’ 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 (69) hide show
  1. package/README.md +167 -69
  2. package/dist/commands/down.js +179 -0
  3. package/dist/commands/down.js.map +1 -0
  4. package/dist/commands/redeploy.js +59 -0
  5. package/dist/commands/redeploy.js.map +1 -0
  6. package/dist/commands/up.js +213 -0
  7. package/dist/commands/up.js.map +1 -0
  8. package/dist/index.js +28 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/utils/deployment.js +10 -1
  11. package/dist/utils/deployment.js.map +1 -1
  12. package/dist/utils/docker.js +35 -0
  13. package/dist/utils/docker.js.map +1 -1
  14. package/dist/utils/nginx.js +10 -0
  15. package/dist/utils/nginx.js.map +1 -1
  16. package/docs/README.md +25 -82
  17. package/docs/english/01-introduction.md +84 -0
  18. package/docs/english/02-installation.md +200 -0
  19. package/docs/english/03-quick-start.md +256 -0
  20. package/docs/english/04-configuration.md +358 -0
  21. package/docs/english/05-commands.md +363 -0
  22. package/docs/english/06-examples.md +456 -0
  23. package/docs/english/07-troubleshooting.md +417 -0
  24. package/docs/english/08-advanced.md +411 -0
  25. package/docs/english/README.md +48 -0
  26. package/docs/thai/01-introduction.md +84 -0
  27. package/docs/thai/02-installation.md +200 -0
  28. package/docs/thai/03-quick-start.md +256 -0
  29. package/docs/thai/04-configuration.md +358 -0
  30. package/docs/thai/05-commands.md +363 -0
  31. package/docs/thai/06-examples.md +456 -0
  32. package/docs/thai/07-troubleshooting.md +417 -0
  33. package/docs/thai/08-advanced.md +411 -0
  34. package/docs/thai/README.md +48 -0
  35. package/example/README.md +282 -53
  36. package/example/suthep-complete.yml +103 -0
  37. package/example/suthep-docker-only.yml +71 -0
  38. package/example/suthep-no-docker.yml +51 -0
  39. package/example/{muacle.yml โ†’ suthep-path-routing.yml} +20 -5
  40. package/example/suthep.example.yml +89 -0
  41. package/package.json +1 -1
  42. package/src/commands/down.ts +240 -0
  43. package/src/commands/redeploy.ts +78 -0
  44. package/src/commands/up.ts +271 -0
  45. package/src/index.ts +53 -0
  46. package/suthep-0.1.0-beta.1.tgz +0 -0
  47. package/suthep.example.yml +5 -0
  48. package/suthep.yml +39 -0
  49. package/docs/TRANSLATIONS.md +0 -211
  50. package/docs/en/README.md +0 -76
  51. package/docs/en/api-reference.md +0 -545
  52. package/docs/en/architecture.md +0 -369
  53. package/docs/en/commands.md +0 -273
  54. package/docs/en/configuration.md +0 -347
  55. package/docs/en/developer-guide.md +0 -588
  56. package/docs/en/docker-ports-config.md +0 -333
  57. package/docs/en/examples.md +0 -537
  58. package/docs/en/getting-started.md +0 -202
  59. package/docs/en/port-binding.md +0 -268
  60. package/docs/en/troubleshooting.md +0 -441
  61. package/docs/th/README.md +0 -64
  62. package/docs/th/commands.md +0 -202
  63. package/docs/th/configuration.md +0 -325
  64. package/docs/th/getting-started.md +0 -203
  65. package/example/docker-compose.yml +0 -76
  66. package/example/docker-ports-example.yml +0 -81
  67. package/example/port-binding-example.yml +0 -45
  68. package/example/suthep.yml +0 -46
  69. package/example/suthep=1.yml +0 -46
@@ -0,0 +1,213 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs-extra";
3
+ import { loadConfig } from "../utils/config-loader.js";
4
+ import { waitForService } from "../utils/deployment.js";
5
+ import { startDockerContainer, isContainerRunning } from "../utils/docker.js";
6
+ import { generateNginxConfig, generateMultiServiceNginxConfig, writeNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
7
+ async function upCommand(options) {
8
+ console.log(chalk.blue.bold("\n๐Ÿš€ Bringing Up Services\n"));
9
+ try {
10
+ if (!await fs.pathExists(options.file)) {
11
+ throw new Error(`Configuration file not found: ${options.file}`);
12
+ }
13
+ console.log(chalk.cyan(`๐Ÿ“„ Loading configuration from ${options.file}...`));
14
+ const config = await loadConfig(options.file);
15
+ console.log(chalk.green(`โœ… Configuration loaded for project: ${config.project.name}`));
16
+ let servicesToUp = [];
17
+ if (options.all) {
18
+ servicesToUp = config.services;
19
+ console.log(
20
+ chalk.cyan(`๐Ÿ“‹ Bringing up all services: ${servicesToUp.map((s) => s.name).join(", ")}
21
+ `)
22
+ );
23
+ } else if (options.serviceName) {
24
+ const service = config.services.find((s) => s.name === options.serviceName);
25
+ if (!service) {
26
+ throw new Error(
27
+ `Service "${options.serviceName}" not found in configuration. Available services: ${config.services.map((s) => s.name).join(", ")}`
28
+ );
29
+ }
30
+ servicesToUp = [service];
31
+ console.log(chalk.cyan(`๐Ÿ“‹ Bringing up service: ${options.serviceName}
32
+ `));
33
+ } else {
34
+ throw new Error("Either specify a service name or use --all flag");
35
+ }
36
+ const domainToServices = /* @__PURE__ */ new Map();
37
+ const allDomains = /* @__PURE__ */ new Set();
38
+ for (const service of servicesToUp) {
39
+ for (const domain of service.domains) {
40
+ allDomains.add(domain);
41
+ if (!domainToServices.has(domain)) {
42
+ domainToServices.set(domain, []);
43
+ }
44
+ domainToServices.get(domain).push(service);
45
+ }
46
+ }
47
+ for (const service of servicesToUp) {
48
+ if (service.docker) {
49
+ console.log(chalk.cyan(`
50
+ ๐Ÿณ Starting Docker container for service: ${service.name}`));
51
+ try {
52
+ await startDockerContainer(service);
53
+ console.log(chalk.green(` โœ… Container started for service: ${service.name}`));
54
+ } catch (error) {
55
+ console.error(
56
+ chalk.red(` โŒ Failed to start container for service ${service.name}:`),
57
+ error instanceof Error ? error.message : error
58
+ );
59
+ throw error;
60
+ }
61
+ }
62
+ }
63
+ for (const service of servicesToUp) {
64
+ if (service.healthCheck) {
65
+ console.log(chalk.cyan(`
66
+ ๐Ÿฅ Waiting for service ${service.name} to be healthy...`));
67
+ const isHealthy = await waitForService(service, config.deployment.healthCheckTimeout);
68
+ if (isHealthy) {
69
+ console.log(chalk.green(` โœ… Service ${service.name} is healthy`));
70
+ } else {
71
+ console.log(
72
+ chalk.yellow(` โš ๏ธ Service ${service.name} health check timeout, continuing anyway...`)
73
+ );
74
+ }
75
+ }
76
+ }
77
+ if (options.nginx && allDomains.size > 0) {
78
+ console.log(chalk.cyan(`
79
+ โš™๏ธ Configuring Nginx reverse proxy...`));
80
+ const servicesBeingUpped = new Set(servicesToUp.map((s) => s.name));
81
+ for (const domain of allDomains) {
82
+ const configName = domain.replace(/\./g, "_");
83
+ const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain));
84
+ const activeServices = [];
85
+ for (const service of allServicesForDomain) {
86
+ if (service.docker) {
87
+ const isRunning = await isContainerRunning(service.docker.container);
88
+ if (isRunning) {
89
+ activeServices.push(service);
90
+ }
91
+ } else {
92
+ if (servicesBeingUpped.has(service.name)) {
93
+ activeServices.push(service);
94
+ } else {
95
+ activeServices.push(service);
96
+ }
97
+ }
98
+ }
99
+ if (activeServices.length === 0) {
100
+ console.log(
101
+ chalk.yellow(` โš ๏ธ No active services found for ${domain}, skipping Nginx config`)
102
+ );
103
+ continue;
104
+ }
105
+ try {
106
+ if (activeServices.length > 1) {
107
+ console.log(
108
+ chalk.cyan(
109
+ ` ๐Ÿ“‹ Configuring ${domain} with ${activeServices.length} active service(s): ${activeServices.map((s) => s.name).join(", ")}`
110
+ )
111
+ );
112
+ } else {
113
+ console.log(
114
+ chalk.cyan(` ๐Ÿ“‹ Configuring ${domain} for service: ${activeServices[0].name}`)
115
+ );
116
+ }
117
+ let nginxConfigContent;
118
+ if (activeServices.length === 1) {
119
+ nginxConfigContent = generateNginxConfig(activeServices[0], false);
120
+ } else {
121
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false);
122
+ }
123
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
124
+ await enableSite(configName, config.nginx.configPath);
125
+ console.log(chalk.green(` โœ… Nginx configured for ${domain}`));
126
+ } catch (error) {
127
+ console.error(
128
+ chalk.red(` โŒ Failed to configure Nginx for ${domain}:`),
129
+ error instanceof Error ? error.message : error
130
+ );
131
+ throw error;
132
+ }
133
+ }
134
+ if (options.https) {
135
+ console.log(chalk.cyan(`
136
+ ๐Ÿ”„ Updating Nginx configs with HTTPS...`));
137
+ for (const domain of allDomains) {
138
+ const configName = domain.replace(/\./g, "_");
139
+ const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain));
140
+ const activeServices = [];
141
+ for (const service of allServicesForDomain) {
142
+ if (service.docker) {
143
+ const isRunning = await isContainerRunning(service.docker.container);
144
+ if (isRunning) {
145
+ activeServices.push(service);
146
+ }
147
+ } else {
148
+ if (servicesBeingUpped.has(service.name)) {
149
+ activeServices.push(service);
150
+ } else {
151
+ activeServices.push(service);
152
+ }
153
+ }
154
+ }
155
+ if (activeServices.length === 0) {
156
+ continue;
157
+ }
158
+ try {
159
+ let nginxConfigContent;
160
+ if (activeServices.length === 1) {
161
+ nginxConfigContent = generateNginxConfig(activeServices[0], true);
162
+ } else {
163
+ nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true);
164
+ }
165
+ await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
166
+ console.log(chalk.green(` โœ… HTTPS config updated for ${domain}`));
167
+ } catch (error) {
168
+ console.error(
169
+ chalk.red(` โŒ Failed to update HTTPS config for ${domain}:`),
170
+ error instanceof Error ? error.message : error
171
+ );
172
+ throw error;
173
+ }
174
+ }
175
+ }
176
+ console.log(chalk.cyan(`
177
+ ๐Ÿ”„ Reloading Nginx...`));
178
+ try {
179
+ await reloadNginx(config.nginx.reloadCommand);
180
+ console.log(chalk.green(` โœ… Nginx reloaded`));
181
+ } catch (error) {
182
+ console.error(
183
+ chalk.red(` โŒ Failed to reload Nginx:`),
184
+ error instanceof Error ? error.message : error
185
+ );
186
+ throw error;
187
+ }
188
+ }
189
+ console.log(chalk.green.bold("\nโœ… Services brought up successfully!\n"));
190
+ if (allDomains.size > 0) {
191
+ console.log(chalk.cyan("๐Ÿ“‹ Service URLs:"));
192
+ for (const service of servicesToUp) {
193
+ for (const domain of service.domains) {
194
+ const protocol = options.https ? "https" : "http";
195
+ const servicePath = service.path || "/";
196
+ const fullPath = servicePath === "/" ? "" : servicePath;
197
+ console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`));
198
+ }
199
+ }
200
+ console.log();
201
+ }
202
+ } catch (error) {
203
+ console.error(
204
+ chalk.red("\nโŒ Failed to bring up services:"),
205
+ error instanceof Error ? error.message : error
206
+ );
207
+ process.exit(1);
208
+ }
209
+ }
210
+ export {
211
+ upCommand
212
+ };
213
+ //# sourceMappingURL=up.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"up.js","sources":["../../src/commands/up.ts"],"sourcesContent":["import chalk from 'chalk'\nimport fs from 'fs-extra'\nimport type { ServiceConfig } from '../types/config'\nimport { loadConfig } from '../utils/config-loader'\nimport { waitForService } from '../utils/deployment'\nimport { isContainerRunning, startDockerContainer } from '../utils/docker'\nimport {\n enableSite,\n generateMultiServiceNginxConfig,\n generateNginxConfig,\n reloadNginx,\n writeNginxConfig,\n} from '../utils/nginx'\n\ninterface UpOptions {\n file: string\n all: boolean\n serviceName?: string\n https: boolean\n nginx: boolean\n}\n\nexport async function upCommand(options: UpOptions): Promise<void> {\n console.log(chalk.blue.bold('\\n๐Ÿš€ Bringing Up Services\\n'))\n\n try {\n // Load configuration\n if (!(await fs.pathExists(options.file))) {\n throw new Error(`Configuration file not found: ${options.file}`)\n }\n\n console.log(chalk.cyan(`๐Ÿ“„ Loading configuration from ${options.file}...`))\n const config = await loadConfig(options.file)\n\n console.log(chalk.green(`โœ… Configuration loaded for project: ${config.project.name}`))\n\n // Determine which services to bring up\n let servicesToUp: ServiceConfig[] = []\n\n if (options.all) {\n servicesToUp = config.services\n console.log(\n chalk.cyan(`๐Ÿ“‹ Bringing up all services: ${servicesToUp.map((s) => s.name).join(', ')}\\n`)\n )\n } else if (options.serviceName) {\n const service = config.services.find((s) => s.name === options.serviceName)\n if (!service) {\n throw new Error(\n `Service \"${\n options.serviceName\n }\" not found in configuration. Available services: ${config.services\n .map((s) => s.name)\n .join(', ')}`\n )\n }\n servicesToUp = [service]\n console.log(chalk.cyan(`๐Ÿ“‹ Bringing up service: ${options.serviceName}\\n`))\n } else {\n throw new Error('Either specify a service name or use --all flag')\n }\n\n // Group services by domain for nginx config management\n const domainToServices = new Map<string, ServiceConfig[]>()\n const allDomains = new Set<string>()\n\n for (const service of servicesToUp) {\n for (const domain of service.domains) {\n allDomains.add(domain)\n if (!domainToServices.has(domain)) {\n domainToServices.set(domain, [])\n }\n domainToServices.get(domain)!.push(service)\n }\n }\n\n // Start Docker containers\n for (const service of servicesToUp) {\n if (service.docker) {\n console.log(chalk.cyan(`\\n๐Ÿณ Starting Docker container for service: ${service.name}`))\n try {\n await startDockerContainer(service)\n console.log(chalk.green(` โœ… Container started for service: ${service.name}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to start container for service ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n\n // Wait for services to be healthy\n for (const service of servicesToUp) {\n if (service.healthCheck) {\n console.log(chalk.cyan(`\\n๐Ÿฅ Waiting for service ${service.name} to be healthy...`))\n const isHealthy = await waitForService(service, config.deployment.healthCheckTimeout)\n if (isHealthy) {\n console.log(chalk.green(` โœ… Service ${service.name} is healthy`))\n } else {\n console.log(\n chalk.yellow(` โš ๏ธ Service ${service.name} health check timeout, continuing anyway...`)\n )\n }\n }\n }\n\n // Configure Nginx\n if (options.nginx && allDomains.size > 0) {\n console.log(chalk.cyan(`\\nโš™๏ธ Configuring Nginx reverse proxy...`))\n\n // Create a set of service names being brought up for quick lookup\n const servicesBeingUpped = new Set(servicesToUp.map((s) => s.name))\n\n // For each domain, find all services that use it (from all config services)\n // and include all active services (both newly brought up and already running)\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n\n // Find all services that use this domain (from entire config)\n const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))\n\n // Check which services are actually running (have active containers)\n const activeServices: ServiceConfig[] = []\n for (const service of allServicesForDomain) {\n if (service.docker) {\n const isRunning = await isContainerRunning(service.docker.container)\n if (isRunning) {\n activeServices.push(service)\n }\n } else {\n // Service without docker - include if it's being brought up or assume it's running\n if (servicesBeingUpped.has(service.name)) {\n activeServices.push(service)\n } else {\n // For non-docker services, assume they're running if not explicitly being brought up\n // This is a best-effort approach\n activeServices.push(service)\n }\n }\n }\n\n if (activeServices.length === 0) {\n console.log(\n chalk.yellow(` โš ๏ธ No active services found for ${domain}, skipping Nginx config`)\n )\n continue\n }\n\n try {\n // Log domain and services configuration\n if (activeServices.length > 1) {\n console.log(\n chalk.cyan(\n ` ๐Ÿ“‹ Configuring ${domain} with ${\n activeServices.length\n } active service(s): ${activeServices.map((s) => s.name).join(', ')}`\n )\n )\n } else {\n console.log(\n chalk.cyan(` ๐Ÿ“‹ Configuring ${domain} for service: ${activeServices[0].name}`)\n )\n }\n\n // Generate Nginx config for all active services on this domain\n let nginxConfigContent: string\n if (activeServices.length === 1) {\n nginxConfigContent = generateNginxConfig(activeServices[0], false)\n } else {\n nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)\n }\n\n // Write config file\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n await enableSite(configName, config.nginx.configPath)\n\n console.log(chalk.green(` โœ… Nginx configured for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to configure Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Update with HTTPS if enabled\n if (options.https) {\n console.log(chalk.cyan(`\\n๐Ÿ”„ Updating Nginx configs with HTTPS...`))\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n\n // Find all active services for this domain again\n const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))\n const activeServices: ServiceConfig[] = []\n for (const service of allServicesForDomain) {\n if (service.docker) {\n const isRunning = await isContainerRunning(service.docker.container)\n if (isRunning) {\n activeServices.push(service)\n }\n } else {\n if (servicesBeingUpped.has(service.name)) {\n activeServices.push(service)\n } else {\n activeServices.push(service)\n }\n }\n }\n\n if (activeServices.length === 0) {\n continue\n }\n\n try {\n let nginxConfigContent: string\n if (activeServices.length === 1) {\n nginxConfigContent = generateNginxConfig(activeServices[0], true)\n } else {\n nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)\n }\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n console.log(chalk.green(` โœ… HTTPS config updated for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to update HTTPS config for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n\n // Reload Nginx\n console.log(chalk.cyan(`\\n๐Ÿ”„ Reloading Nginx...`))\n try {\n await reloadNginx(config.nginx.reloadCommand)\n console.log(chalk.green(` โœ… Nginx reloaded`))\n } catch (error) {\n console.error(\n chalk.red(` โŒ Failed to reload Nginx:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n console.log(chalk.green.bold('\\nโœ… Services brought up successfully!\\n'))\n\n // Print service URLs\n if (allDomains.size > 0) {\n console.log(chalk.cyan('๐Ÿ“‹ Service URLs:'))\n for (const service of servicesToUp) {\n for (const domain of service.domains) {\n const protocol = options.https ? 'https' : 'http'\n const servicePath = service.path || '/'\n const fullPath = servicePath === '/' ? '' : servicePath\n console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))\n }\n }\n console.log()\n }\n } catch (error) {\n console.error(\n chalk.red('\\nโŒ Failed to bring up services:'),\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,8 +4,11 @@ import { readFileSync } from "fs";
4
4
  import { dirname, join } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { deployCommand } from "./commands/deploy.js";
7
+ import { downCommand } from "./commands/down.js";
7
8
  import { initCommand } from "./commands/init.js";
9
+ import { redeployCommand } from "./commands/redeploy.js";
8
10
  import { setupCommand } from "./commands/setup.js";
11
+ import { upCommand } from "./commands/up.js";
9
12
  const __filename$1 = fileURLToPath(import.meta.url);
10
13
  const __dirname$1 = dirname(__filename$1);
11
14
  const packageJsonPath = join(__dirname$1, "..", "package.json");
@@ -15,5 +18,30 @@ program.name("suthep").description("CLI tool for deploying projects with automat
15
18
  program.command("init").description("Initialize a new deployment configuration file").option("-f, --file <path>", "Configuration file path", "suthep.yml").action(initCommand);
16
19
  program.command("setup").description("Setup Nginx and Certbot on the system").option("--nginx-only", "Only setup Nginx").option("--certbot-only", "Only setup Certbot").action(setupCommand);
17
20
  program.command("deploy").description("Deploy a project using the configuration file").option("-f, --file <path>", "Configuration file path", "suthep.yml").option("--no-https", "Skip HTTPS setup").option("--no-nginx", "Skip Nginx configuration").action(deployCommand);
21
+ program.command("down").description("Bring down services (stop containers and disable Nginx configs)").option("-f, --file <path>", "Configuration file path", "suthep.yml").option("--all", "Bring down all services", false).argument("[service-name]", "Name of the service to bring down").action((serviceName, options) => {
22
+ downCommand({
23
+ file: options.file || "suthep.yml",
24
+ all: options.all || false,
25
+ serviceName
26
+ });
27
+ });
28
+ program.command("up").description("Bring up services (start containers and enable Nginx configs)").option("-f, --file <path>", "Configuration file path", "suthep.yml").option("--all", "Bring up all services", false).option("--no-https", "Skip HTTPS setup").option("--no-nginx", "Skip Nginx configuration").argument("[service-name]", "Name of the service to bring up").action((serviceName, options) => {
29
+ upCommand({
30
+ file: options.file || "suthep.yml",
31
+ all: options.all || false,
32
+ serviceName,
33
+ https: options.https !== false,
34
+ nginx: options.nginx !== false
35
+ });
36
+ });
37
+ program.command("redeploy").description("Redeploy services (bring down and deploy again)").option("-f, --file <path>", "Configuration file path", "suthep.yml").option("--all", "Redeploy all services", false).option("--no-https", "Skip HTTPS setup").option("--no-nginx", "Skip Nginx configuration").argument("[service-name]", "Name of the service to redeploy").action((serviceName, options) => {
38
+ redeployCommand({
39
+ file: options.file || "suthep.yml",
40
+ all: options.all || false,
41
+ serviceName,
42
+ https: options.https !== false,
43
+ nginx: options.nginx !== false
44
+ });
45
+ });
18
46
  program.parse();
19
47
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { readFileSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\nimport { deployCommand } from './commands/deploy'\nimport { initCommand } from './commands/init'\nimport { setupCommand } from './commands/setup'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst packageJsonPath = join(__dirname, '..', 'package.json')\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n\nconst program = new Command()\n\nprogram\n .name('suthep')\n .description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')\n .version(packageJson.version)\n\nprogram\n .command('init')\n .description('Initialize a new deployment configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .action(initCommand)\n\nprogram\n .command('setup')\n .description('Setup Nginx and Certbot on the system')\n .option('--nginx-only', 'Only setup Nginx')\n .option('--certbot-only', 'Only setup Certbot')\n .action(setupCommand)\n\nprogram\n .command('deploy')\n .description('Deploy a project using the configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--no-https', 'Skip HTTPS setup')\n .option('--no-nginx', 'Skip Nginx configuration')\n .action(deployCommand)\n\nprogram.parse()\n"],"names":["__filename","__dirname"],"mappings":";;;;;;;AAQA,MAAMA,eAAa,cAAc,YAAY,GAAG;AAChD,MAAMC,cAAY,QAAQD,YAAU;AACpC,MAAM,kBAAkB,KAAKC,aAAW,MAAM,cAAc;AAC5D,MAAM,cAAc,KAAK,MAAM,aAAa,iBAAiB,OAAO,CAAC;AAErE,MAAM,UAAU,IAAI,QAAA;AAEpB,QACG,KAAK,QAAQ,EACb,YAAY,oFAAoF,EAChG,QAAQ,YAAY,OAAO;AAE9B,QACG,QAAQ,MAAM,EACd,YAAY,gDAAgD,EAC5D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,WAAW;AAErB,QACG,QAAQ,OAAO,EACf,YAAY,uCAAuC,EACnD,OAAO,gBAAgB,kBAAkB,EACzC,OAAO,kBAAkB,oBAAoB,EAC7C,OAAO,YAAY;AAEtB,QACG,QAAQ,QAAQ,EAChB,YAAY,+CAA+C,EAC3D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,cAAc,kBAAkB,EACvC,OAAO,cAAc,0BAA0B,EAC/C,OAAO,aAAa;AAEvB,QAAQ,MAAA;"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { readFileSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\nimport { deployCommand } from './commands/deploy'\nimport { downCommand } from './commands/down'\nimport { initCommand } from './commands/init'\nimport { redeployCommand } from './commands/redeploy'\nimport { setupCommand } from './commands/setup'\nimport { upCommand } from './commands/up'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst packageJsonPath = join(__dirname, '..', 'package.json')\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n\nconst program = new Command()\n\nprogram\n .name('suthep')\n .description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')\n .version(packageJson.version)\n\nprogram\n .command('init')\n .description('Initialize a new deployment configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .action(initCommand)\n\nprogram\n .command('setup')\n .description('Setup Nginx and Certbot on the system')\n .option('--nginx-only', 'Only setup Nginx')\n .option('--certbot-only', 'Only setup Certbot')\n .action(setupCommand)\n\nprogram\n .command('deploy')\n .description('Deploy a project using the configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--no-https', 'Skip HTTPS setup')\n .option('--no-nginx', 'Skip Nginx configuration')\n .action(deployCommand)\n\nprogram\n .command('down')\n .description('Bring down services (stop containers and disable Nginx configs)')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--all', 'Bring down all services', false)\n .argument('[service-name]', 'Name of the service to bring down')\n .action((serviceName, options) => {\n downCommand({\n file: options.file || 'suthep.yml',\n all: options.all || false,\n serviceName: serviceName,\n })\n })\n\nprogram\n .command('up')\n .description('Bring up services (start containers and enable Nginx configs)')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--all', 'Bring up all services', false)\n .option('--no-https', 'Skip HTTPS setup')\n .option('--no-nginx', 'Skip Nginx configuration')\n .argument('[service-name]', 'Name of the service to bring up')\n .action((serviceName, options) => {\n upCommand({\n file: options.file || 'suthep.yml',\n all: options.all || false,\n serviceName: serviceName,\n https: options.https !== false,\n nginx: options.nginx !== false,\n })\n })\n\nprogram\n .command('redeploy')\n .description('Redeploy services (bring down and deploy again)')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--all', 'Redeploy all services', false)\n .option('--no-https', 'Skip HTTPS setup')\n .option('--no-nginx', 'Skip Nginx configuration')\n .argument('[service-name]', 'Name of the service to redeploy')\n .action((serviceName, options) => {\n redeployCommand({\n file: options.file || 'suthep.yml',\n all: options.all || false,\n serviceName: serviceName,\n https: options.https !== false,\n nginx: options.nginx !== false,\n })\n })\n\nprogram.parse()\n"],"names":["__filename","__dirname"],"mappings":";;;;;;;;;;AAWA,MAAMA,eAAa,cAAc,YAAY,GAAG;AAChD,MAAMC,cAAY,QAAQD,YAAU;AACpC,MAAM,kBAAkB,KAAKC,aAAW,MAAM,cAAc;AAC5D,MAAM,cAAc,KAAK,MAAM,aAAa,iBAAiB,OAAO,CAAC;AAErE,MAAM,UAAU,IAAI,QAAA;AAEpB,QACG,KAAK,QAAQ,EACb,YAAY,oFAAoF,EAChG,QAAQ,YAAY,OAAO;AAE9B,QACG,QAAQ,MAAM,EACd,YAAY,gDAAgD,EAC5D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,WAAW;AAErB,QACG,QAAQ,OAAO,EACf,YAAY,uCAAuC,EACnD,OAAO,gBAAgB,kBAAkB,EACzC,OAAO,kBAAkB,oBAAoB,EAC7C,OAAO,YAAY;AAEtB,QACG,QAAQ,QAAQ,EAChB,YAAY,+CAA+C,EAC3D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,cAAc,kBAAkB,EACvC,OAAO,cAAc,0BAA0B,EAC/C,OAAO,aAAa;AAEvB,QACG,QAAQ,MAAM,EACd,YAAY,iEAAiE,EAC7E,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,SAAS,2BAA2B,KAAK,EAChD,SAAS,kBAAkB,mCAAmC,EAC9D,OAAO,CAAC,aAAa,YAAY;AAChC,cAAY;AAAA,IACV,MAAM,QAAQ,QAAQ;AAAA,IACtB,KAAK,QAAQ,OAAO;AAAA,IACpB;AAAA,EAAA,CACD;AACH,CAAC;AAEH,QACG,QAAQ,IAAI,EACZ,YAAY,+DAA+D,EAC3E,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,SAAS,yBAAyB,KAAK,EAC9C,OAAO,cAAc,kBAAkB,EACvC,OAAO,cAAc,0BAA0B,EAC/C,SAAS,kBAAkB,iCAAiC,EAC5D,OAAO,CAAC,aAAa,YAAY;AAChC,YAAU;AAAA,IACR,MAAM,QAAQ,QAAQ;AAAA,IACtB,KAAK,QAAQ,OAAO;AAAA,IACpB;AAAA,IACA,OAAO,QAAQ,UAAU;AAAA,IACzB,OAAO,QAAQ,UAAU;AAAA,EAAA,CAC1B;AACH,CAAC;AAEH,QACG,QAAQ,UAAU,EAClB,YAAY,iDAAiD,EAC7D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,SAAS,yBAAyB,KAAK,EAC9C,OAAO,cAAc,kBAAkB,EACvC,OAAO,cAAc,0BAA0B,EAC/C,SAAS,kBAAkB,iCAAiC,EAC5D,OAAO,CAAC,aAAa,YAAY;AAChC,kBAAgB;AAAA,IACd,MAAM,QAAQ,QAAQ;AAAA,IACtB,KAAK,QAAQ,OAAO;AAAA,IACpB;AAAA,IACA,OAAO,QAAQ,UAAU;AAAA,IACzB,OAAO,QAAQ,UAAU;AAAA,EAAA,CAC1B;AACH,CAAC;AAEH,QAAQ,MAAA;"}
@@ -69,8 +69,17 @@ async function blueGreenDeploy(service, deploymentConfig, tempInfo) {
69
69
  }
70
70
  }
71
71
  }
72
+ async function waitForService(service, timeout = 6e4) {
73
+ if (!service.healthCheck) {
74
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
75
+ return true;
76
+ }
77
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`;
78
+ return await performHealthCheck(healthUrl, timeout);
79
+ }
72
80
  export {
73
81
  deployService,
74
- performHealthCheck
82
+ performHealthCheck,
83
+ waitForService
75
84
  };
76
85
  //# sourceMappingURL=deployment.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deployment.js","sources":["../../src/utils/deployment.ts"],"sourcesContent":["import type { DeploymentConfig, ServiceConfig } from '../types/config'\nimport type { ZeroDowntimeContainerInfo } from './docker'\n\n/**\n * Perform a health check on a service endpoint\n */\nexport async function performHealthCheck(url: string, timeout: number = 30000): Promise<boolean> {\n const startTime = Date.now()\n const interval = 2000 // Check every 2 seconds\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await fetch(url, {\n method: 'GET',\n signal: AbortSignal.timeout(5000), // 5 second timeout per request\n })\n\n if (response.ok) {\n return true\n }\n } catch (error) {\n // Endpoint not ready yet, continue waiting\n }\n\n // Wait before next check\n await new Promise((resolve) => setTimeout(resolve, interval))\n }\n\n return false\n}\n\n/**\n * Deploy a service with zero-downtime strategy\n */\nexport async function deployService(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null = null\n): Promise<void> {\n if (deploymentConfig.strategy === 'rolling') {\n await rollingDeploy(service, deploymentConfig, tempInfo)\n } else if (deploymentConfig.strategy === 'blue-green') {\n await blueGreenDeploy(service, deploymentConfig, tempInfo)\n } else {\n throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)\n }\n}\n\n/**\n * Rolling deployment strategy\n * For single instance, uses zero-downtime approach similar to blue-green\n */\nasync function rollingDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For rolling deployment with single instance:\n // Similar to blue-green - use temporary container and port\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during rolling deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during rolling deployment`\n )\n }\n }\n }\n\n // Add a small delay to ensure service is fully ready\n await new Promise((resolve) => setTimeout(resolve, 2000))\n}\n\n/**\n * Blue-green deployment strategy for single instance\n * Uses temporary container and port for zero-downtime deployment\n */\nasync function blueGreenDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For blue-green deployment with single instance:\n // 1. New container is already started on temporary port (handled in deploy command)\n // 2. Run health checks on new container\n // 3. Switch nginx to new port (handled in deploy command)\n // 4. Stop old container and promote new one (handled in deploy command)\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during blue-green deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during blue-green deployment`\n )\n }\n }\n }\n}\n\n/**\n * Wait for a service to become healthy\n */\nexport async function waitForService(\n service: ServiceConfig,\n timeout: number = 60000\n): Promise<boolean> {\n if (!service.healthCheck) {\n // No health check configured, assume service is ready after a short delay\n await new Promise((resolve) => setTimeout(resolve, 5000))\n return true\n }\n\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n return await performHealthCheck(healthUrl, timeout)\n}\n\n/**\n * Gracefully shutdown a service\n */\nexport async function gracefulShutdown(\n _service: ServiceConfig,\n timeout: number = 30000\n): Promise<void> {\n // Send shutdown signal and wait for graceful termination\n // This is a placeholder - actual implementation would depend on how services are managed\n\n await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 5000)))\n}\n"],"names":[],"mappings":"AAMA,eAAsB,mBAAmB,KAAa,UAAkB,KAAyB;AAC/F,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,WAAW;AAEjB,SAAO,KAAK,QAAQ,YAAY,SAAS;AACvC,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,QAAQ;AAAA,QACR,QAAQ,YAAY,QAAQ,GAAI;AAAA;AAAA,MAAA,CACjC;AAED,UAAI,SAAS,IAAI;AACf,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAGA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,QAAQ,CAAC;AAAA,EAC9D;AAEA,SAAO;AACT;AAKA,eAAsB,cACpB,SACA,kBACA,WAA6C,MAC9B;AACf,MAAI,iBAAiB,aAAa,WAAW;AAC3C,UAAM,cAAc,SAAS,kBAAkB,QAAQ;AAAA,EACzD,WAAW,iBAAiB,aAAa,cAAc;AACrD,UAAM,gBAAgB,SAAS,kBAAkB,QAAQ;AAAA,EAC3D,OAAO;AACL,UAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,EAAE;AAAA,EAC7E;AACF;AAMA,eAAe,cACb,SACA,kBACA,UACe;AAIf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,gDAAgD;AAAA,MACzF;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AAGA,QAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAI,CAAC;AAC1D;AAMA,eAAe,gBACb,SACA,kBACA,UACe;AAOf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,mDAAmD;AAAA,MAC5F;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AACF;"}
1
+ {"version":3,"file":"deployment.js","sources":["../../src/utils/deployment.ts"],"sourcesContent":["import type { DeploymentConfig, ServiceConfig } from '../types/config'\nimport type { ZeroDowntimeContainerInfo } from './docker'\n\n/**\n * Perform a health check on a service endpoint\n */\nexport async function performHealthCheck(url: string, timeout: number = 30000): Promise<boolean> {\n const startTime = Date.now()\n const interval = 2000 // Check every 2 seconds\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await fetch(url, {\n method: 'GET',\n signal: AbortSignal.timeout(5000), // 5 second timeout per request\n })\n\n if (response.ok) {\n return true\n }\n } catch (error) {\n // Endpoint not ready yet, continue waiting\n }\n\n // Wait before next check\n await new Promise((resolve) => setTimeout(resolve, interval))\n }\n\n return false\n}\n\n/**\n * Deploy a service with zero-downtime strategy\n */\nexport async function deployService(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null = null\n): Promise<void> {\n if (deploymentConfig.strategy === 'rolling') {\n await rollingDeploy(service, deploymentConfig, tempInfo)\n } else if (deploymentConfig.strategy === 'blue-green') {\n await blueGreenDeploy(service, deploymentConfig, tempInfo)\n } else {\n throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)\n }\n}\n\n/**\n * Rolling deployment strategy\n * For single instance, uses zero-downtime approach similar to blue-green\n */\nasync function rollingDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For rolling deployment with single instance:\n // Similar to blue-green - use temporary container and port\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during rolling deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during rolling deployment`\n )\n }\n }\n }\n\n // Add a small delay to ensure service is fully ready\n await new Promise((resolve) => setTimeout(resolve, 2000))\n}\n\n/**\n * Blue-green deployment strategy for single instance\n * Uses temporary container and port for zero-downtime deployment\n */\nasync function blueGreenDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For blue-green deployment with single instance:\n // 1. New container is already started on temporary port (handled in deploy command)\n // 2. Run health checks on new container\n // 3. Switch nginx to new port (handled in deploy command)\n // 4. Stop old container and promote new one (handled in deploy command)\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during blue-green deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during blue-green deployment`\n )\n }\n }\n }\n}\n\n/**\n * Wait for a service to become healthy\n */\nexport async function waitForService(\n service: ServiceConfig,\n timeout: number = 60000\n): Promise<boolean> {\n if (!service.healthCheck) {\n // No health check configured, assume service is ready after a short delay\n await new Promise((resolve) => setTimeout(resolve, 5000))\n return true\n }\n\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n return await performHealthCheck(healthUrl, timeout)\n}\n\n/**\n * Gracefully shutdown a service\n */\nexport async function gracefulShutdown(\n _service: ServiceConfig,\n timeout: number = 30000\n): Promise<void> {\n // Send shutdown signal and wait for graceful termination\n // This is a placeholder - actual implementation would depend on how services are managed\n\n await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 5000)))\n}\n"],"names":[],"mappings":"AAMA,eAAsB,mBAAmB,KAAa,UAAkB,KAAyB;AAC/F,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,WAAW;AAEjB,SAAO,KAAK,QAAQ,YAAY,SAAS;AACvC,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,QAAQ;AAAA,QACR,QAAQ,YAAY,QAAQ,GAAI;AAAA;AAAA,MAAA,CACjC;AAED,UAAI,SAAS,IAAI;AACf,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAGA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,QAAQ,CAAC;AAAA,EAC9D;AAEA,SAAO;AACT;AAKA,eAAsB,cACpB,SACA,kBACA,WAA6C,MAC9B;AACf,MAAI,iBAAiB,aAAa,WAAW;AAC3C,UAAM,cAAc,SAAS,kBAAkB,QAAQ;AAAA,EACzD,WAAW,iBAAiB,aAAa,cAAc;AACrD,UAAM,gBAAgB,SAAS,kBAAkB,QAAQ;AAAA,EAC3D,OAAO;AACL,UAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,EAAE;AAAA,EAC7E;AACF;AAMA,eAAe,cACb,SACA,kBACA,UACe;AAIf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,gDAAgD;AAAA,MACzF;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AAGA,QAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAI,CAAC;AAC1D;AAMA,eAAe,gBACb,SACA,kBACA,UACe;AAOf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,mDAAmD;AAAA,MAC5F;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AACF;AAKA,eAAsB,eACpB,SACA,UAAkB,KACA;AAClB,MAAI,CAAC,QAAQ,aAAa;AAExB,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAI,CAAC;AACxD,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,SAAO,MAAM,mBAAmB,WAAW,OAAO;AACpD;"}
@@ -384,10 +384,45 @@ async function startDockerContainer(service) {
384
384
  );
385
385
  }
386
386
  }
387
+ async function stopDockerContainer(containerName) {
388
+ try {
389
+ await execa("docker", ["stop", containerName]);
390
+ } catch (error) {
391
+ throw new Error(
392
+ `Failed to stop container ${containerName}: ${error instanceof Error ? error.message : error}`
393
+ );
394
+ }
395
+ }
396
+ async function removeDockerContainer(containerName) {
397
+ try {
398
+ await execa("docker", ["rm", "-f", containerName]);
399
+ } catch (error) {
400
+ throw new Error(
401
+ `Failed to remove container ${containerName}: ${error instanceof Error ? error.message : error}`
402
+ );
403
+ }
404
+ }
405
+ async function isContainerRunning(containerName) {
406
+ try {
407
+ const { stdout } = await execa("docker", [
408
+ "ps",
409
+ "--filter",
410
+ `name=${containerName}`,
411
+ "--format",
412
+ "{{.Names}}"
413
+ ]);
414
+ return stdout.includes(containerName);
415
+ } catch (error) {
416
+ return false;
417
+ }
418
+ }
387
419
  export {
388
420
  cleanupTempContainer,
421
+ isContainerRunning,
422
+ removeDockerContainer,
389
423
  startDockerContainer,
390
424
  startDockerContainerZeroDowntime,
425
+ stopDockerContainer,
391
426
  swapContainersForZeroDowntime
392
427
  };
393
428
  //# sourceMappingURL=docker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"docker.js","sources":["../../src/utils/docker.ts"],"sourcesContent":["import chalk from 'chalk'\nimport { execa } from 'execa'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Interface for zero-downtime deployment container info\n */\nexport interface ZeroDowntimeContainerInfo {\n tempContainerName: string\n tempPort: number\n oldContainerExists: boolean\n}\n\n/**\n * Start a new container on a temporary port for zero-downtime deployment\n * Returns information about the temporary container\n */\nexport async function startDockerContainerZeroDowntime(\n service: ServiceConfig\n): Promise<ZeroDowntimeContainerInfo | null> {\n if (!service.docker) {\n return null\n }\n\n const { image, container, port } = service.docker\n\n if (!image) {\n throw new Error(\n `Image is required for zero-downtime deployment. Please specify an \"image\" field in the docker configuration for service \"${service.name}\".`\n )\n }\n\n try {\n // Check if old container exists\n let oldContainerExists = false\n let oldContainerRunning = false\n try {\n const { stdout } = await execa('docker', ['inspect', '--type', 'container', container], {\n stderr: 'pipe',\n })\n oldContainerExists = true\n\n try {\n const containerInfo = JSON.parse(stdout)\n if (containerInfo && containerInfo[0]) {\n oldContainerRunning = containerInfo[0].State?.Running || false\n console.log(\n chalk.dim(\n ` ๐Ÿ“‹ Existing container \"${container}\" found (running: ${oldContainerRunning})`\n )\n )\n }\n } catch (parseError) {\n // If we can't parse, that's okay - we know the container exists\n }\n } catch (error: any) {\n // Container doesn't exist - this is a fresh deployment\n oldContainerExists = false\n console.log(chalk.dim(` ๐Ÿ“‹ No existing container found, performing fresh deployment`))\n }\n\n // For zero-downtime, we need a temporary port and container name\n const tempPort = oldContainerExists ? service.port + 10000 : service.port\n const tempContainerName = oldContainerExists ? `${container}-new` : container\n\n // Check if temp container already exists (from a failed previous deployment)\n try {\n await execa('docker', ['inspect', '--type', 'container', tempContainerName], {\n stderr: 'pipe',\n })\n // Temp container exists, remove it\n console.log(chalk.yellow(` ๐Ÿงน Cleaning up previous temporary container...`))\n await execa('docker', ['rm', '-f', tempContainerName])\n } catch (error) {\n // Temp container doesn't exist, which is fine\n }\n\n // Check if temp port is available\n if (oldContainerExists) {\n try {\n const { stdout: portCheck } = await execa('docker', ['ps', '--format', '{{.Ports}}'])\n const portPattern = new RegExp(`:${tempPort}->`, 'g')\n if (portCheck && portPattern.test(portCheck)) {\n throw new Error(\n `Temporary port ${tempPort} is already in use. Please ensure no other containers are using ports in the range ${service.port}-${tempPort}.`\n )\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('Temporary port')) {\n throw error\n }\n }\n }\n\n // Pull the latest image\n try {\n console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`))\n await execa('docker', ['pull', image])\n console.log(chalk.green(` โœ… Image pulled successfully: ${image}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n console.log(\n chalk.yellow(` โš ๏ธ Failed to pull image ${image}, using existing local image if available`)\n )\n console.log(chalk.dim(` Error: ${errorDetails}`))\n }\n\n // Create Docker port binding\n const args = [\n 'run',\n '-d',\n '--name',\n tempContainerName,\n '-p',\n `${tempPort}:${port}`, // Port binding: host:container\n '--restart',\n 'unless-stopped',\n ]\n\n // Add environment variables if configured\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n if (oldContainerExists) {\n console.log(\n chalk.green(\n ` โœ… Created new container \"${tempContainerName}\" on temporary port ${tempPort}`\n )\n )\n } else {\n console.log(chalk.green(` โœ… Created and started container: ${tempContainerName}`))\n }\n } catch (error: any) {\n const errorMessage = error?.message || String(error) || 'Unknown error'\n const errorStderr = error?.stderr || ''\n const errorStdout = error?.stdout || ''\n\n const fullError = [errorMessage, errorStderr, errorStdout]\n .filter(Boolean)\n .join('\\n')\n .toLowerCase()\n\n if (\n fullError.includes('port is already allocated') ||\n fullError.includes('bind: address already in use')\n ) {\n throw new Error(\n `Port ${tempPort} is already in use. Please ensure the port is available for zero-downtime deployment.`\n )\n }\n\n if (\n fullError.includes('container name is already in use') ||\n fullError.includes('is already in use')\n ) {\n throw new Error(\n `Container name \"${tempContainerName}\" is already in use. Please remove it manually and try again.`\n )\n }\n\n const details = errorStderr || errorStdout || errorMessage\n throw new Error(`Failed to create Docker container \"${tempContainerName}\": ${details}`)\n }\n\n return {\n tempContainerName,\n tempPort,\n oldContainerExists,\n }\n } catch (error: any) {\n if (error instanceof Error && error.message) {\n throw new Error(\n `Failed to start Docker container for zero-downtime deployment of service \"${service.name}\": ${error.message}`\n )\n }\n\n const errorDetails =\n error?.message || error?.stderr || error?.stdout || String(error) || 'Unknown error'\n throw new Error(\n `Failed to start Docker container for zero-downtime deployment of service \"${service.name}\": ${errorDetails}`\n )\n }\n}\n\n/**\n * Swap containers for zero-downtime deployment\n * Stops old container and creates new container on original port\n * Note: Temp container cleanup should happen after nginx is updated to original port\n */\nexport async function swapContainersForZeroDowntime(\n service: ServiceConfig,\n tempInfo: ZeroDowntimeContainerInfo\n): Promise<void> {\n if (!service.docker) {\n return\n }\n\n const { container, image, port } = service.docker\n\n if (!image) {\n throw new Error(`Image is required for container swap. Service: ${service.name}`)\n }\n\n try {\n // Step 1: Stop and remove old container (nginx still pointing to temp port, so no downtime)\n if (tempInfo.oldContainerExists) {\n console.log(chalk.cyan(` ๐Ÿ”„ Stopping old container \"${container}\"...`))\n try {\n await execa('docker', ['stop', container])\n console.log(chalk.green(` โœ… Stopped old container: ${container}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If container is already stopped, that's fine\n if (!errorDetails.toLowerCase().includes('already stopped')) {\n console.log(chalk.yellow(` โš ๏ธ Could not stop old container: ${errorDetails}`))\n }\n }\n\n try {\n await execa('docker', ['rm', container])\n console.log(chalk.green(` โœ… Removed old container: ${container}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If container doesn't exist, that's fine\n if (\n !errorDetails.toLowerCase().includes('no such container') &&\n !errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Could not remove old container: ${errorDetails}`))\n }\n }\n\n // Step 2: Create new container on original port (temp container still running on temp port)\n console.log(chalk.cyan(` ๐Ÿ”„ Creating new container on production port...`))\n\n const args = [\n 'run',\n '-d',\n '--name',\n container,\n '-p',\n `${service.port}:${port}`,\n '--restart',\n 'unless-stopped',\n ]\n\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n console.log(\n chalk.green(\n ` โœ… Created new container \"${container}\" on production port ${service.port}`\n )\n )\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n throw new Error(`Failed to create final container \"${container}\": ${errorDetails}`)\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n throw new Error(`Failed to swap containers for zero-downtime deployment: ${errorDetails}`)\n }\n}\n\n/**\n * Clean up temporary container after zero-downtime deployment\n * Should be called after nginx has been updated to point to the new container\n */\nexport async function cleanupTempContainer(tempContainerName: string): Promise<void> {\n try {\n console.log(chalk.cyan(` ๐Ÿงน Cleaning up temporary container \"${tempContainerName}\"...`))\n\n // Stop temp container\n try {\n await execa('docker', ['stop', tempContainerName])\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If already stopped, that's fine\n if (!errorDetails.toLowerCase().includes('already stopped')) {\n console.log(chalk.yellow(` โš ๏ธ Could not stop temp container: ${errorDetails}`))\n }\n }\n\n // Remove temp container\n try {\n await execa('docker', ['rm', tempContainerName])\n console.log(chalk.green(` โœ… Removed temporary container: ${tempContainerName}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If doesn't exist, that's fine\n if (\n !errorDetails.toLowerCase().includes('no such container') &&\n !errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Could not remove temp container: ${errorDetails}`))\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n console.log(chalk.yellow(` โš ๏ธ Error during temp container cleanup: ${errorDetails}`))\n // Don't throw - cleanup failures shouldn't fail the deployment\n }\n}\n\n/**\n * Start or connect to a Docker container for a service\n * For zero-downtime deployments, use startDockerContainerZeroDowntime instead\n */\nexport async function startDockerContainer(service: ServiceConfig): Promise<void> {\n if (!service.docker) {\n return\n }\n\n const { image, container, port } = service.docker\n\n try {\n // Check if container exists using docker inspect (exact name match)\n let containerExists = false\n let containerState = ''\n try {\n const { stdout } = await execa('docker', ['inspect', '--type', 'container', container], {\n stderr: 'pipe',\n })\n containerExists = true\n\n // Parse container state from inspect output\n try {\n const containerInfo = JSON.parse(stdout)\n if (containerInfo && containerInfo[0]) {\n containerState = containerInfo[0].State?.Status || 'unknown'\n const isRunning = containerInfo[0].State?.Running || false\n console.log(\n chalk.dim(\n ` ๐Ÿ“‹ Container \"${container}\" exists (state: ${containerState}, running: ${isRunning})`\n )\n )\n }\n } catch (parseError) {\n // If we can't parse, that's okay - we know the container exists\n }\n } catch (error: any) {\n // Container doesn't exist - this is expected for new deployments\n containerExists = false\n const errorMessage = error?.stderr || error?.message || ''\n if (\n errorMessage.includes('No such container') ||\n errorMessage.includes('Error: No such object')\n ) {\n console.log(chalk.dim(` ๐Ÿ“‹ Container \"${container}\" does not exist, will create new one`))\n }\n }\n\n let shouldCreateNewContainer = true\n\n if (containerExists) {\n // Container exists - always remove and recreate for fresh deployment\n if (!image) {\n throw new Error(\n `Container \"${container}\" exists and needs to be recreated for redeployment. ` +\n `No image specified in configuration for service \"${service.name}\". ` +\n `Please add an \"image\" field to the docker configuration to allow container recreation.`\n )\n }\n\n // Always recreate container on redeploy to ensure fresh deployment\n console.log(\n chalk.yellow(` ๐Ÿ”„ Removing existing container \"${container}\" for redeployment...`)\n )\n\n // Stop and remove old container (force remove will stop if running)\n try {\n await execa('docker', ['rm', '-f', container])\n console.log(chalk.green(` โœ… Removed existing container: ${container}`))\n\n // Verify container was actually removed\n try {\n await execa('docker', ['inspect', '--type', 'container', container], {\n stdout: 'ignore',\n stderr: 'ignore',\n })\n // If we get here, container still exists - this shouldn't happen\n throw new Error(\n `Container \"${container}\" was not properly removed. Please remove it manually and try again.`\n )\n } catch (verifyError: any) {\n // Container doesn't exist anymore - this is what we want\n const verifyMessage = verifyError?.stderr || verifyError?.message || ''\n if (\n verifyMessage.includes('No such container') ||\n verifyMessage.includes('Error: No such object')\n ) {\n console.log(chalk.dim(` โœ“ Verified container \"${container}\" was removed`))\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n // If container doesn't exist, that's okay - it might have been removed already\n if (\n errorDetails.toLowerCase().includes('no such container') ||\n errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Container \"${container}\" was already removed`))\n } else {\n throw new Error(\n `Failed to remove old container \"${container}\" for service \"${service.name}\": ${errorDetails}`\n )\n }\n }\n // Will create new container below with fresh image\n }\n\n // Create new container (either doesn't exist, or was recreated above)\n if (shouldCreateNewContainer && image) {\n // Pull the latest image before creating container\n try {\n console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`))\n await execa('docker', ['pull', image])\n console.log(chalk.green(` โœ… Image pulled successfully: ${image}`))\n } catch (error: any) {\n // If pull fails, log warning but continue (image might be local or pull might fail)\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n console.log(\n chalk.yellow(\n ` โš ๏ธ Failed to pull image ${image}, using existing local image if available`\n )\n )\n console.log(chalk.dim(` Error: ${errorDetails}`))\n }\n\n // Container doesn't exist and image is provided, create and run it\n // First check if the host port is already in use\n try {\n const { stdout: portCheck } = await execa('docker', ['ps', '--format', '{{.Ports}}'])\n\n // Check if port is already mapped\n const portPattern = new RegExp(`:${service.port}->`, 'g')\n if (portCheck && portPattern.test(portCheck)) {\n throw new Error(\n `Port ${service.port} is already in use by another container. Please use a different port for service \"${service.name}\".`\n )\n }\n } catch (error) {\n // If docker ps fails or port check fails, we'll let docker run handle it\n // But if it's our custom error, rethrow it\n if (error instanceof Error && error.message.includes('Port')) {\n throw error\n }\n }\n\n // Create Docker port binding: hostPort:containerPort\n // service.port = host port (accessible from host machine)\n // port = container port (what the app listens on inside container)\n // Format: -p hostPort:containerPort\n const args = [\n 'run',\n '-d',\n '--name',\n container,\n '-p',\n `${service.port}:${port}`, // Port binding: host:container\n '--restart',\n 'unless-stopped',\n ]\n\n // Add environment variables if configured\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n console.log(chalk.green(` โœ… Created and started container: ${container}`))\n } catch (error: any) {\n // Extract error details from execa error\n const errorMessage = error?.message || String(error) || 'Unknown error'\n const errorStderr = error?.stderr || ''\n const errorStdout = error?.stdout || ''\n\n const fullError = [errorMessage, errorStderr, errorStdout]\n .filter(Boolean)\n .join('\\n')\n .toLowerCase()\n\n // Check if error is due to port binding\n if (\n fullError.includes('port is already allocated') ||\n fullError.includes('bind: address already in use') ||\n fullError.includes('port already in use') ||\n fullError.includes('port is already in use')\n ) {\n throw new Error(\n `Port ${service.port} is already in use. Please use a different port for service \"${service.name}\".`\n )\n }\n\n // Check if error is due to container name already in use\n if (\n fullError.includes('container name is already in use') ||\n fullError.includes('is already in use')\n ) {\n throw new Error(\n `Container name \"${container}\" is already in use. This might happen if the container was created between checks. ` +\n `Please remove the container manually or wait a moment and try again.`\n )\n }\n\n // Check if error is due to image not found\n if (\n fullError.includes('no such image') ||\n fullError.includes('pull access denied') ||\n fullError.includes('repository does not exist')\n ) {\n throw new Error(\n `Docker image \"${image}\" not found or cannot be accessed. ` +\n `Please verify the image name and ensure you have access to pull it.`\n )\n }\n\n // Generic error with more details\n const details = errorStderr || errorStdout || errorMessage\n throw new Error(`Failed to create Docker container \"${container}\": ${details}`)\n }\n } else if (shouldCreateNewContainer && !image) {\n // Only throw error if we need to create a container but no image is provided\n throw new Error(\n `Container \"${container}\" does not exist and no image specified in configuration. ` +\n `Please either:\\n` +\n ` 1. Add an \"image\" field to the docker configuration for service \"${service.name}\", or\\n` +\n ` 2. Create the container \"${container}\" manually before deploying.`\n )\n }\n // If shouldCreateNewContainer is false, it means we successfully handled an existing container\n } catch (error: any) {\n // If error is already a well-formed Error with a message, preserve it\n if (error instanceof Error && error.message) {\n // Check if the error message already includes context about the container/service\n if (error.message.includes(container) || error.message.includes(service.name)) {\n throw error\n }\n // Otherwise, wrap with more context\n throw new Error(\n `Failed to start Docker container \"${container}\" for service \"${service.name}\": ${error.message}`\n )\n }\n\n // Handle non-Error objects or errors without messages\n const errorDetails =\n error?.message || error?.stderr || error?.stdout || String(error) || 'Unknown error'\n throw new Error(\n `Failed to start Docker container \"${container}\" for service \"${service.name}\": ${errorDetails}`\n )\n }\n}\n\n/**\n * Stop a Docker container\n */\nexport async function stopDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['stop', containerName])\n } catch (error) {\n throw new Error(\n `Failed to stop container ${containerName}: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n\n/**\n * Remove a Docker container\n */\nexport async function removeDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['rm', '-f', containerName])\n } catch (error) {\n throw new Error(\n `Failed to remove container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Check if a Docker container is running\n */\nexport async function isContainerRunning(containerName: string): Promise<boolean> {\n try {\n const { stdout } = await execa('docker', [\n 'ps',\n '--filter',\n `name=${containerName}`,\n '--format',\n '{{.Names}}',\n ])\n return stdout.includes(containerName)\n } catch (error) {\n return false\n }\n}\n\n/**\n * Get container logs\n */\nexport async function getContainerLogs(\n containerName: string,\n lines: number = 100\n): Promise<string> {\n try {\n const { stdout } = await execa('docker', ['logs', '--tail', lines.toString(), containerName])\n return stdout\n } catch (error) {\n throw new Error(\n `Failed to get logs for container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Inspect a Docker container\n */\nexport async function inspectContainer(containerName: string): Promise<any> {\n try {\n const { stdout } = await execa('docker', ['inspect', containerName])\n return JSON.parse(stdout)[0]\n } catch (error) {\n throw new Error(\n `Failed to inspect container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Get the port mapping for an existing container\n * Returns the port mapping in format \"hostPort:containerPort\" or null if not found\n */\nexport async function getContainerPortMapping(containerName: string): Promise<string | null> {\n try {\n const containerInfo = await inspectContainer(containerName)\n const portBindings = containerInfo.NetworkSettings?.Ports\n\n if (!portBindings) {\n return null\n }\n\n // Find the first port binding\n for (const [containerPort, hostBindings] of Object.entries(portBindings)) {\n if (hostBindings && Array.isArray(hostBindings) && hostBindings.length > 0) {\n const hostPort = hostBindings[0].HostPort\n // Remove /tcp or /udp suffix from container port\n const cleanContainerPort = containerPort.replace(/\\/.*$/, '')\n return `${hostPort}:${cleanContainerPort}`\n }\n }\n\n return null\n } catch (error) {\n return null\n }\n}\n\n/**\n * Check if container needs to be recreated based on configuration changes\n */\nexport async function needsRecreate(\n service: ServiceConfig,\n containerName: string\n): Promise<boolean> {\n if (!service.docker) {\n return false\n }\n\n const expectedPortMapping = `${service.port}:${service.docker.port}`\n const currentPortMapping = await getContainerPortMapping(containerName)\n\n // If port mapping is different, need to recreate\n if (currentPortMapping !== expectedPortMapping) {\n return true\n }\n\n // Check if image is different (if image is specified in config)\n if (service.docker.image) {\n try {\n const containerInfo = await inspectContainer(containerName)\n const currentImage = containerInfo.Config?.Image\n\n if (currentImage && currentImage !== service.docker.image) {\n return true\n }\n } catch (error) {\n // If we can't check, assume no recreation needed\n }\n }\n\n // Check if environment variables have changed\n if (service.environment) {\n try {\n const containerInfo = await inspectContainer(containerName)\n const currentEnv = containerInfo.Config?.Env || []\n\n // Convert current env array to object\n const currentEnvObj: Record<string, string> = {}\n for (const envVar of currentEnv) {\n const [key, ...valueParts] = envVar.split('=')\n if (key) {\n currentEnvObj[key] = valueParts.join('=')\n }\n }\n\n // Compare with expected environment variables\n for (const [key, value] of Object.entries(service.environment)) {\n if (currentEnvObj[key] !== value) {\n return true // Environment variable changed, need to recreate\n }\n }\n\n // Check if any environment variables were removed\n for (const key of Object.keys(currentEnvObj)) {\n // Skip PATH and other system variables\n if (key === 'PATH' || key === 'HOSTNAME' || key.startsWith('_')) {\n continue\n }\n // If a variable exists in container but not in config, and it was explicitly set before\n // we'll recreate to ensure consistency (this is a conservative approach)\n // For now, we only check if config vars match, not if extra vars exist\n }\n } catch (error) {\n // If we can't check, assume no recreation needed\n }\n }\n\n return false\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"docker.js","sources":["../../src/utils/docker.ts"],"sourcesContent":["import chalk from 'chalk'\nimport { execa } from 'execa'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Interface for zero-downtime deployment container info\n */\nexport interface ZeroDowntimeContainerInfo {\n tempContainerName: string\n tempPort: number\n oldContainerExists: boolean\n}\n\n/**\n * Start a new container on a temporary port for zero-downtime deployment\n * Returns information about the temporary container\n */\nexport async function startDockerContainerZeroDowntime(\n service: ServiceConfig\n): Promise<ZeroDowntimeContainerInfo | null> {\n if (!service.docker) {\n return null\n }\n\n const { image, container, port } = service.docker\n\n if (!image) {\n throw new Error(\n `Image is required for zero-downtime deployment. Please specify an \"image\" field in the docker configuration for service \"${service.name}\".`\n )\n }\n\n try {\n // Check if old container exists\n let oldContainerExists = false\n let oldContainerRunning = false\n try {\n const { stdout } = await execa('docker', ['inspect', '--type', 'container', container], {\n stderr: 'pipe',\n })\n oldContainerExists = true\n\n try {\n const containerInfo = JSON.parse(stdout)\n if (containerInfo && containerInfo[0]) {\n oldContainerRunning = containerInfo[0].State?.Running || false\n console.log(\n chalk.dim(\n ` ๐Ÿ“‹ Existing container \"${container}\" found (running: ${oldContainerRunning})`\n )\n )\n }\n } catch (parseError) {\n // If we can't parse, that's okay - we know the container exists\n }\n } catch (error: any) {\n // Container doesn't exist - this is a fresh deployment\n oldContainerExists = false\n console.log(chalk.dim(` ๐Ÿ“‹ No existing container found, performing fresh deployment`))\n }\n\n // For zero-downtime, we need a temporary port and container name\n const tempPort = oldContainerExists ? service.port + 10000 : service.port\n const tempContainerName = oldContainerExists ? `${container}-new` : container\n\n // Check if temp container already exists (from a failed previous deployment)\n try {\n await execa('docker', ['inspect', '--type', 'container', tempContainerName], {\n stderr: 'pipe',\n })\n // Temp container exists, remove it\n console.log(chalk.yellow(` ๐Ÿงน Cleaning up previous temporary container...`))\n await execa('docker', ['rm', '-f', tempContainerName])\n } catch (error) {\n // Temp container doesn't exist, which is fine\n }\n\n // Check if temp port is available\n if (oldContainerExists) {\n try {\n const { stdout: portCheck } = await execa('docker', ['ps', '--format', '{{.Ports}}'])\n const portPattern = new RegExp(`:${tempPort}->`, 'g')\n if (portCheck && portPattern.test(portCheck)) {\n throw new Error(\n `Temporary port ${tempPort} is already in use. Please ensure no other containers are using ports in the range ${service.port}-${tempPort}.`\n )\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('Temporary port')) {\n throw error\n }\n }\n }\n\n // Pull the latest image\n try {\n console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`))\n await execa('docker', ['pull', image])\n console.log(chalk.green(` โœ… Image pulled successfully: ${image}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n console.log(\n chalk.yellow(` โš ๏ธ Failed to pull image ${image}, using existing local image if available`)\n )\n console.log(chalk.dim(` Error: ${errorDetails}`))\n }\n\n // Create Docker port binding\n const args = [\n 'run',\n '-d',\n '--name',\n tempContainerName,\n '-p',\n `${tempPort}:${port}`, // Port binding: host:container\n '--restart',\n 'unless-stopped',\n ]\n\n // Add environment variables if configured\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n if (oldContainerExists) {\n console.log(\n chalk.green(\n ` โœ… Created new container \"${tempContainerName}\" on temporary port ${tempPort}`\n )\n )\n } else {\n console.log(chalk.green(` โœ… Created and started container: ${tempContainerName}`))\n }\n } catch (error: any) {\n const errorMessage = error?.message || String(error) || 'Unknown error'\n const errorStderr = error?.stderr || ''\n const errorStdout = error?.stdout || ''\n\n const fullError = [errorMessage, errorStderr, errorStdout]\n .filter(Boolean)\n .join('\\n')\n .toLowerCase()\n\n if (\n fullError.includes('port is already allocated') ||\n fullError.includes('bind: address already in use')\n ) {\n throw new Error(\n `Port ${tempPort} is already in use. Please ensure the port is available for zero-downtime deployment.`\n )\n }\n\n if (\n fullError.includes('container name is already in use') ||\n fullError.includes('is already in use')\n ) {\n throw new Error(\n `Container name \"${tempContainerName}\" is already in use. Please remove it manually and try again.`\n )\n }\n\n const details = errorStderr || errorStdout || errorMessage\n throw new Error(`Failed to create Docker container \"${tempContainerName}\": ${details}`)\n }\n\n return {\n tempContainerName,\n tempPort,\n oldContainerExists,\n }\n } catch (error: any) {\n if (error instanceof Error && error.message) {\n throw new Error(\n `Failed to start Docker container for zero-downtime deployment of service \"${service.name}\": ${error.message}`\n )\n }\n\n const errorDetails =\n error?.message || error?.stderr || error?.stdout || String(error) || 'Unknown error'\n throw new Error(\n `Failed to start Docker container for zero-downtime deployment of service \"${service.name}\": ${errorDetails}`\n )\n }\n}\n\n/**\n * Swap containers for zero-downtime deployment\n * Stops old container and creates new container on original port\n * Note: Temp container cleanup should happen after nginx is updated to original port\n */\nexport async function swapContainersForZeroDowntime(\n service: ServiceConfig,\n tempInfo: ZeroDowntimeContainerInfo\n): Promise<void> {\n if (!service.docker) {\n return\n }\n\n const { container, image, port } = service.docker\n\n if (!image) {\n throw new Error(`Image is required for container swap. Service: ${service.name}`)\n }\n\n try {\n // Step 1: Stop and remove old container (nginx still pointing to temp port, so no downtime)\n if (tempInfo.oldContainerExists) {\n console.log(chalk.cyan(` ๐Ÿ”„ Stopping old container \"${container}\"...`))\n try {\n await execa('docker', ['stop', container])\n console.log(chalk.green(` โœ… Stopped old container: ${container}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If container is already stopped, that's fine\n if (!errorDetails.toLowerCase().includes('already stopped')) {\n console.log(chalk.yellow(` โš ๏ธ Could not stop old container: ${errorDetails}`))\n }\n }\n\n try {\n await execa('docker', ['rm', container])\n console.log(chalk.green(` โœ… Removed old container: ${container}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If container doesn't exist, that's fine\n if (\n !errorDetails.toLowerCase().includes('no such container') &&\n !errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Could not remove old container: ${errorDetails}`))\n }\n }\n\n // Step 2: Create new container on original port (temp container still running on temp port)\n console.log(chalk.cyan(` ๐Ÿ”„ Creating new container on production port...`))\n\n const args = [\n 'run',\n '-d',\n '--name',\n container,\n '-p',\n `${service.port}:${port}`,\n '--restart',\n 'unless-stopped',\n ]\n\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n console.log(\n chalk.green(\n ` โœ… Created new container \"${container}\" on production port ${service.port}`\n )\n )\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n throw new Error(`Failed to create final container \"${container}\": ${errorDetails}`)\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n throw new Error(`Failed to swap containers for zero-downtime deployment: ${errorDetails}`)\n }\n}\n\n/**\n * Clean up temporary container after zero-downtime deployment\n * Should be called after nginx has been updated to point to the new container\n */\nexport async function cleanupTempContainer(tempContainerName: string): Promise<void> {\n try {\n console.log(chalk.cyan(` ๐Ÿงน Cleaning up temporary container \"${tempContainerName}\"...`))\n\n // Stop temp container\n try {\n await execa('docker', ['stop', tempContainerName])\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If already stopped, that's fine\n if (!errorDetails.toLowerCase().includes('already stopped')) {\n console.log(chalk.yellow(` โš ๏ธ Could not stop temp container: ${errorDetails}`))\n }\n }\n\n // Remove temp container\n try {\n await execa('docker', ['rm', tempContainerName])\n console.log(chalk.green(` โœ… Removed temporary container: ${tempContainerName}`))\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n // If doesn't exist, that's fine\n if (\n !errorDetails.toLowerCase().includes('no such container') &&\n !errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Could not remove temp container: ${errorDetails}`))\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n console.log(chalk.yellow(` โš ๏ธ Error during temp container cleanup: ${errorDetails}`))\n // Don't throw - cleanup failures shouldn't fail the deployment\n }\n}\n\n/**\n * Start or connect to a Docker container for a service\n * For zero-downtime deployments, use startDockerContainerZeroDowntime instead\n */\nexport async function startDockerContainer(service: ServiceConfig): Promise<void> {\n if (!service.docker) {\n return\n }\n\n const { image, container, port } = service.docker\n\n try {\n // Check if container exists using docker inspect (exact name match)\n let containerExists = false\n let containerState = ''\n try {\n const { stdout } = await execa('docker', ['inspect', '--type', 'container', container], {\n stderr: 'pipe',\n })\n containerExists = true\n\n // Parse container state from inspect output\n try {\n const containerInfo = JSON.parse(stdout)\n if (containerInfo && containerInfo[0]) {\n containerState = containerInfo[0].State?.Status || 'unknown'\n const isRunning = containerInfo[0].State?.Running || false\n console.log(\n chalk.dim(\n ` ๐Ÿ“‹ Container \"${container}\" exists (state: ${containerState}, running: ${isRunning})`\n )\n )\n }\n } catch (parseError) {\n // If we can't parse, that's okay - we know the container exists\n }\n } catch (error: any) {\n // Container doesn't exist - this is expected for new deployments\n containerExists = false\n const errorMessage = error?.stderr || error?.message || ''\n if (\n errorMessage.includes('No such container') ||\n errorMessage.includes('Error: No such object')\n ) {\n console.log(chalk.dim(` ๐Ÿ“‹ Container \"${container}\" does not exist, will create new one`))\n }\n }\n\n let shouldCreateNewContainer = true\n\n if (containerExists) {\n // Container exists - always remove and recreate for fresh deployment\n if (!image) {\n throw new Error(\n `Container \"${container}\" exists and needs to be recreated for redeployment. ` +\n `No image specified in configuration for service \"${service.name}\". ` +\n `Please add an \"image\" field to the docker configuration to allow container recreation.`\n )\n }\n\n // Always recreate container on redeploy to ensure fresh deployment\n console.log(\n chalk.yellow(` ๐Ÿ”„ Removing existing container \"${container}\" for redeployment...`)\n )\n\n // Stop and remove old container (force remove will stop if running)\n try {\n await execa('docker', ['rm', '-f', container])\n console.log(chalk.green(` โœ… Removed existing container: ${container}`))\n\n // Verify container was actually removed\n try {\n await execa('docker', ['inspect', '--type', 'container', container], {\n stdout: 'ignore',\n stderr: 'ignore',\n })\n // If we get here, container still exists - this shouldn't happen\n throw new Error(\n `Container \"${container}\" was not properly removed. Please remove it manually and try again.`\n )\n } catch (verifyError: any) {\n // Container doesn't exist anymore - this is what we want\n const verifyMessage = verifyError?.stderr || verifyError?.message || ''\n if (\n verifyMessage.includes('No such container') ||\n verifyMessage.includes('Error: No such object')\n ) {\n console.log(chalk.dim(` โœ“ Verified container \"${container}\" was removed`))\n }\n }\n } catch (error: any) {\n const errorDetails = error?.stderr || error?.message || String(error) || 'Unknown error'\n // If container doesn't exist, that's okay - it might have been removed already\n if (\n errorDetails.toLowerCase().includes('no such container') ||\n errorDetails.toLowerCase().includes('container not found')\n ) {\n console.log(chalk.yellow(` โš ๏ธ Container \"${container}\" was already removed`))\n } else {\n throw new Error(\n `Failed to remove old container \"${container}\" for service \"${service.name}\": ${errorDetails}`\n )\n }\n }\n // Will create new container below with fresh image\n }\n\n // Create new container (either doesn't exist, or was recreated above)\n if (shouldCreateNewContainer && image) {\n // Pull the latest image before creating container\n try {\n console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`))\n await execa('docker', ['pull', image])\n console.log(chalk.green(` โœ… Image pulled successfully: ${image}`))\n } catch (error: any) {\n // If pull fails, log warning but continue (image might be local or pull might fail)\n const errorDetails = error?.stderr || error?.message || 'Unknown error'\n console.log(\n chalk.yellow(\n ` โš ๏ธ Failed to pull image ${image}, using existing local image if available`\n )\n )\n console.log(chalk.dim(` Error: ${errorDetails}`))\n }\n\n // Container doesn't exist and image is provided, create and run it\n // First check if the host port is already in use\n try {\n const { stdout: portCheck } = await execa('docker', ['ps', '--format', '{{.Ports}}'])\n\n // Check if port is already mapped\n const portPattern = new RegExp(`:${service.port}->`, 'g')\n if (portCheck && portPattern.test(portCheck)) {\n throw new Error(\n `Port ${service.port} is already in use by another container. Please use a different port for service \"${service.name}\".`\n )\n }\n } catch (error) {\n // If docker ps fails or port check fails, we'll let docker run handle it\n // But if it's our custom error, rethrow it\n if (error instanceof Error && error.message.includes('Port')) {\n throw error\n }\n }\n\n // Create Docker port binding: hostPort:containerPort\n // service.port = host port (accessible from host machine)\n // port = container port (what the app listens on inside container)\n // Format: -p hostPort:containerPort\n const args = [\n 'run',\n '-d',\n '--name',\n container,\n '-p',\n `${service.port}:${port}`, // Port binding: host:container\n '--restart',\n 'unless-stopped',\n ]\n\n // Add environment variables if configured\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n try {\n await execa('docker', args)\n console.log(chalk.green(` โœ… Created and started container: ${container}`))\n } catch (error: any) {\n // Extract error details from execa error\n const errorMessage = error?.message || String(error) || 'Unknown error'\n const errorStderr = error?.stderr || ''\n const errorStdout = error?.stdout || ''\n\n const fullError = [errorMessage, errorStderr, errorStdout]\n .filter(Boolean)\n .join('\\n')\n .toLowerCase()\n\n // Check if error is due to port binding\n if (\n fullError.includes('port is already allocated') ||\n fullError.includes('bind: address already in use') ||\n fullError.includes('port already in use') ||\n fullError.includes('port is already in use')\n ) {\n throw new Error(\n `Port ${service.port} is already in use. Please use a different port for service \"${service.name}\".`\n )\n }\n\n // Check if error is due to container name already in use\n if (\n fullError.includes('container name is already in use') ||\n fullError.includes('is already in use')\n ) {\n throw new Error(\n `Container name \"${container}\" is already in use. This might happen if the container was created between checks. ` +\n `Please remove the container manually or wait a moment and try again.`\n )\n }\n\n // Check if error is due to image not found\n if (\n fullError.includes('no such image') ||\n fullError.includes('pull access denied') ||\n fullError.includes('repository does not exist')\n ) {\n throw new Error(\n `Docker image \"${image}\" not found or cannot be accessed. ` +\n `Please verify the image name and ensure you have access to pull it.`\n )\n }\n\n // Generic error with more details\n const details = errorStderr || errorStdout || errorMessage\n throw new Error(`Failed to create Docker container \"${container}\": ${details}`)\n }\n } else if (shouldCreateNewContainer && !image) {\n // Only throw error if we need to create a container but no image is provided\n throw new Error(\n `Container \"${container}\" does not exist and no image specified in configuration. ` +\n `Please either:\\n` +\n ` 1. Add an \"image\" field to the docker configuration for service \"${service.name}\", or\\n` +\n ` 2. Create the container \"${container}\" manually before deploying.`\n )\n }\n // If shouldCreateNewContainer is false, it means we successfully handled an existing container\n } catch (error: any) {\n // If error is already a well-formed Error with a message, preserve it\n if (error instanceof Error && error.message) {\n // Check if the error message already includes context about the container/service\n if (error.message.includes(container) || error.message.includes(service.name)) {\n throw error\n }\n // Otherwise, wrap with more context\n throw new Error(\n `Failed to start Docker container \"${container}\" for service \"${service.name}\": ${error.message}`\n )\n }\n\n // Handle non-Error objects or errors without messages\n const errorDetails =\n error?.message || error?.stderr || error?.stdout || String(error) || 'Unknown error'\n throw new Error(\n `Failed to start Docker container \"${container}\" for service \"${service.name}\": ${errorDetails}`\n )\n }\n}\n\n/**\n * Stop a Docker container\n */\nexport async function stopDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['stop', containerName])\n } catch (error) {\n throw new Error(\n `Failed to stop container ${containerName}: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n\n/**\n * Remove a Docker container\n */\nexport async function removeDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['rm', '-f', containerName])\n } catch (error) {\n throw new Error(\n `Failed to remove container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Check if a Docker container is running\n */\nexport async function isContainerRunning(containerName: string): Promise<boolean> {\n try {\n const { stdout } = await execa('docker', [\n 'ps',\n '--filter',\n `name=${containerName}`,\n '--format',\n '{{.Names}}',\n ])\n return stdout.includes(containerName)\n } catch (error) {\n return false\n }\n}\n\n/**\n * Get container logs\n */\nexport async function getContainerLogs(\n containerName: string,\n lines: number = 100\n): Promise<string> {\n try {\n const { stdout } = await execa('docker', ['logs', '--tail', lines.toString(), containerName])\n return stdout\n } catch (error) {\n throw new Error(\n `Failed to get logs for container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Inspect a Docker container\n */\nexport async function inspectContainer(containerName: string): Promise<any> {\n try {\n const { stdout } = await execa('docker', ['inspect', containerName])\n return JSON.parse(stdout)[0]\n } catch (error) {\n throw new Error(\n `Failed to inspect container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Get the port mapping for an existing container\n * Returns the port mapping in format \"hostPort:containerPort\" or null if not found\n */\nexport async function getContainerPortMapping(containerName: string): Promise<string | null> {\n try {\n const containerInfo = await inspectContainer(containerName)\n const portBindings = containerInfo.NetworkSettings?.Ports\n\n if (!portBindings) {\n return null\n }\n\n // Find the first port binding\n for (const [containerPort, hostBindings] of Object.entries(portBindings)) {\n if (hostBindings && Array.isArray(hostBindings) && hostBindings.length > 0) {\n const hostPort = hostBindings[0].HostPort\n // Remove /tcp or /udp suffix from container port\n const cleanContainerPort = containerPort.replace(/\\/.*$/, '')\n return `${hostPort}:${cleanContainerPort}`\n }\n }\n\n return null\n } catch (error) {\n return null\n }\n}\n\n/**\n * Check if container needs to be recreated based on configuration changes\n */\nexport async function needsRecreate(\n service: ServiceConfig,\n containerName: string\n): Promise<boolean> {\n if (!service.docker) {\n return false\n }\n\n const expectedPortMapping = `${service.port}:${service.docker.port}`\n const currentPortMapping = await getContainerPortMapping(containerName)\n\n // If port mapping is different, need to recreate\n if (currentPortMapping !== expectedPortMapping) {\n return true\n }\n\n // Check if image is different (if image is specified in config)\n if (service.docker.image) {\n try {\n const containerInfo = await inspectContainer(containerName)\n const currentImage = containerInfo.Config?.Image\n\n if (currentImage && currentImage !== service.docker.image) {\n return true\n }\n } catch (error) {\n // If we can't check, assume no recreation needed\n }\n }\n\n // Check if environment variables have changed\n if (service.environment) {\n try {\n const containerInfo = await inspectContainer(containerName)\n const currentEnv = containerInfo.Config?.Env || []\n\n // Convert current env array to object\n const currentEnvObj: Record<string, string> = {}\n for (const envVar of currentEnv) {\n const [key, ...valueParts] = envVar.split('=')\n if (key) {\n currentEnvObj[key] = valueParts.join('=')\n }\n }\n\n // Compare with expected environment variables\n for (const [key, value] of Object.entries(service.environment)) {\n if (currentEnvObj[key] !== value) {\n return true // Environment variable changed, need to recreate\n }\n }\n\n // Check if any environment variables were removed\n for (const key of Object.keys(currentEnvObj)) {\n // Skip PATH and other system variables\n if (key === 'PATH' || key === 'HOSTNAME' || key.startsWith('_')) {\n continue\n }\n // If a variable exists in container but not in config, and it was explicitly set before\n // we'll recreate to ensure consistency (this is a conservative approach)\n // For now, we only check if config vars match, not if extra vars exist\n }\n } catch (error) {\n // If we can't check, assume no recreation needed\n }\n }\n\n return false\n}\n"],"names":[],"mappings":"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"}
@@ -293,7 +293,17 @@ async function reloadNginx(reloadCommand) {
293
293
  throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`);
294
294
  }
295
295
  }
296
+ async function disableSite(siteName, configPath) {
297
+ const enabledPath = path.join(
298
+ configPath.replace("sites-available", "sites-enabled"),
299
+ `${siteName}.conf`
300
+ );
301
+ if (await fs.pathExists(enabledPath)) {
302
+ await fs.remove(enabledPath);
303
+ }
304
+ }
296
305
  export {
306
+ disableSite,
297
307
  enableSite,
298
308
  generateMultiServiceNginxConfig,
299
309
  generateNginxConfig,