suthep 0.1.0 โ†’ 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
@@ -1,30 +1,324 @@
1
+ import chalk from "chalk";
1
2
  import { execa } from "execa";
3
+ async function startDockerContainerZeroDowntime(service) {
4
+ if (!service.docker) {
5
+ return null;
6
+ }
7
+ const { image, container, port } = service.docker;
8
+ if (!image) {
9
+ throw new Error(
10
+ `Image is required for zero-downtime deployment. Please specify an "image" field in the docker configuration for service "${service.name}".`
11
+ );
12
+ }
13
+ try {
14
+ let oldContainerExists = false;
15
+ let oldContainerRunning = false;
16
+ try {
17
+ const { stdout } = await execa("docker", ["inspect", "--type", "container", container], {
18
+ stderr: "pipe"
19
+ });
20
+ oldContainerExists = true;
21
+ try {
22
+ const containerInfo = JSON.parse(stdout);
23
+ if (containerInfo && containerInfo[0]) {
24
+ oldContainerRunning = containerInfo[0].State?.Running || false;
25
+ console.log(
26
+ chalk.dim(
27
+ ` ๐Ÿ“‹ Existing container "${container}" found (running: ${oldContainerRunning})`
28
+ )
29
+ );
30
+ }
31
+ } catch (parseError) {
32
+ }
33
+ } catch (error) {
34
+ oldContainerExists = false;
35
+ console.log(chalk.dim(` ๐Ÿ“‹ No existing container found, performing fresh deployment`));
36
+ }
37
+ const tempPort = oldContainerExists ? service.port + 1e4 : service.port;
38
+ const tempContainerName = oldContainerExists ? `${container}-new` : container;
39
+ try {
40
+ await execa("docker", ["inspect", "--type", "container", tempContainerName], {
41
+ stderr: "pipe"
42
+ });
43
+ console.log(chalk.yellow(` ๐Ÿงน Cleaning up previous temporary container...`));
44
+ await execa("docker", ["rm", "-f", tempContainerName]);
45
+ } catch (error) {
46
+ }
47
+ if (oldContainerExists) {
48
+ try {
49
+ const { stdout: portCheck } = await execa("docker", ["ps", "--format", "{{.Ports}}"]);
50
+ const portPattern = new RegExp(`:${tempPort}->`, "g");
51
+ if (portCheck && portPattern.test(portCheck)) {
52
+ throw new Error(
53
+ `Temporary port ${tempPort} is already in use. Please ensure no other containers are using ports in the range ${service.port}-${tempPort}.`
54
+ );
55
+ }
56
+ } catch (error) {
57
+ if (error instanceof Error && error.message.includes("Temporary port")) {
58
+ throw error;
59
+ }
60
+ }
61
+ }
62
+ try {
63
+ console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`));
64
+ await execa("docker", ["pull", image]);
65
+ console.log(chalk.green(` โœ… Image pulled successfully: ${image}`));
66
+ } catch (error) {
67
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
68
+ console.log(
69
+ chalk.yellow(` โš ๏ธ Failed to pull image ${image}, using existing local image if available`)
70
+ );
71
+ console.log(chalk.dim(` Error: ${errorDetails}`));
72
+ }
73
+ const args = [
74
+ "run",
75
+ "-d",
76
+ "--name",
77
+ tempContainerName,
78
+ "-p",
79
+ `${tempPort}:${port}`,
80
+ // Port binding: host:container
81
+ "--restart",
82
+ "unless-stopped"
83
+ ];
84
+ if (service.environment) {
85
+ for (const [key, value] of Object.entries(service.environment)) {
86
+ args.push("-e", `${key}=${value}`);
87
+ }
88
+ }
89
+ args.push(image);
90
+ try {
91
+ await execa("docker", args);
92
+ if (oldContainerExists) {
93
+ console.log(
94
+ chalk.green(
95
+ ` โœ… Created new container "${tempContainerName}" on temporary port ${tempPort}`
96
+ )
97
+ );
98
+ } else {
99
+ console.log(chalk.green(` โœ… Created and started container: ${tempContainerName}`));
100
+ }
101
+ } catch (error) {
102
+ const errorMessage = error?.message || String(error) || "Unknown error";
103
+ const errorStderr = error?.stderr || "";
104
+ const errorStdout = error?.stdout || "";
105
+ const fullError = [errorMessage, errorStderr, errorStdout].filter(Boolean).join("\n").toLowerCase();
106
+ if (fullError.includes("port is already allocated") || fullError.includes("bind: address already in use")) {
107
+ throw new Error(
108
+ `Port ${tempPort} is already in use. Please ensure the port is available for zero-downtime deployment.`
109
+ );
110
+ }
111
+ if (fullError.includes("container name is already in use") || fullError.includes("is already in use")) {
112
+ throw new Error(
113
+ `Container name "${tempContainerName}" is already in use. Please remove it manually and try again.`
114
+ );
115
+ }
116
+ const details = errorStderr || errorStdout || errorMessage;
117
+ throw new Error(`Failed to create Docker container "${tempContainerName}": ${details}`);
118
+ }
119
+ return {
120
+ tempContainerName,
121
+ tempPort,
122
+ oldContainerExists
123
+ };
124
+ } catch (error) {
125
+ if (error instanceof Error && error.message) {
126
+ throw new Error(
127
+ `Failed to start Docker container for zero-downtime deployment of service "${service.name}": ${error.message}`
128
+ );
129
+ }
130
+ const errorDetails = error?.message || error?.stderr || error?.stdout || String(error) || "Unknown error";
131
+ throw new Error(
132
+ `Failed to start Docker container for zero-downtime deployment of service "${service.name}": ${errorDetails}`
133
+ );
134
+ }
135
+ }
136
+ async function swapContainersForZeroDowntime(service, tempInfo) {
137
+ if (!service.docker) {
138
+ return;
139
+ }
140
+ const { container, image, port } = service.docker;
141
+ if (!image) {
142
+ throw new Error(`Image is required for container swap. Service: ${service.name}`);
143
+ }
144
+ try {
145
+ if (tempInfo.oldContainerExists) {
146
+ console.log(chalk.cyan(` ๐Ÿ”„ Stopping old container "${container}"...`));
147
+ try {
148
+ await execa("docker", ["stop", container]);
149
+ console.log(chalk.green(` โœ… Stopped old container: ${container}`));
150
+ } catch (error) {
151
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
152
+ if (!errorDetails.toLowerCase().includes("already stopped")) {
153
+ console.log(chalk.yellow(` โš ๏ธ Could not stop old container: ${errorDetails}`));
154
+ }
155
+ }
156
+ try {
157
+ await execa("docker", ["rm", container]);
158
+ console.log(chalk.green(` โœ… Removed old container: ${container}`));
159
+ } catch (error) {
160
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
161
+ if (!errorDetails.toLowerCase().includes("no such container") && !errorDetails.toLowerCase().includes("container not found")) {
162
+ console.log(chalk.yellow(` โš ๏ธ Could not remove old container: ${errorDetails}`));
163
+ }
164
+ }
165
+ console.log(chalk.cyan(` ๐Ÿ”„ Creating new container on production port...`));
166
+ const args = [
167
+ "run",
168
+ "-d",
169
+ "--name",
170
+ container,
171
+ "-p",
172
+ `${service.port}:${port}`,
173
+ "--restart",
174
+ "unless-stopped"
175
+ ];
176
+ if (service.environment) {
177
+ for (const [key, value] of Object.entries(service.environment)) {
178
+ args.push("-e", `${key}=${value}`);
179
+ }
180
+ }
181
+ args.push(image);
182
+ try {
183
+ await execa("docker", args);
184
+ console.log(
185
+ chalk.green(
186
+ ` โœ… Created new container "${container}" on production port ${service.port}`
187
+ )
188
+ );
189
+ } catch (error) {
190
+ const errorDetails = error?.stderr || error?.message || String(error) || "Unknown error";
191
+ throw new Error(`Failed to create final container "${container}": ${errorDetails}`);
192
+ }
193
+ }
194
+ } catch (error) {
195
+ const errorDetails = error?.stderr || error?.message || String(error) || "Unknown error";
196
+ throw new Error(`Failed to swap containers for zero-downtime deployment: ${errorDetails}`);
197
+ }
198
+ }
199
+ async function cleanupTempContainer(tempContainerName) {
200
+ try {
201
+ console.log(chalk.cyan(` ๐Ÿงน Cleaning up temporary container "${tempContainerName}"...`));
202
+ try {
203
+ await execa("docker", ["stop", tempContainerName]);
204
+ } catch (error) {
205
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
206
+ if (!errorDetails.toLowerCase().includes("already stopped")) {
207
+ console.log(chalk.yellow(` โš ๏ธ Could not stop temp container: ${errorDetails}`));
208
+ }
209
+ }
210
+ try {
211
+ await execa("docker", ["rm", tempContainerName]);
212
+ console.log(chalk.green(` โœ… Removed temporary container: ${tempContainerName}`));
213
+ } catch (error) {
214
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
215
+ if (!errorDetails.toLowerCase().includes("no such container") && !errorDetails.toLowerCase().includes("container not found")) {
216
+ console.log(chalk.yellow(` โš ๏ธ Could not remove temp container: ${errorDetails}`));
217
+ }
218
+ }
219
+ } catch (error) {
220
+ const errorDetails = error?.stderr || error?.message || String(error) || "Unknown error";
221
+ console.log(chalk.yellow(` โš ๏ธ Error during temp container cleanup: ${errorDetails}`));
222
+ }
223
+ }
2
224
  async function startDockerContainer(service) {
3
225
  if (!service.docker) {
4
226
  return;
5
227
  }
6
228
  const { image, container, port } = service.docker;
7
229
  try {
8
- const { stdout: existingContainers } = await execa("docker", [
9
- "ps",
10
- "-a",
11
- "--filter",
12
- `name=${container}`,
13
- "--format",
14
- "{{.Names}}"
15
- ]);
16
- if (existingContainers.includes(container)) {
17
- const { stdout: runningContainers } = await execa("docker", [
18
- "ps",
19
- "--filter",
20
- `name=${container}`,
21
- "--format",
22
- "{{.Names}}"
23
- ]);
24
- if (!runningContainers.includes(container)) {
25
- await execa("docker", ["start", container]);
26
- }
27
- } else if (image) {
230
+ let containerExists = false;
231
+ let containerState = "";
232
+ try {
233
+ const { stdout } = await execa("docker", ["inspect", "--type", "container", container], {
234
+ stderr: "pipe"
235
+ });
236
+ containerExists = true;
237
+ try {
238
+ const containerInfo = JSON.parse(stdout);
239
+ if (containerInfo && containerInfo[0]) {
240
+ containerState = containerInfo[0].State?.Status || "unknown";
241
+ const isRunning = containerInfo[0].State?.Running || false;
242
+ console.log(
243
+ chalk.dim(
244
+ ` ๐Ÿ“‹ Container "${container}" exists (state: ${containerState}, running: ${isRunning})`
245
+ )
246
+ );
247
+ }
248
+ } catch (parseError) {
249
+ }
250
+ } catch (error) {
251
+ containerExists = false;
252
+ const errorMessage = error?.stderr || error?.message || "";
253
+ if (errorMessage.includes("No such container") || errorMessage.includes("Error: No such object")) {
254
+ console.log(chalk.dim(` ๐Ÿ“‹ Container "${container}" does not exist, will create new one`));
255
+ }
256
+ }
257
+ let shouldCreateNewContainer = true;
258
+ if (containerExists) {
259
+ if (!image) {
260
+ throw new Error(
261
+ `Container "${container}" exists and needs to be recreated for redeployment. No image specified in configuration for service "${service.name}". Please add an "image" field to the docker configuration to allow container recreation.`
262
+ );
263
+ }
264
+ console.log(
265
+ chalk.yellow(` ๐Ÿ”„ Removing existing container "${container}" for redeployment...`)
266
+ );
267
+ try {
268
+ await execa("docker", ["rm", "-f", container]);
269
+ console.log(chalk.green(` โœ… Removed existing container: ${container}`));
270
+ try {
271
+ await execa("docker", ["inspect", "--type", "container", container], {
272
+ stdout: "ignore",
273
+ stderr: "ignore"
274
+ });
275
+ throw new Error(
276
+ `Container "${container}" was not properly removed. Please remove it manually and try again.`
277
+ );
278
+ } catch (verifyError) {
279
+ const verifyMessage = verifyError?.stderr || verifyError?.message || "";
280
+ if (verifyMessage.includes("No such container") || verifyMessage.includes("Error: No such object")) {
281
+ console.log(chalk.dim(` โœ“ Verified container "${container}" was removed`));
282
+ }
283
+ }
284
+ } catch (error) {
285
+ const errorDetails = error?.stderr || error?.message || String(error) || "Unknown error";
286
+ if (errorDetails.toLowerCase().includes("no such container") || errorDetails.toLowerCase().includes("container not found")) {
287
+ console.log(chalk.yellow(` โš ๏ธ Container "${container}" was already removed`));
288
+ } else {
289
+ throw new Error(
290
+ `Failed to remove old container "${container}" for service "${service.name}": ${errorDetails}`
291
+ );
292
+ }
293
+ }
294
+ }
295
+ if (shouldCreateNewContainer && image) {
296
+ try {
297
+ console.log(chalk.dim(` ๐Ÿ“ฅ Pulling latest image: ${image}...`));
298
+ await execa("docker", ["pull", image]);
299
+ console.log(chalk.green(` โœ… Image pulled successfully: ${image}`));
300
+ } catch (error) {
301
+ const errorDetails = error?.stderr || error?.message || "Unknown error";
302
+ console.log(
303
+ chalk.yellow(
304
+ ` โš ๏ธ Failed to pull image ${image}, using existing local image if available`
305
+ )
306
+ );
307
+ console.log(chalk.dim(` Error: ${errorDetails}`));
308
+ }
309
+ try {
310
+ const { stdout: portCheck } = await execa("docker", ["ps", "--format", "{{.Ports}}"]);
311
+ const portPattern = new RegExp(`:${service.port}->`, "g");
312
+ if (portCheck && portPattern.test(portCheck)) {
313
+ throw new Error(
314
+ `Port ${service.port} is already in use by another container. Please use a different port for service "${service.name}".`
315
+ );
316
+ }
317
+ } catch (error) {
318
+ if (error instanceof Error && error.message.includes("Port")) {
319
+ throw error;
320
+ }
321
+ }
28
322
  const args = [
29
323
  "run",
30
324
  "-d",
@@ -32,6 +326,7 @@ async function startDockerContainer(service) {
32
326
  container,
33
327
  "-p",
34
328
  `${service.port}:${port}`,
329
+ // Port binding: host:container
35
330
  "--restart",
36
331
  "unless-stopped"
37
332
  ];
@@ -41,17 +336,93 @@ async function startDockerContainer(service) {
41
336
  }
42
337
  }
43
338
  args.push(image);
44
- await execa("docker", args);
45
- } else {
46
- throw new Error(`Container ${container} not found and no image specified`);
339
+ try {
340
+ await execa("docker", args);
341
+ console.log(chalk.green(` โœ… Created and started container: ${container}`));
342
+ } catch (error) {
343
+ const errorMessage = error?.message || String(error) || "Unknown error";
344
+ const errorStderr = error?.stderr || "";
345
+ const errorStdout = error?.stdout || "";
346
+ const fullError = [errorMessage, errorStderr, errorStdout].filter(Boolean).join("\n").toLowerCase();
347
+ if (fullError.includes("port is already allocated") || fullError.includes("bind: address already in use") || fullError.includes("port already in use") || fullError.includes("port is already in use")) {
348
+ throw new Error(
349
+ `Port ${service.port} is already in use. Please use a different port for service "${service.name}".`
350
+ );
351
+ }
352
+ if (fullError.includes("container name is already in use") || fullError.includes("is already in use")) {
353
+ throw new Error(
354
+ `Container name "${container}" is already in use. This might happen if the container was created between checks. Please remove the container manually or wait a moment and try again.`
355
+ );
356
+ }
357
+ if (fullError.includes("no such image") || fullError.includes("pull access denied") || fullError.includes("repository does not exist")) {
358
+ throw new Error(
359
+ `Docker image "${image}" not found or cannot be accessed. Please verify the image name and ensure you have access to pull it.`
360
+ );
361
+ }
362
+ const details = errorStderr || errorStdout || errorMessage;
363
+ throw new Error(`Failed to create Docker container "${container}": ${details}`);
364
+ }
365
+ } else if (shouldCreateNewContainer && !image) {
366
+ throw new Error(
367
+ `Container "${container}" does not exist and no image specified in configuration. Please either:
368
+ 1. Add an "image" field to the docker configuration for service "${service.name}", or
369
+ 2. Create the container "${container}" manually before deploying.`
370
+ );
371
+ }
372
+ } catch (error) {
373
+ if (error instanceof Error && error.message) {
374
+ if (error.message.includes(container) || error.message.includes(service.name)) {
375
+ throw error;
376
+ }
377
+ throw new Error(
378
+ `Failed to start Docker container "${container}" for service "${service.name}": ${error.message}`
379
+ );
47
380
  }
381
+ const errorDetails = error?.message || error?.stderr || error?.stdout || String(error) || "Unknown error";
382
+ throw new Error(
383
+ `Failed to start Docker container "${container}" for service "${service.name}": ${errorDetails}`
384
+ );
385
+ }
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]);
48
399
  } catch (error) {
49
400
  throw new Error(
50
- `Failed to start Docker container: ${error instanceof Error ? error.message : error}`
401
+ `Failed to remove container ${containerName}: ${error instanceof Error ? error.message : error}`
51
402
  );
52
403
  }
53
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
+ }
54
419
  export {
55
- startDockerContainer
420
+ cleanupTempContainer,
421
+ isContainerRunning,
422
+ removeDockerContainer,
423
+ startDockerContainer,
424
+ startDockerContainerZeroDowntime,
425
+ stopDockerContainer,
426
+ swapContainersForZeroDowntime
56
427
  };
57
428
  //# sourceMappingURL=docker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"docker.js","sources":["../../src/utils/docker.ts"],"sourcesContent":["import { execa } from 'execa'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Start or connect to a Docker container for a service\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\n const { stdout: existingContainers } = await execa('docker', [\n 'ps',\n '-a',\n '--filter',\n `name=${container}`,\n '--format',\n '{{.Names}}',\n ])\n\n if (existingContainers.includes(container)) {\n // Container exists, check if it's running\n const { stdout: runningContainers } = await execa('docker', [\n 'ps',\n '--filter',\n `name=${container}`,\n '--format',\n '{{.Names}}',\n ])\n\n if (!runningContainers.includes(container)) {\n // Container exists but not running, start it\n await execa('docker', ['start', container])\n }\n } else if (image) {\n // Container doesn't exist and image is provided, create and run it\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 // 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 await execa('docker', args)\n } else {\n throw new Error(`Container ${container} not found and no image specified`)\n }\n } catch (error) {\n throw new Error(\n `Failed to start Docker container: ${error instanceof Error ? error.message : error}`\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"],"names":[],"mappings":";AAMA,eAAsB,qBAAqB,SAAuC;AAChF,MAAI,CAAC,QAAQ,QAAQ;AACnB;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,WAAW,KAAA,IAAS,QAAQ;AAE3C,MAAI;AAEF,UAAM,EAAE,QAAQ,mBAAA,IAAuB,MAAM,MAAM,UAAU;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,SAAS;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAED,QAAI,mBAAmB,SAAS,SAAS,GAAG;AAE1C,YAAM,EAAE,QAAQ,kBAAA,IAAsB,MAAM,MAAM,UAAU;AAAA,QAC1D;AAAA,QACA;AAAA,QACA,QAAQ,SAAS;AAAA,QACjB;AAAA,QACA;AAAA,MAAA,CACD;AAED,UAAI,CAAC,kBAAkB,SAAS,SAAS,GAAG;AAE1C,cAAM,MAAM,UAAU,CAAC,SAAS,SAAS,CAAC;AAAA,MAC5C;AAAA,IACF,WAAW,OAAO;AAEhB,YAAM,OAAO;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,GAAG,QAAQ,IAAI,IAAI,IAAI;AAAA,QACvB;AAAA,QACA;AAAA,MAAA;AAIF,UAAI,QAAQ,aAAa;AACvB,mBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAC9D,eAAK,KAAK,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE;AAAA,QACnC;AAAA,MACF;AAEA,WAAK,KAAK,KAAK;AAEf,YAAM,MAAM,UAAU,IAAI;AAAA,IAC5B,OAAO;AACL,YAAM,IAAI,MAAM,aAAa,SAAS,mCAAmC;AAAA,IAC3E;AAAA,EACF,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,IAAA;AAAA,EAEvF;AACF;"}
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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"}