dank-ai 1.0.18 → 1.0.19

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 (2) hide show
  1. package/lib/docker/manager.js +1014 -420
  2. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Docker Container Manager
3
- *
3
+ *
4
4
  * Manages Docker containers for Dank agents including:
5
5
  * - Building agent images
6
6
  * - Starting/stopping containers
@@ -8,32 +8,68 @@
8
8
  * - Managing Docker resources
9
9
  */
10
10
 
11
- const Docker = require('dockerode');
12
- const fs = require('fs-extra');
13
- const path = require('path');
14
- const tar = require('tar');
15
- const winston = require('winston');
16
- const { spawn, exec } = require('child_process');
17
- const { promisify } = require('util');
18
- const { DOCKER_CONFIG } = require('../constants');
19
- const { AgentConfig } = require('../config');
11
+ const Docker = require("dockerode");
12
+ const fs = require("fs-extra");
13
+ const path = require("path");
14
+ const os = require("os");
15
+ const tar = require("tar");
16
+ const winston = require("winston");
17
+ const { spawn, exec } = require("child_process");
18
+ const { promisify } = require("util");
19
+ const { DOCKER_CONFIG } = require("../constants");
20
+ const { AgentConfig } = require("../config");
20
21
 
21
22
  const execAsync = promisify(exec);
22
23
 
23
24
  class DockerManager {
24
25
  constructor(options = {}) {
25
26
  this.docker = new Docker(options.dockerOptions || {});
26
- this.logger = options.logger || winston.createLogger({
27
- level: 'info',
28
- format: winston.format.simple(),
29
- transports: [new winston.transports.Console()]
30
- });
31
-
27
+ this.logger =
28
+ options.logger ||
29
+ winston.createLogger({
30
+ level: "info",
31
+ format: winston.format.simple(),
32
+ transports: [new winston.transports.Console()],
33
+ });
34
+
32
35
  this.defaultBaseImageName = `${DOCKER_CONFIG.baseImagePrefix}:${DOCKER_CONFIG.defaultTag}`;
33
36
  this.networkName = DOCKER_CONFIG.networkName;
34
37
  this.containers = new Map();
35
38
  }
36
39
 
40
+ /**
41
+ * Create Docker client with proper socket detection
42
+ */
43
+ createDockerClient() {
44
+ const fs = require("fs-extra");
45
+ const path = require("path");
46
+
47
+ // Common Docker socket locations
48
+ const socketPaths = [
49
+ "/var/run/docker.sock", // Standard Linux
50
+ "//var/run/docker.sock", // Windows WSL2
51
+ "\\\\.\\pipe\\docker_engine", // Windows named pipe
52
+ path.join(os.homedir(), ".docker", "run", "docker.sock"), // macOS Docker Desktop
53
+ "/Users/Shared/docker.sock", // Alternative macOS location
54
+ ];
55
+
56
+ // Find the first available socket
57
+ for (const socketPath of socketPaths) {
58
+ try {
59
+ if (fs.existsSync(socketPath)) {
60
+ this.logger.info(`Found Docker socket at: ${socketPath}`);
61
+ return new Docker({ socketPath });
62
+ }
63
+ } catch (error) {
64
+ // Continue to next socket path
65
+ }
66
+ }
67
+
68
+ // If no socket found, try default connection (will use environment variables)
69
+ this.logger.info("No Docker socket found, using default connection");
70
+ return new Docker();
71
+ }
72
+
37
73
  /**
38
74
  * Initialize Docker environment
39
75
  */
@@ -41,21 +77,25 @@ class DockerManager {
41
77
  try {
42
78
  // Ensure Docker is available and running
43
79
  await this.ensureDockerAvailable();
44
-
80
+
81
+ // Initialize Docker client with proper socket detection
82
+ this.docker = this.createDockerClient();
83
+
45
84
  // Check Docker connection
46
85
  await this.docker.ping();
47
- this.logger.info('Docker connection established');
86
+ this.logger.info("Docker connection established");
48
87
 
49
88
  // Create network if it doesn't exist
50
89
  await this.ensureNetwork();
51
-
90
+
52
91
  // Check if default base image exists, pull if not found
53
92
  const hasBaseImage = await this.hasImage(this.defaultBaseImageName);
54
93
  if (!hasBaseImage) {
55
- this.logger.info(`Default base image '${this.defaultBaseImageName}' not found. Pulling from registry...`);
94
+ this.logger.info(
95
+ `Default base image '${this.defaultBaseImageName}' not found. Pulling from registry...`
96
+ );
56
97
  await this.pullBaseImage();
57
98
  }
58
-
59
99
  } catch (error) {
60
100
  throw new Error(`Failed to initialize Docker: ${error.message}`);
61
101
  }
@@ -66,22 +106,27 @@ class DockerManager {
66
106
  */
67
107
  async ensureDockerAvailable() {
68
108
  try {
109
+ // Initialize Docker client with proper socket detection
110
+ this.docker = this.createDockerClient();
111
+
69
112
  // First, try to ping Docker to see if it's running
70
113
  await this.docker.ping();
71
- this.logger.info('Docker is running and accessible');
114
+ this.logger.info("Docker is running and accessible");
72
115
  return;
73
116
  } catch (error) {
74
- this.logger.info('Docker is not accessible, checking installation...');
117
+ this.logger.info("Docker is not accessible, checking installation...");
75
118
  }
76
119
 
77
120
  // Check if Docker is installed
78
121
  const isInstalled = await this.isDockerInstalled();
79
-
122
+
80
123
  if (!isInstalled) {
81
- this.logger.info('Docker is not installed. Installing Docker...');
124
+ this.logger.info("Docker is not installed. Installing Docker...");
82
125
  await this.installDocker();
83
126
  } else {
84
- this.logger.info('Docker is installed but not running. Starting Docker...');
127
+ this.logger.info(
128
+ "Docker is installed but not running. Starting Docker..."
129
+ );
85
130
  await this.startDocker();
86
131
  }
87
132
 
@@ -94,33 +139,140 @@ class DockerManager {
94
139
  */
95
140
  async isDockerInstalled() {
96
141
  try {
97
- await execAsync('docker --version');
142
+ // Check if docker command is available using multiple paths
143
+ const dockerPaths = [
144
+ "/usr/local/bin/docker",
145
+ "/opt/homebrew/bin/docker",
146
+ "/usr/bin/docker",
147
+ "docker", // fallback to PATH
148
+ ];
149
+
150
+ let dockerFound = false;
151
+ for (const path of dockerPaths) {
152
+ try {
153
+ await execAsync(`${path} --version`);
154
+ dockerFound = true;
155
+ break;
156
+ } catch (error) {
157
+ // Try next path
158
+ }
159
+ }
160
+
161
+ if (!dockerFound) {
162
+ return false;
163
+ }
164
+
165
+ // On macOS, also check if Docker Desktop is installed
166
+ if (process.platform === "darwin") {
167
+ const dockerAppPath = "/Applications/Docker.app";
168
+ const fs = require("fs-extra");
169
+ if (!(await fs.pathExists(dockerAppPath))) {
170
+ this.logger.info("Docker CLI found but Docker Desktop not installed");
171
+ return false;
172
+ }
173
+ }
174
+
98
175
  return true;
99
176
  } catch (error) {
100
177
  return false;
101
178
  }
102
179
  }
103
180
 
181
+ /**
182
+ * Check if Docker is accessible and running
183
+ */
184
+ async checkDockerAccess() {
185
+ try {
186
+ // Try to run a simple docker command to check if Docker is accessible
187
+ // Use full path to docker command to avoid PATH issues after installation
188
+ const dockerPaths = [
189
+ "/usr/local/bin/docker",
190
+ "/opt/homebrew/bin/docker",
191
+ "/usr/bin/docker",
192
+ "docker", // fallback to PATH
193
+ ];
194
+
195
+ let dockerCommand = "docker";
196
+ let dockerFound = false;
197
+
198
+ for (const path of dockerPaths) {
199
+ try {
200
+ this.logger.info(`Trying Docker path: ${path}`);
201
+ const result = await execAsync(`${path} --version`);
202
+ this.logger.info(`✅ Docker found at: ${path}`);
203
+ this.logger.info(`Docker version: ${result.stdout.trim()}`);
204
+ dockerCommand = path;
205
+ dockerFound = true;
206
+ break;
207
+ } catch (error) {
208
+ this.logger.debug(
209
+ `❌ Docker not found at: ${path} - ${error.message}`
210
+ );
211
+ }
212
+ }
213
+
214
+ if (!dockerFound) {
215
+ throw new Error("Docker executable not found in any expected location");
216
+ }
217
+
218
+ // Test if Docker daemon service is accessible and running
219
+ this.logger.info("🔍 Testing Docker daemon service...");
220
+ try {
221
+ const result = await execAsync(`${dockerCommand} ps`);
222
+ this.logger.info("✅ Docker daemon service is running and accessible");
223
+ this.logger.debug(`Docker ps output: ${result.stdout.trim()}`);
224
+ return true;
225
+ } catch (error) {
226
+ this.logger.error(
227
+ `❌ Docker daemon service not accessible: ${error.message}`
228
+ );
229
+ this.logger.error(`STDERR: ${error.stderr || "No stderr"}`);
230
+
231
+ // Provide helpful error information
232
+ if (error.message.includes("Cannot connect to the Docker daemon")) {
233
+ this.logger.info(
234
+ "💡 Docker daemon is not running. Please start Docker Desktop or Docker service."
235
+ );
236
+ } else if (error.message.includes("permission denied")) {
237
+ this.logger.info(
238
+ "💡 Permission denied. You may need to add your user to the docker group or restart your terminal."
239
+ );
240
+ } else if (error.message.includes("connection refused")) {
241
+ this.logger.info(
242
+ "💡 Connection refused. Docker daemon may not be started yet."
243
+ );
244
+ }
245
+
246
+ throw new Error(
247
+ `Docker daemon service is not accessible: ${error.message}`
248
+ );
249
+ }
250
+ } catch (error) {
251
+ this.logger.error(`Docker access check failed: ${error.message}`);
252
+ throw new Error(`Docker is not accessible: ${error.message}`);
253
+ }
254
+ }
255
+
104
256
  /**
105
257
  * Install Docker on the system
106
258
  */
107
259
  async installDocker() {
108
260
  const platform = process.platform;
109
-
261
+
110
262
  this.logger.info(`Installing Docker for ${platform}...`);
111
-
263
+
112
264
  try {
113
- if (platform === 'darwin') {
265
+ if (platform === "darwin") {
114
266
  await this.installDockerMacOS();
115
- } else if (platform === 'linux') {
267
+ } else if (platform === "linux") {
116
268
  await this.installDockerLinux();
117
- } else if (platform === 'win32') {
269
+ } else if (platform === "win32") {
118
270
  await this.installDockerWindows();
119
271
  } else {
120
272
  throw new Error(`Unsupported platform: ${platform}`);
121
273
  }
122
-
123
- this.logger.info('Docker installation completed');
274
+
275
+ this.logger.info("Docker installation completed");
124
276
  } catch (error) {
125
277
  throw new Error(`Failed to install Docker: ${error.message}`);
126
278
  }
@@ -130,121 +282,421 @@ class DockerManager {
130
282
  * Install Docker on macOS
131
283
  */
132
284
  async installDockerMacOS() {
133
- this.logger.info('Installing Docker Desktop for macOS...');
134
-
135
- // Check if Homebrew is available with multiple methods
136
- let homebrewAvailable = false;
137
- let homebrewPath = 'brew';
138
-
285
+ this.logger.info("Installing Docker Desktop for macOS...");
286
+
139
287
  try {
140
- // Try different ways to detect Homebrew
141
- await execAsync('brew --version');
142
- homebrewAvailable = true;
143
- homebrewPath = 'brew';
288
+ // Download Docker Desktop installer
289
+ await this.downloadDockerInstaller();
290
+
291
+ // Launch the installer
292
+ await this.launchDockerInstaller();
293
+
294
+ // Prompt user to complete installation
295
+ this.promptUserToCompleteInstallation();
296
+
297
+ // Wait for Docker to be installed and available
298
+ await this.waitForDockerInstallation();
144
299
  } catch (error) {
300
+ this.logger.error("Docker installation failed:", error.message);
301
+ throw new Error(`Docker Desktop installation failed: ${error.message}`);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Wait for Docker to be ready and accessible
307
+ */
308
+ async waitForDockerReady(maxWaitTime = 120000) {
309
+ // 2 minutes max wait
310
+ const startTime = Date.now();
311
+ const checkInterval = 5000; // Check every 5 seconds
312
+
313
+ this.logger.info("Checking if Docker is ready...");
314
+
315
+ while (Date.now() - startTime < maxWaitTime) {
145
316
  try {
146
- // Try with full path (Apple Silicon Macs)
147
- await execAsync('/opt/homebrew/bin/brew --version');
148
- homebrewAvailable = true;
149
- homebrewPath = '/opt/homebrew/bin/brew';
150
- } catch (error2) {
151
- try {
152
- // Try with usr/local path (Intel Macs)
153
- await execAsync('/usr/local/bin/brew --version');
154
- homebrewAvailable = true;
155
- homebrewPath = '/usr/local/bin/brew';
156
- } catch (error3) {
157
- // Try to find brew in PATH
158
- try {
159
- const { stdout } = await execAsync('which brew');
160
- if (stdout.trim()) {
161
- await execAsync(`${stdout.trim()} --version`);
162
- homebrewAvailable = true;
163
- homebrewPath = stdout.trim();
164
- }
165
- } catch (error4) {
166
- // Homebrew not found
167
- this.logger.debug('Homebrew detection failed:', error4.message);
168
- }
169
- }
317
+ await this.checkDockerAccess();
318
+ this.logger.info("Docker is ready and accessible!");
319
+ return true;
320
+ } catch (error) {
321
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
322
+ this.logger.info(
323
+ `Docker not ready yet (${elapsed}s elapsed), waiting...`
324
+ );
325
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
170
326
  }
171
327
  }
172
-
173
- if (homebrewAvailable) {
174
- this.logger.info(`Using Homebrew to install Docker Desktop (found at: ${homebrewPath})...`);
175
-
176
- try {
177
- // First, try to update Homebrew to ensure it's working
178
- this.logger.info('Updating Homebrew...');
179
- try {
180
- const updateCommand = `${homebrewPath} update`;
181
- this.logger.info(`🔧 About to run command: ${updateCommand}`);
182
- await this.runCommandWithEnv(updateCommand, 'Updating Homebrew');
183
- } catch (updateError) {
184
- this.logger.warn('Homebrew update failed, continuing with installation...');
328
+
329
+ throw new Error(
330
+ `Docker did not become ready within ${maxWaitTime / 1000} seconds`
331
+ );
332
+ }
333
+
334
+ /**
335
+ * Download a file using Node.js built-in modules with proper binary handling
336
+ */
337
+ async downloadFile(url, filePath) {
338
+ this.logger.info(`Downloading file from ${url} to ${filePath}`);
339
+
340
+ const https = require("https");
341
+ const http = require("http");
342
+ const fs = require("fs-extra");
343
+ const { URL } = require("url");
344
+
345
+ return new Promise((resolve, reject) => {
346
+ const parsedUrl = new URL(url);
347
+ const client = parsedUrl.protocol === "https:" ? https : http;
348
+
349
+ const request = client.get(url, (response) => {
350
+ if (response.statusCode === 200) {
351
+ const file = fs.createWriteStream(filePath);
352
+
353
+ // Handle binary data properly
354
+ response.setEncoding("binary");
355
+
356
+ response.on("data", (chunk) => {
357
+ file.write(chunk, "binary");
358
+ });
359
+
360
+ response.on("end", () => {
361
+ file.end();
362
+ this.logger.info("File downloaded successfully");
363
+ resolve();
364
+ });
365
+
366
+ file.on("error", (error) => {
367
+ fs.unlink(filePath, () => {}); // Delete the file on error
368
+ reject(new Error(`File write error: ${error.message}`));
369
+ });
370
+ } else if (response.statusCode === 302 || response.statusCode === 301) {
371
+ // Handle redirects
372
+ this.logger.info(
373
+ `Following redirect to: ${response.headers.location}`
374
+ );
375
+ this.downloadFile(response.headers.location, filePath)
376
+ .then(resolve)
377
+ .catch(reject);
378
+ } else {
379
+ reject(
380
+ new Error(
381
+ `HTTP error: ${response.statusCode} ${response.statusMessage}`
382
+ )
383
+ );
185
384
  }
186
-
187
- // Try different installation approaches
188
- let installSuccess = false;
189
-
190
- // Approach 1: Direct installation
191
- try {
192
- this.logger.info('Attempting direct installation...');
193
- const command1 = `${homebrewPath} install cask docker`;
194
- this.logger.info(`🔧 About to run command: ${command1}`);
195
- await this.runCommandWithEnv(command1, 'Installing Docker Desktop via Homebrew');
196
- installSuccess = true;
197
- } catch (error1) {
198
- this.logger.warn('Direct installation failed, trying alternative approach...');
199
-
200
- // Approach 2: Try with --force flag
201
- try {
202
- this.logger.info('Attempting installation with --force flag...');
203
- const command2 = `${homebrewPath} install cask --force docker`;
204
- this.logger.info(`🔧 About to run command: ${command2}`);
205
- await this.runCommandWithEnv(command2, 'Installing Docker Desktop via Homebrew (force)');
206
- installSuccess = true;
207
- } catch (error2) {
208
- this.logger.warn('Force installation failed, trying with --no-quarantine flag...');
209
-
210
- // Approach 3: Try with --no-quarantine flag
211
- try {
212
- this.logger.info('Attempting installation with --no-quarantine flag...');
213
- const command3 = `${homebrewPath} install cask --no-quarantine docker`;
214
- this.logger.info(`🔧 About to run command: ${command3}`);
215
- await this.runCommandWithEnv(command3, 'Installing Docker Desktop via Homebrew (no-quarantine)');
216
- installSuccess = true;
217
- } catch (error3) {
218
- this.logger.error('All installation approaches failed');
219
- throw error3;
220
- }
385
+ });
386
+
387
+ request.on("error", (error) => {
388
+ reject(new Error(`Download error: ${error.message}`));
389
+ });
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Download Docker Desktop installer for the current platform
395
+ */
396
+ async downloadDockerInstaller() {
397
+ this.logger.info("Downloading Docker Desktop installer...");
398
+
399
+ try {
400
+ // Determine the correct download URL based on platform and architecture
401
+ let downloadUrl, installerName, installerPath;
402
+
403
+ if (process.platform === "darwin") {
404
+ const arch = process.arch === "arm64" ? "arm64" : "amd64";
405
+ downloadUrl = `https://desktop.docker.com/mac/main/${arch}/Docker.dmg`;
406
+ installerName = "Docker.dmg";
407
+ installerPath = path.join(os.homedir(), "Downloads", installerName);
408
+ } else if (process.platform === "win32") {
409
+ downloadUrl =
410
+ "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe";
411
+ installerName = "Docker Desktop Installer.exe";
412
+ installerPath = path.join(os.homedir(), "Downloads", installerName);
413
+ } else if (process.platform === "linux") {
414
+ downloadUrl =
415
+ "https://desktop.docker.com/linux/main/amd64/docker-desktop-4.46.0-amd64.deb";
416
+ installerName = "docker-desktop.deb";
417
+ installerPath = path.join(os.homedir(), "Downloads", installerName);
418
+ } else {
419
+ throw new Error(`Unsupported platform: ${process.platform}`);
420
+ }
421
+
422
+ this.logger.info(
423
+ `Downloading Docker Desktop for ${process.platform} (${process.arch})...`
424
+ );
425
+ this.logger.info(`URL: ${downloadUrl}`);
426
+ this.logger.info(`Destination: ${installerPath}`);
427
+
428
+ // Download the installer using Node.js built-in modules
429
+ await this.downloadFile(downloadUrl, installerPath);
430
+
431
+ this.logger.info("Docker Desktop installer downloaded successfully");
432
+ return { installerPath, installerName };
433
+ } catch (error) {
434
+ this.logger.error(
435
+ "Failed to download Docker Desktop installer:",
436
+ error.message
437
+ );
438
+ throw new Error(
439
+ `Failed to download Docker Desktop installer: ${error.message}`
440
+ );
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Launch the Docker Desktop installer
446
+ */
447
+ async launchDockerInstaller() {
448
+ this.logger.info("Launching Docker Desktop installer...");
449
+
450
+ try {
451
+ let launchCommand;
452
+
453
+ if (process.platform === "darwin") {
454
+ // Mount DMG and copy Docker.app to Applications
455
+ const dmgPath = path.join(os.homedir(), "Downloads", "Docker.dmg");
456
+
457
+ // Mount the DMG
458
+ const mountCommand = `hdiutil attach "${dmgPath}"`;
459
+ this.logger.info(`🔧 About to run command: ${mountCommand}`);
460
+ await this.runCommand(mountCommand, "Mounting Docker Desktop DMG");
461
+
462
+ // Wait a moment for the mount to complete
463
+ await this.sleep(2000);
464
+
465
+ // Check if the volume was mounted and find the correct path
466
+ const fs = require("fs-extra");
467
+ const possiblePaths = [
468
+ "/Volumes/Docker/Docker.app",
469
+ "/Volumes/Docker.app",
470
+ "/Volumes/Docker Desktop/Docker.app",
471
+ ];
472
+
473
+ let dockerAppPath = null;
474
+ for (const path of possiblePaths) {
475
+ if (await fs.pathExists(path)) {
476
+ dockerAppPath = path;
477
+ break;
221
478
  }
222
479
  }
223
-
224
- if (installSuccess) {
225
- // Try to start Docker Desktop
226
- this.logger.info('Starting Docker Desktop...');
227
- try {
228
- const startCommand = 'open -a Docker';
229
- this.logger.info(`🔧 About to run command: ${startCommand}`);
230
- await this.runCommandWithEnv(startCommand, 'Starting Docker Desktop');
231
- } catch (startError) {
232
- this.logger.warn('Could not start Docker Desktop automatically. Please start it manually from Applications.');
233
- }
480
+
481
+ if (!dockerAppPath) {
482
+ throw new Error("Could not find Docker.app in mounted volume");
234
483
  }
235
-
484
+
485
+ // Copy Docker.app to Applications folder
486
+ this.logger.info("Copying Docker.app to Applications folder...");
487
+ const copyCommand = `cp -R "${dockerAppPath}" /Applications/`;
488
+ this.logger.info(`🔧 About to run command: ${copyCommand}`);
489
+ await this.runCommand(
490
+ copyCommand,
491
+ "Copying Docker.app to Applications"
492
+ );
493
+
494
+ // Unmount the DMG
495
+ this.logger.info("Unmounting DMG...");
496
+ const unmountCommand = "hdiutil detach /Volumes/Docker";
497
+ this.logger.info(`🔧 About to run command: ${unmountCommand}`);
498
+ try {
499
+ await this.runCommand(unmountCommand, "Unmounting DMG");
500
+ } catch (error) {
501
+ this.logger.warn(
502
+ "Could not unmount DMG (may already be unmounted):",
503
+ error.message
504
+ );
505
+ }
506
+
507
+ // Launch Docker Desktop from Applications
508
+ launchCommand = "open -a Docker";
509
+ } else if (process.platform === "win32") {
510
+ const installerPath = path.join(
511
+ os.homedir(),
512
+ "Downloads",
513
+ "Docker Desktop Installer.exe"
514
+ );
515
+ launchCommand = `"${installerPath}"`;
516
+ } else if (process.platform === "linux") {
517
+ const installerPath = path.join(
518
+ os.homedir(),
519
+ "Downloads",
520
+ "docker-desktop.deb"
521
+ );
522
+ launchCommand = `sudo dpkg -i "${installerPath}"`;
523
+ }
524
+
525
+ this.logger.info(`🔧 About to run command: ${launchCommand}`);
526
+ await this.runCommand(
527
+ launchCommand,
528
+ "Launching Docker Desktop installer"
529
+ );
530
+
531
+ this.logger.info("Docker Desktop installer launched successfully");
532
+ } catch (error) {
533
+ this.logger.error(
534
+ "Failed to launch Docker Desktop installer:",
535
+ error.message
536
+ );
537
+ throw new Error(
538
+ `Failed to launch Docker Desktop installer: ${error.message}`
539
+ );
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Prompt user to complete Docker installation
545
+ */
546
+ promptUserToCompleteInstallation() {
547
+ console.log("\n" + "=".repeat(60));
548
+ console.log("🐳 DOCKER INSTALLATION REQUIRED");
549
+ console.log("=".repeat(60));
550
+ console.log("");
551
+ console.log("The Docker Desktop installer has been launched.");
552
+ console.log("Please follow these steps to complete the installation:");
553
+ console.log("");
554
+
555
+ if (process.platform === "darwin") {
556
+ console.log(
557
+ "1. Docker.app has been automatically copied to Applications"
558
+ );
559
+ console.log("2. Docker Desktop should launch automatically");
560
+ console.log("3. Follow the setup wizard to complete installation");
561
+ console.log("4. Start Docker Desktop when prompted");
562
+ } else if (process.platform === "win32") {
563
+ console.log("1. The Docker Desktop installer should open automatically");
564
+ console.log("2. Follow the installation wizard");
565
+ console.log("3. Restart your computer if prompted");
566
+ console.log("4. Launch Docker Desktop from Start Menu");
567
+ } else if (process.platform === "linux") {
568
+ console.log("1. The Docker Desktop package has been installed");
569
+ console.log("2. Launch Docker Desktop from your applications menu");
570
+ console.log("3. Follow the setup wizard to complete installation");
571
+ }
572
+
573
+ console.log("");
574
+ console.log("⏳ Waiting for Docker to be installed and started...");
575
+ console.log(" (This may take a few minutes)");
576
+ console.log("");
577
+ console.log("=".repeat(60));
578
+ }
579
+
580
+ /**
581
+ * Wait for Docker to be installed and available
582
+ */
583
+ async waitForDockerInstallation() {
584
+ this.logger.info("Waiting for Docker to be installed and available...");
585
+
586
+ const maxWaitTime = 300000; // 5 minutes max wait
587
+ const checkInterval = 5000; // Check every 5 seconds
588
+ const startTime = Date.now();
589
+
590
+ while (Date.now() - startTime < maxWaitTime) {
591
+ try {
592
+ // Check if Docker is installed and running
593
+ await this.checkDockerAccess();
594
+ this.logger.info("Docker is installed and running!");
595
+ return true;
236
596
  } catch (error) {
237
- this.logger.warn('Homebrew found but installation failed. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
238
- this.logger.error(`Installation error: ${error.message}`);
239
- this.logger.info('You can also try installing Docker Desktop manually:');
240
- this.logger.info('1. Download from: https://www.docker.com/products/docker-desktop/');
241
- this.logger.info('2. Or try: brew install cask docker');
242
- throw new Error('Docker Desktop installation via Homebrew failed. Please install manually from https://www.docker.com/products/docker-desktop/');
597
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
598
+ const remaining = Math.round(
599
+ (maxWaitTime - (Date.now() - startTime)) / 1000
600
+ );
601
+
602
+ this.logger.info(
603
+ `Docker not ready yet (${elapsed}s elapsed, ${remaining}s remaining)...`
604
+ );
605
+
606
+ // Show progress every 30 seconds
607
+ if (elapsed % 30 === 0) {
608
+ console.log(`⏳ Still waiting for Docker... (${elapsed}s elapsed)`);
609
+ }
610
+
611
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
243
612
  }
244
- } else {
245
- this.logger.warn('Homebrew not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
246
- this.logger.info('You can install Homebrew by running: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
247
- throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
613
+ }
614
+
615
+ throw new Error(
616
+ `Docker did not become available within ${
617
+ maxWaitTime / 1000
618
+ } seconds. Please ensure Docker Desktop is installed and running.`
619
+ );
620
+ }
621
+
622
+ /**
623
+ * Install Docker Desktop manually by downloading and installing the DMG
624
+ */
625
+ async installDockerDesktopManually() {
626
+ this.logger.info("Downloading Docker Desktop manually...");
627
+
628
+ try {
629
+ // Determine the correct download URL based on architecture
630
+ const arch = process.arch === "arm64" ? "arm64" : "amd64";
631
+ const downloadUrl = `https://desktop.docker.com/mac/main/${arch}/Docker.dmg`;
632
+ const dmgPath = path.join(os.homedir(), "Downloads", "Docker.dmg");
633
+
634
+ this.logger.info(`Downloading Docker Desktop for ${arch}...`);
635
+ this.logger.info(`URL: ${downloadUrl}`);
636
+ this.logger.info(`Destination: ${dmgPath}`);
637
+
638
+ // Download the DMG file
639
+ const downloadCommand = `curl -L "${downloadUrl}" -o "${dmgPath}"`;
640
+ this.logger.info(`🔧 About to run command: ${downloadCommand}`);
641
+ await this.runCommandWithEnv(
642
+ downloadCommand,
643
+ "Downloading Docker Desktop"
644
+ );
645
+
646
+ // Mount the DMG
647
+ this.logger.info("Mounting Docker Desktop DMG...");
648
+ const mountCommand = `hdiutil attach "${dmgPath}"`;
649
+ this.logger.info(`🔧 About to run command: ${mountCommand}`);
650
+ await this.runCommandWithEnv(mountCommand, "Mounting Docker Desktop DMG");
651
+
652
+ // Copy Docker.app to Applications
653
+ this.logger.info("Installing Docker Desktop to Applications...");
654
+ const copyCommand = 'cp -R "/Volumes/Docker/Docker.app" /Applications/';
655
+ this.logger.info(`🔧 About to run command: ${copyCommand}`);
656
+ await this.runCommandWithEnv(copyCommand, "Installing Docker Desktop");
657
+
658
+ // Unmount the DMG
659
+ this.logger.info("Unmounting Docker Desktop DMG...");
660
+ const unmountCommand = "hdiutil detach /Volumes/Docker";
661
+ this.logger.info(`🔧 About to run command: ${unmountCommand}`);
662
+ await this.runCommandWithEnv(
663
+ unmountCommand,
664
+ "Unmounting Docker Desktop DMG"
665
+ );
666
+
667
+ // Clean up the DMG file
668
+ this.logger.info("Cleaning up downloaded DMG...");
669
+ const cleanupCommand = `rm "${dmgPath}"`;
670
+ this.logger.info(`🔧 About to run command: ${cleanupCommand}`);
671
+ await this.runCommandWithEnv(cleanupCommand, "Cleaning up DMG file");
672
+
673
+ this.logger.info(
674
+ "Docker Desktop installed successfully via manual download"
675
+ );
676
+
677
+ // Try to start Docker Desktop
678
+ this.logger.info("Starting Docker Desktop...");
679
+ try {
680
+ const startCommand = "open -a Docker";
681
+ this.logger.info(`🔧 About to run command: ${startCommand}`);
682
+ await this.runCommandWithEnv(startCommand, "Starting Docker Desktop");
683
+
684
+ // Wait for Docker to be ready
685
+ this.logger.info("Waiting for Docker Desktop to start...");
686
+ await this.waitForDockerReady();
687
+ } catch (startError) {
688
+ this.logger.warn(
689
+ "Could not start Docker Desktop automatically. Please start it manually from Applications."
690
+ );
691
+ }
692
+ } catch (error) {
693
+ this.logger.error(
694
+ "Manual Docker Desktop installation failed:",
695
+ error.message
696
+ );
697
+ throw new Error(
698
+ `Manual Docker Desktop installation failed: ${error.message}`
699
+ );
248
700
  }
249
701
  }
250
702
 
@@ -252,34 +704,23 @@ class DockerManager {
252
704
  * Install Docker on Linux
253
705
  */
254
706
  async installDockerLinux() {
255
- this.logger.info('Installing Docker on Linux...');
256
-
707
+ this.logger.info("Installing Docker on Linux...");
708
+
257
709
  try {
258
- // Update package index
259
- await this.runCommand('sudo apt-get update', 'Updating package index');
260
-
261
- // Install prerequisites
262
- await this.runCommand('sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release', 'Installing prerequisites');
263
-
264
- // Add Docker's official GPG key
265
- await this.runCommand('curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg', 'Adding Docker GPG key');
266
-
267
- // Add Docker repository
268
- await this.runCommand('echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', 'Adding Docker repository');
269
-
270
- // Update package index again
271
- await this.runCommand('sudo apt-get update', 'Updating package index with Docker repository');
272
-
273
- // Install Docker
274
- await this.runCommand('sudo apt-get install -y docker-ce docker-ce-cli containerd.io', 'Installing Docker');
275
-
276
- // Add current user to docker groupl
277
- await this.runCommand('sudo usermod -aG docker $USER', 'Adding user to docker group');
278
-
279
- this.logger.info('Docker installation completed. You may need to log out and back in for group changes to take effect.');
280
-
710
+ // Download Docker Desktop installer
711
+ await this.downloadDockerInstaller();
712
+
713
+ // Launch the installer
714
+ await this.launchDockerInstaller();
715
+
716
+ // Prompt user to complete installation
717
+ this.promptUserToCompleteInstallation();
718
+
719
+ // Wait for Docker to be installed and available
720
+ await this.waitForDockerInstallation();
281
721
  } catch (error) {
282
- throw new Error(`Failed to install Docker on Linux: ${error.message}`);
722
+ this.logger.error("Docker installation failed:", error.message);
723
+ throw new Error(`Docker Desktop installation failed: ${error.message}`);
283
724
  }
284
725
  }
285
726
 
@@ -287,19 +728,23 @@ class DockerManager {
287
728
  * Install Docker on Windows
288
729
  */
289
730
  async installDockerWindows() {
290
- this.logger.info('Installing Docker Desktop for Windows...');
291
-
292
- // Check if Chocolatey is available
731
+ this.logger.info("Installing Docker Desktop for Windows...");
732
+
293
733
  try {
294
- await execAsync('choco --version');
295
- this.logger.info('Using Chocolatey to install Docker Desktop...');
296
-
297
- // Install Docker Desktop via Chocolatey
298
- await this.runCommand('choco install docker-desktop -y', 'Installing Docker Desktop via Chocolatey');
299
-
734
+ // Download Docker Desktop installer
735
+ await this.downloadDockerInstaller();
736
+
737
+ // Launch the installer
738
+ await this.launchDockerInstaller();
739
+
740
+ // Prompt user to complete installation
741
+ this.promptUserToCompleteInstallation();
742
+
743
+ // Wait for Docker to be installed and available
744
+ await this.waitForDockerInstallation();
300
745
  } catch (error) {
301
- this.logger.warn('Chocolatey not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
302
- throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
746
+ this.logger.error("Docker installation failed:", error.message);
747
+ throw new Error(`Docker Desktop installation failed: ${error.message}`);
303
748
  }
304
749
  }
305
750
 
@@ -308,24 +753,33 @@ class DockerManager {
308
753
  */
309
754
  async startDocker() {
310
755
  const platform = process.platform;
311
-
756
+
312
757
  try {
313
- if (platform === 'darwin') {
758
+ if (platform === "darwin") {
314
759
  // On macOS, try to start Docker Desktop
315
- await this.runCommand('open -a Docker', 'Starting Docker Desktop');
316
- } else if (platform === 'linux') {
760
+ await this.runCommand("open -a Docker", "Starting Docker Desktop");
761
+ } else if (platform === "linux") {
317
762
  // On Linux, start Docker service
318
- await this.runCommand('sudo systemctl start docker', 'Starting Docker service');
319
- await this.runCommand('sudo systemctl enable docker', 'Enabling Docker service');
320
- } else if (platform === 'win32') {
763
+ await this.runCommand(
764
+ "sudo systemctl start docker",
765
+ "Starting Docker service"
766
+ );
767
+ await this.runCommand(
768
+ "sudo systemctl enable docker",
769
+ "Enabling Docker service"
770
+ );
771
+ } else if (platform === "win32") {
321
772
  // On Windows, try to start Docker Desktop
322
- await this.runCommand('start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', 'Starting Docker Desktop');
773
+ await this.runCommand(
774
+ 'start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"',
775
+ "Starting Docker Desktop"
776
+ );
323
777
  }
324
-
325
- this.logger.info('Docker service started');
778
+
779
+ this.logger.info("Docker service started");
326
780
  } catch (error) {
327
781
  this.logger.warn(`Failed to start Docker service: ${error.message}`);
328
- this.logger.info('Please start Docker manually and try again');
782
+ this.logger.info("Please start Docker manually and try again");
329
783
  throw error;
330
784
  }
331
785
  }
@@ -334,25 +788,85 @@ class DockerManager {
334
788
  * Wait for Docker to become available
335
789
  */
336
790
  async waitForDocker() {
337
- this.logger.info('Waiting for Docker to become available...');
338
-
339
- const maxAttempts = 30; // 30 seconds
340
- const delay = 1000; // 1 second
341
-
342
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
791
+ const maxWaitTime = 300000; // 5 minutes
792
+ const checkInterval = 3000; // 3 seconds
793
+ const stabilityCheckDuration = 10000; // 10 seconds of consistent readiness
794
+ const stabilityCheckInterval = 2000; // Check every 2 seconds during stability phase
795
+ let elapsedTime = 0;
796
+
797
+ this.logger.info(
798
+ "⏳ Waiting for Docker daemon service to become available..."
799
+ );
800
+ this.logger.info("This may take a few minutes if Docker is starting up...");
801
+
802
+ while (elapsedTime < maxWaitTime) {
343
803
  try {
344
- await this.docker.ping();
345
- this.logger.info('Docker is now available');
804
+ await this.checkDockerAccess();
805
+ this.logger.info("✅ Docker daemon detected! Verifying stability...");
806
+
807
+ // Wait for Docker to be consistently ready for stabilityCheckDuration
808
+ const stabilityStartTime = Date.now();
809
+ let consecutiveSuccessfulChecks = 0;
810
+ const requiredConsecutiveChecks = Math.ceil(
811
+ stabilityCheckDuration / stabilityCheckInterval
812
+ );
813
+
814
+ while (Date.now() - stabilityStartTime < stabilityCheckDuration) {
815
+ try {
816
+ await this.checkDockerAccess();
817
+ consecutiveSuccessfulChecks++;
818
+ this.logger.info(
819
+ `Stability check ${consecutiveSuccessfulChecks}/${requiredConsecutiveChecks} passed`
820
+ );
821
+
822
+ if (consecutiveSuccessfulChecks >= requiredConsecutiveChecks) {
823
+ this.logger.info("✅ Docker daemon is stable and ready!");
824
+ return;
825
+ }
826
+ } catch (error) {
827
+ this.logger.info(
828
+ "Docker daemon became unstable, restarting stability check..."
829
+ );
830
+ consecutiveSuccessfulChecks = 0;
831
+ }
832
+
833
+ await this.sleep(stabilityCheckInterval);
834
+ }
835
+
836
+ // If we get here, stability check timed out
837
+ this.logger.warn(
838
+ "Docker daemon stability check timed out, continuing anyway..."
839
+ );
346
840
  return;
347
841
  } catch (error) {
348
- if (attempt === maxAttempts) {
349
- throw new Error('Docker did not become available within the expected time');
842
+ const remainingTime = Math.round((maxWaitTime - elapsedTime) / 1000);
843
+ const elapsedSeconds = Math.round(elapsedTime / 1000);
844
+
845
+ if (elapsedSeconds % 30 === 0) {
846
+ // Log every 30 seconds
847
+ this.logger.info(
848
+ `Docker daemon service not ready yet (${elapsedSeconds}s elapsed, ${remainingTime}s remaining)...`
849
+ );
850
+ this.logger.info(`Last error: ${error.message}`);
350
851
  }
351
-
352
- this.logger.info(`Waiting for Docker... (${attempt}/${maxAttempts})`);
353
- await this.sleep(delay);
852
+
853
+ await this.sleep(checkInterval);
854
+ elapsedTime += checkInterval;
354
855
  }
355
856
  }
857
+
858
+ this.logger.error(
859
+ "❌ Docker daemon service did not become available within the expected time"
860
+ );
861
+ this.logger.error("Please check:");
862
+ this.logger.error(
863
+ "1. Docker is installed and the daemon service is running"
864
+ );
865
+ this.logger.error("2. Docker daemon has finished starting up");
866
+ this.logger.error("3. You have permission to access Docker");
867
+ this.logger.error('4. Try running "docker ps" manually to test');
868
+
869
+ throw new Error("Docker did not become available within the expected time");
356
870
  }
357
871
 
358
872
  /**
@@ -361,36 +875,38 @@ class DockerManager {
361
875
  async runCommand(command, description) {
362
876
  this.logger.info(`${description}...`);
363
877
  this.logger.debug(`Executing command: ${command}`);
364
-
878
+
365
879
  return new Promise((resolve, reject) => {
366
- const child = spawn('sh', ['-c', command], {
367
- stdio: ['inherit', 'pipe', 'pipe'],
368
- shell: true
880
+ const child = spawn(command, {
881
+ stdio: ["inherit", "pipe", "pipe"],
882
+ shell: true,
369
883
  });
370
-
371
- let stdout = '';
372
- let stderr = '';
373
-
374
- child.stdout.on('data', (data) => {
884
+
885
+ let stdout = "";
886
+ let stderr = "";
887
+
888
+ child.stdout.on("data", (data) => {
375
889
  const output = data.toString();
376
890
  stdout += output;
377
891
  // Log output in debug mode
378
892
  this.logger.debug(`STDOUT: ${output.trim()}`);
379
893
  });
380
-
381
- child.stderr.on('data', (data) => {
894
+
895
+ child.stderr.on("data", (data) => {
382
896
  const output = data.toString();
383
897
  stderr += output;
384
898
  // Log stderr in debug mode
385
899
  this.logger.debug(`STDERR: ${output.trim()}`);
386
900
  });
387
-
388
- child.on('close', (code) => {
901
+
902
+ child.on("close", (code) => {
389
903
  if (code === 0) {
390
904
  this.logger.info(`${description} completed successfully`);
391
905
  resolve({ stdout, stderr });
392
906
  } else {
393
- const error = new Error(`Command failed with exit code ${code}: ${stderr}`);
907
+ const error = new Error(
908
+ `Command failed with exit code ${code}: ${stderr}`
909
+ );
394
910
  this.logger.error(`${description} failed: ${error.message}`);
395
911
  this.logger.error(`Command: ${command}`);
396
912
  this.logger.error(`STDOUT: ${stdout}`);
@@ -398,8 +914,8 @@ class DockerManager {
398
914
  reject(error);
399
915
  }
400
916
  });
401
-
402
- child.on('error', (error) => {
917
+
918
+ child.on("error", (error) => {
403
919
  this.logger.error(`${description} failed: ${error.message}`);
404
920
  this.logger.error(`Command: ${command}`);
405
921
  reject(error);
@@ -413,51 +929,74 @@ class DockerManager {
413
929
  async runCommandWithEnv(command, description) {
414
930
  this.logger.info(`${description}...`);
415
931
  this.logger.debug(`Executing command: ${command}`);
416
-
932
+
417
933
  return new Promise((resolve, reject) => {
418
934
  // Set up environment for Homebrew
935
+ const homebrewPaths = [
936
+ "/opt/homebrew/bin",
937
+ "/usr/local/bin",
938
+ "/opt/homebrew/sbin",
939
+ "/usr/local/sbin",
940
+ ];
941
+
942
+ const existingPath = process.env.PATH || "";
943
+ const allPaths = [...homebrewPaths, ...existingPath.split(":")];
944
+ const uniquePaths = [...new Set(allPaths.filter((p) => p))];
945
+ const effectivePath = uniquePaths.join(":");
946
+
419
947
  const env = {
420
948
  ...process.env,
421
- PATH: process.env.PATH,
422
- HOMEBREW_NO_AUTO_UPDATE: '1', // Prevent auto-update during installation
423
- HOMEBREW_NO_INSTALL_CLEANUP: '1' // Keep installation files
949
+ PATH: effectivePath,
950
+ HOMEBREW_NO_AUTO_UPDATE: "1",
951
+ HOMEBREW_NO_INSTALL_CLEANUP: "1",
952
+ HOMEBREW_PREFIX: "/opt/homebrew",
953
+ HOMEBREW_CELLAR: "/opt/homebrew/Cellar",
954
+ HOMEBREW_REPOSITORY: "/opt/homebrew",
955
+ HOMEBREW_SHELLENV_PREFIX: "/opt/homebrew",
424
956
  };
425
-
957
+
426
958
  // Debug: Log environment details
427
959
  this.logger.debug(`Environment PATH: ${env.PATH}`);
428
- this.logger.debug(`HOMEBREW_NO_AUTO_UPDATE: ${env.HOMEBREW_NO_AUTO_UPDATE}`);
429
- this.logger.debug(`HOMEBREW_NO_INSTALL_CLEANUP: ${env.HOMEBREW_NO_INSTALL_CLEANUP}`);
430
-
431
- // Use bash instead of sh for better environment support
432
- const child = spawn('bash', ['-c', command], {
433
- stdio: ['inherit', 'pipe', 'pipe'],
434
- shell: true,
435
- env: env
960
+ this.logger.debug(`HOMEBREW_PREFIX: ${env.HOMEBREW_PREFIX}`);
961
+ this.logger.debug(
962
+ `HOMEBREW_NO_AUTO_UPDATE: ${env.HOMEBREW_NO_AUTO_UPDATE}`
963
+ );
964
+ this.logger.debug(
965
+ `HOMEBREW_NO_INSTALL_CLEANUP: ${env.HOMEBREW_NO_INSTALL_CLEANUP}`
966
+ );
967
+
968
+ // Use zsh with login shell for better environment support on macOS
969
+ const child = spawn("zsh", ["-l", "-c", command], {
970
+ stdio: ["inherit", "pipe", "pipe"],
971
+ shell: false,
972
+ env: env,
436
973
  });
437
-
438
- let stdout = '';
439
- let stderr = '';
440
-
441
- child.stdout.on('data', (data) => {
974
+
975
+ let stdout = "";
976
+ let stderr = "";
977
+
978
+ child.stdout.on("data", (data) => {
442
979
  const output = data.toString();
443
980
  stdout += output;
444
981
  // Log output in debug mode
445
982
  this.logger.debug(`STDOUT: ${output.trim()}`);
446
983
  });
447
-
448
- child.stderr.on('data', (data) => {
984
+
985
+ child.stderr.on("data", (data) => {
449
986
  const output = data.toString();
450
987
  stderr += output;
451
988
  // Log stderr in debug mode
452
989
  this.logger.debug(`STDERR: ${output.trim()}`);
453
990
  });
454
-
455
- child.on('close', (code) => {
991
+
992
+ child.on("close", (code) => {
456
993
  if (code === 0) {
457
994
  this.logger.info(`${description} completed successfully`);
458
995
  resolve({ stdout, stderr });
459
996
  } else {
460
- const error = new Error(`Command failed with exit code ${code}: ${stderr}`);
997
+ const error = new Error(
998
+ `Command failed with exit code ${code}: ${stderr}`
999
+ );
461
1000
  this.logger.error(`${description} failed: ${error.message}`);
462
1001
  this.logger.error(`Command: ${command}`);
463
1002
  this.logger.error(`STDOUT: ${stdout}`);
@@ -465,8 +1004,8 @@ class DockerManager {
465
1004
  reject(error);
466
1005
  }
467
1006
  });
468
-
469
- child.on('error', (error) => {
1007
+
1008
+ child.on("error", (error) => {
470
1009
  this.logger.error(`${description} failed: ${error.message}`);
471
1010
  this.logger.error(`Command: ${command}`);
472
1011
  reject(error);
@@ -478,7 +1017,7 @@ class DockerManager {
478
1017
  * Sleep utility
479
1018
  */
480
1019
  sleep(ms) {
481
- return new Promise(resolve => setTimeout(resolve, ms));
1020
+ return new Promise((resolve) => setTimeout(resolve, ms));
482
1021
  }
483
1022
 
484
1023
  /**
@@ -487,20 +1026,21 @@ class DockerManager {
487
1026
  async pullBaseImage(baseImageName = null, options = {}) {
488
1027
  const imageName = baseImageName || this.defaultBaseImageName;
489
1028
  this.logger.info(`Pulling base Docker image: ${imageName}`);
490
-
1029
+
491
1030
  try {
492
1031
  const stream = await this.docker.pull(imageName);
493
-
494
- await this.followPullProgress(stream, 'Base image pull');
495
-
1032
+
1033
+ await this.followPullProgress(stream, "Base image pull");
1034
+
496
1035
  // Verify the image was pulled
497
1036
  const hasImage = await this.hasImage(imageName);
498
1037
  if (hasImage) {
499
1038
  this.logger.info(`Base image '${imageName}' pulled successfully`);
500
1039
  } else {
501
- throw new Error(`Base image '${imageName}' was not pulled successfully`);
1040
+ throw new Error(
1041
+ `Base image '${imageName}' was not pulled successfully`
1042
+ );
502
1043
  }
503
-
504
1044
  } catch (error) {
505
1045
  throw new Error(`Failed to pull base image: ${error.message}`);
506
1046
  }
@@ -510,55 +1050,61 @@ class DockerManager {
510
1050
  * Clean up existing containers from previous runs
511
1051
  */
512
1052
  async cleanupExistingContainers(agents) {
513
- this.logger.info('Cleaning up existing containers from previous runs...');
514
-
1053
+ this.logger.info("Cleaning up existing containers from previous runs...");
1054
+
515
1055
  try {
516
1056
  // Get all containers (running and stopped) that match our agent naming pattern
517
1057
  const containers = await this.docker.listContainers({ all: true });
518
-
519
- const agentNames = agents.map(agent => agent.name.toLowerCase());
520
- const containersToCleanup = containers.filter(container => {
1058
+
1059
+ const agentNames = agents.map((agent) => agent.name.toLowerCase());
1060
+ const containersToCleanup = containers.filter((container) => {
521
1061
  // Check if container name matches our dank agent pattern
522
- const containerName = container.Names[0].replace(/^\//, ''); // Remove leading slash
523
- return agentNames.some(agentName =>
524
- containerName.startsWith(`dank-${agentName}-`) ||
525
- containerName === `dank-${agentName}`
1062
+ const containerName = container.Names[0].replace(/^\//, ""); // Remove leading slash
1063
+ return agentNames.some(
1064
+ (agentName) =>
1065
+ containerName.startsWith(`dank-${agentName}-`) ||
1066
+ containerName === `dank-${agentName}`
526
1067
  );
527
1068
  });
528
1069
 
529
1070
  if (containersToCleanup.length === 0) {
530
- this.logger.info('No existing containers found to cleanup');
1071
+ this.logger.info("No existing containers found to cleanup");
531
1072
  return;
532
1073
  }
533
1074
 
534
- this.logger.info(`Found ${containersToCleanup.length} existing containers to cleanup`);
1075
+ this.logger.info(
1076
+ `Found ${containersToCleanup.length} existing containers to cleanup`
1077
+ );
535
1078
 
536
1079
  // Stop and remove each container
537
1080
  for (const containerInfo of containersToCleanup) {
538
1081
  const container = this.docker.getContainer(containerInfo.Id);
539
- const containerName = containerInfo.Names[0].replace(/^\//, '');
540
-
1082
+ const containerName = containerInfo.Names[0].replace(/^\//, "");
1083
+
541
1084
  try {
542
1085
  // Stop container if running
543
- if (containerInfo.State === 'running') {
1086
+ if (containerInfo.State === "running") {
544
1087
  this.logger.info(`Stopping container: ${containerName}`);
545
1088
  await container.stop({ t: 10 }); // 10 second timeout
546
1089
  }
547
-
1090
+
548
1091
  // Remove container
549
1092
  this.logger.info(`Removing container: ${containerName}`);
550
1093
  await container.remove({ force: true });
551
-
552
1094
  } catch (error) {
553
1095
  // Log but don't fail if we can't clean up a specific container
554
- this.logger.warn(`Failed to cleanup container ${containerName}: ${error.message}`);
1096
+ this.logger.warn(
1097
+ `Failed to cleanup container ${containerName}: ${error.message}`
1098
+ );
555
1099
  }
556
1100
  }
557
1101
 
558
- this.logger.info('Container cleanup completed');
559
-
1102
+ this.logger.info("Container cleanup completed");
560
1103
  } catch (error) {
561
- this.logger.error('Failed to cleanup existing containers:', error.message);
1104
+ this.logger.error(
1105
+ "Failed to cleanup existing containers:",
1106
+ error.message
1107
+ );
562
1108
  // Don't throw - we want to continue even if cleanup fails
563
1109
  }
564
1110
  }
@@ -572,22 +1118,21 @@ class DockerManager {
572
1118
 
573
1119
  try {
574
1120
  const buildContext = await this.createAgentBuildContext(agent);
575
-
1121
+
576
1122
  const stream = await this.docker.buildImage(buildContext, {
577
1123
  t: imageName,
578
- dockerfile: 'Dockerfile',
579
- nocache: options.rebuild || options.noCache || false
1124
+ dockerfile: "Dockerfile",
1125
+ nocache: options.rebuild || options.noCache || false,
580
1126
  });
581
1127
 
582
1128
  await this.followBuildProgress(stream, `Agent ${agent.name} build`);
583
-
1129
+
584
1130
  this.logger.info(`Agent image '${imageName}' built successfully`);
585
-
1131
+
586
1132
  // Clean up build context
587
1133
  await fs.remove(buildContext);
588
-
1134
+
589
1135
  return imageName;
590
-
591
1136
  } catch (error) {
592
1137
  throw new Error(`Failed to build agent image: ${error.message}`);
593
1138
  }
@@ -598,49 +1143,54 @@ class DockerManager {
598
1143
  */
599
1144
  async buildProductionImage(agent, options = {}) {
600
1145
  const {
601
- tag = 'latest',
1146
+ tag = "latest",
602
1147
  registry,
603
1148
  namespace,
604
1149
  force = false,
605
- push = false
1150
+ push = false,
606
1151
  } = options;
607
1152
 
608
1153
  // Construct production image name
609
1154
  let imageName = agent.name.toLowerCase();
610
-
1155
+
611
1156
  // Add namespace if provided
612
1157
  if (namespace) {
613
1158
  imageName = `${namespace}/${imageName}`;
614
1159
  }
615
-
1160
+
616
1161
  // Add registry if provided
617
1162
  if (registry) {
618
1163
  imageName = `${registry}/${imageName}`;
619
1164
  }
620
-
1165
+
621
1166
  // Add tag
622
1167
  imageName = `${imageName}:${tag}`;
623
1168
 
624
- this.logger.info(`Building production image for agent: ${agent.name} -> ${imageName}`);
1169
+ this.logger.info(
1170
+ `Building production image for agent: ${agent.name} -> ${imageName}`
1171
+ );
625
1172
 
626
1173
  try {
627
1174
  const buildContext = await this.createAgentBuildContext(agent);
628
-
1175
+
629
1176
  const stream = await this.docker.buildImage(buildContext, {
630
1177
  t: imageName,
631
- dockerfile: 'Dockerfile',
632
- nocache: force
1178
+ dockerfile: "Dockerfile",
1179
+ nocache: force,
633
1180
  });
634
1181
 
635
- await this.followBuildProgress(stream, `Production build for ${agent.name}`);
636
-
1182
+ await this.followBuildProgress(
1183
+ stream,
1184
+ `Production build for ${agent.name}`
1185
+ );
1186
+
637
1187
  this.logger.info(`Production image '${imageName}' built successfully`);
638
-
1188
+
639
1189
  // Clean up build context
640
1190
  await fs.remove(buildContext);
641
-
1191
+
642
1192
  let pushed = false;
643
-
1193
+
644
1194
  // Push to registry if requested
645
1195
  if (push) {
646
1196
  try {
@@ -650,16 +1200,17 @@ class DockerManager {
650
1200
  this.logger.info(`Successfully pushed image: ${imageName}`);
651
1201
  pushed = true;
652
1202
  } catch (pushError) {
653
- this.logger.warn(`Failed to push image ${imageName}: ${pushError.message}`);
1203
+ this.logger.warn(
1204
+ `Failed to push image ${imageName}: ${pushError.message}`
1205
+ );
654
1206
  // Don't fail the build if push fails
655
1207
  }
656
1208
  }
657
-
1209
+
658
1210
  return {
659
1211
  imageName,
660
- pushed
1212
+ pushed,
661
1213
  };
662
-
663
1214
  } catch (error) {
664
1215
  throw new Error(`Failed to build production image: ${error.message}`);
665
1216
  }
@@ -670,21 +1221,21 @@ class DockerManager {
670
1221
  */
671
1222
  generateHandlersCode(agent) {
672
1223
  const handlers = {};
673
-
1224
+
674
1225
  // Add default handlers
675
1226
  handlers.output = ['(data) => console.log("Output:", data)'];
676
1227
  handlers.error = ['(error) => console.error("Error:", error)'];
677
-
1228
+
678
1229
  // Add custom handlers from agent configuration
679
1230
  if (agent.handlers && agent.handlers.size > 0) {
680
1231
  for (const [eventName, handlerList] of agent.handlers) {
681
1232
  if (!handlers[eventName]) {
682
1233
  handlers[eventName] = [];
683
1234
  }
684
-
1235
+
685
1236
  // Convert handler functions to string representations
686
- handlerList.forEach(handlerObj => {
687
- if (handlerObj && typeof handlerObj.handler === 'function') {
1237
+ handlerList.forEach((handlerObj) => {
1238
+ if (handlerObj && typeof handlerObj.handler === "function") {
688
1239
  // Convert function to string, handling the function properly
689
1240
  const handlerStr = handlerObj.handler.toString();
690
1241
  handlers[eventName].push(handlerStr);
@@ -692,15 +1243,19 @@ class DockerManager {
692
1243
  });
693
1244
  }
694
1245
  }
695
-
1246
+
696
1247
  // Generate the JavaScript object code
697
- const handlersEntries = Object.entries(handlers).map(([eventName, handlerArray]) => {
698
- const handlersStr = handlerArray.join(',\n ');
699
- // Quote event names that contain special characters (like colons)
700
- const quotedEventName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(eventName) ? eventName : `"${eventName}"`;
701
- return ` ${quotedEventName}: [\n ${handlersStr}\n ]`;
702
- }).join(',\n');
703
-
1248
+ const handlersEntries = Object.entries(handlers)
1249
+ .map(([eventName, handlerArray]) => {
1250
+ const handlersStr = handlerArray.join(",\n ");
1251
+ // Quote event names that contain special characters (like colons)
1252
+ const quotedEventName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(eventName)
1253
+ ? eventName
1254
+ : `"${eventName}"`;
1255
+ return ` ${quotedEventName}: [\n ${handlersStr}\n ]`;
1256
+ })
1257
+ .join(",\n");
1258
+
704
1259
  return `{\n${handlersEntries}\n }`;
705
1260
  }
706
1261
 
@@ -710,16 +1265,21 @@ class DockerManager {
710
1265
  async startAgent(agent, options = {}) {
711
1266
  // Finalize agent configuration (auto-detect features)
712
1267
  agent.finalize();
713
-
1268
+
714
1269
  const imageName = `dank-agent-${agent.name.toLowerCase()}`;
715
- const containerName = `dank-${agent.name.toLowerCase()}-${agent.id.split('_').pop()}`;
716
- const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
1270
+ const containerName = `dank-${agent.name.toLowerCase()}-${agent.id
1271
+ .split("_")
1272
+ .pop()}`;
1273
+ const baseImageName =
1274
+ agent.config.docker?.baseImage || this.defaultBaseImageName;
717
1275
 
718
1276
  try {
719
1277
  // Ensure base image exists
720
1278
  const hasBaseImage = await this.hasImage(baseImageName);
721
1279
  if (!hasBaseImage) {
722
- this.logger.info(`Base image '${baseImageName}' not found for agent ${agent.name}. Pulling...`);
1280
+ this.logger.info(
1281
+ `Base image '${baseImageName}' not found for agent ${agent.name}. Pulling...`
1282
+ );
723
1283
  await this.pullBaseImage(baseImageName);
724
1284
  }
725
1285
 
@@ -739,44 +1299,48 @@ class DockerManager {
739
1299
  CpuQuota: Math.floor(agent.config.resources.cpu * 100000),
740
1300
  CpuPeriod: 100000,
741
1301
  RestartPolicy: {
742
- Name: 'on-failure',
743
- MaximumRetryCount: agent.config.resources.maxRestarts || 3
1302
+ Name: "on-failure",
1303
+ MaximumRetryCount: agent.config.resources.maxRestarts || 3,
744
1304
  },
745
1305
  NetworkMode: this.networkName,
746
- ...this.preparePortConfiguration(agent)
1306
+ ...this.preparePortConfiguration(agent),
747
1307
  },
748
1308
  NetworkingConfig: {
749
1309
  EndpointsConfig: {
750
- [this.networkName]: {}
751
- }
1310
+ [this.networkName]: {},
1311
+ },
752
1312
  },
753
- ...this.prepareExposedPorts(agent)
1313
+ ...this.prepareExposedPorts(agent),
754
1314
  };
755
1315
 
756
1316
  // Create container
757
1317
  this.logger.info(`Creating container for agent: ${agent.name}`);
758
1318
  const container = await this.docker.createContainer(containerConfig);
759
-
1319
+
760
1320
  // Start container
761
1321
  await container.start();
762
-
1322
+
763
1323
  // Store container reference
764
1324
  this.containers.set(agent.name, {
765
1325
  container,
766
1326
  agent,
767
1327
  startTime: new Date(),
768
- status: 'running'
1328
+ status: "running",
769
1329
  });
770
1330
 
771
1331
  agent.containerId = container.id;
772
- agent.status = 'running';
1332
+ agent.status = "running";
1333
+
1334
+ this.logger.info(
1335
+ `Agent ${agent.name} started successfully (${container.id.substring(
1336
+ 0,
1337
+ 12
1338
+ )})`
1339
+ );
773
1340
 
774
- this.logger.info(`Agent ${agent.name} started successfully (${container.id.substring(0, 12)})`);
775
-
776
1341
  return container;
777
-
778
1342
  } catch (error) {
779
- agent.status = 'error';
1343
+ agent.status = "error";
780
1344
  throw new Error(`Failed to start agent ${agent.name}: ${error.message}`);
781
1345
  }
782
1346
  }
@@ -792,23 +1356,22 @@ class DockerManager {
792
1356
 
793
1357
  try {
794
1358
  const { container, agent } = containerInfo;
795
-
1359
+
796
1360
  this.logger.info(`Stopping agent: ${agentName}`);
797
-
1361
+
798
1362
  if (options.force) {
799
1363
  await container.kill();
800
1364
  } else {
801
1365
  await container.stop({ t: 10 }); // 10 second timeout
802
1366
  }
803
-
1367
+
804
1368
  await container.remove();
805
-
1369
+
806
1370
  this.containers.delete(agentName);
807
- agent.status = 'stopped';
1371
+ agent.status = "stopped";
808
1372
  agent.containerId = null;
809
-
1373
+
810
1374
  this.logger.info(`Agent ${agentName} stopped successfully`);
811
-
812
1375
  } catch (error) {
813
1376
  throw new Error(`Failed to stop agent ${agentName}: ${error.message}`);
814
1377
  }
@@ -820,28 +1383,27 @@ class DockerManager {
820
1383
  async getAgentStatus(agentName) {
821
1384
  const containerInfo = this.containers.get(agentName);
822
1385
  if (!containerInfo) {
823
- return { status: 'not_running' };
1386
+ return { status: "not_running" };
824
1387
  }
825
1388
 
826
1389
  try {
827
1390
  const { container, agent, startTime } = containerInfo;
828
1391
  const containerData = await container.inspect();
829
-
1392
+
830
1393
  return {
831
- status: containerData.State.Running ? 'running' : 'stopped',
1394
+ status: containerData.State.Running ? "running" : "stopped",
832
1395
  containerId: container.id,
833
1396
  startTime,
834
1397
  uptime: Date.now() - startTime.getTime(),
835
- health: containerData.State.Health?.Status || 'unknown',
1398
+ health: containerData.State.Health?.Status || "unknown",
836
1399
  restartCount: containerData.RestartCount,
837
1400
  resources: {
838
1401
  memory: agent.config.resources.memory,
839
- cpu: agent.config.resources.cpu
840
- }
1402
+ cpu: agent.config.resources.cpu,
1403
+ },
841
1404
  };
842
-
843
1405
  } catch (error) {
844
- return { status: 'error', error: error.message };
1406
+ return { status: "error", error: error.message };
845
1407
  }
846
1408
  }
847
1409
 
@@ -855,14 +1417,14 @@ class DockerManager {
855
1417
  }
856
1418
 
857
1419
  const { container } = containerInfo;
858
-
1420
+
859
1421
  const logStream = await container.logs({
860
1422
  stdout: true,
861
1423
  stderr: true,
862
1424
  follow: options.follow || false,
863
1425
  tail: options.tail || 100,
864
1426
  since: options.since || undefined,
865
- timestamps: true
1427
+ timestamps: true,
866
1428
  });
867
1429
 
868
1430
  return logStream;
@@ -872,22 +1434,25 @@ class DockerManager {
872
1434
  * Create build context for base image
873
1435
  */
874
1436
  async createBaseBuildContext() {
875
- const contextDir = path.join(__dirname, '../../.build-context-base');
1437
+ const contextDir = path.join(__dirname, "../../.build-context-base");
876
1438
  await fs.ensureDir(contextDir);
877
1439
 
878
1440
  // Copy Docker files
879
- await fs.copy(path.join(__dirname, '../../docker'), contextDir);
880
-
1441
+ await fs.copy(path.join(__dirname, "../../docker"), contextDir);
1442
+
881
1443
  // Create runtime directory
882
- const runtimeDir = path.join(contextDir, 'runtime');
1444
+ const runtimeDir = path.join(contextDir, "runtime");
883
1445
  await fs.ensureDir(runtimeDir);
884
-
1446
+
885
1447
  // Create tarball
886
- const tarPath = path.join(__dirname, '../../.base-build-context.tar');
887
- await tar.create({
888
- file: tarPath,
889
- cwd: contextDir
890
- }, ['.']);
1448
+ const tarPath = path.join(__dirname, "../../.base-build-context.tar");
1449
+ await tar.create(
1450
+ {
1451
+ file: tarPath,
1452
+ cwd: contextDir,
1453
+ },
1454
+ ["."]
1455
+ );
891
1456
 
892
1457
  return tarPath;
893
1458
  }
@@ -896,28 +1461,32 @@ class DockerManager {
896
1461
  * Create build context for agent
897
1462
  */
898
1463
  async createAgentBuildContext(agent) {
899
- const contextDir = path.join(__dirname, `../../.build-context-${agent.name}`);
1464
+ const contextDir = path.join(
1465
+ __dirname,
1466
+ `../../.build-context-${agent.name}`
1467
+ );
900
1468
  await fs.ensureDir(contextDir);
901
1469
 
902
1470
  // Get the base image for this agent
903
- const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
1471
+ const baseImageName =
1472
+ agent.config.docker?.baseImage || this.defaultBaseImageName;
904
1473
 
905
1474
  // Create Dockerfile for agent
906
1475
  const dockerfile = `FROM ${baseImageName}
907
1476
  COPY agent-code/ /app/agent-code/
908
1477
  USER dankuser
909
1478
  `;
910
-
911
- await fs.writeFile(path.join(contextDir, 'Dockerfile'), dockerfile);
912
-
1479
+
1480
+ await fs.writeFile(path.join(contextDir, "Dockerfile"), dockerfile);
1481
+
913
1482
  // Copy agent code if it exists
914
- const agentCodeDir = path.join(contextDir, 'agent-code');
1483
+ const agentCodeDir = path.join(contextDir, "agent-code");
915
1484
  await fs.ensureDir(agentCodeDir);
916
-
1485
+
917
1486
  // Create basic agent code structure
918
1487
  // Generate handlers from agent configuration
919
1488
  const handlersCode = this.generateHandlersCode(agent);
920
-
1489
+
921
1490
  const agentCode = `
922
1491
  // Agent: ${agent.name}
923
1492
  // Generated by Dank Agent Service
@@ -961,15 +1530,21 @@ module.exports = {
961
1530
  handlers: ${handlersCode}
962
1531
  };
963
1532
  `;
964
-
965
- await fs.writeFile(path.join(agentCodeDir, 'index.js'), agentCode);
966
-
1533
+
1534
+ await fs.writeFile(path.join(agentCodeDir, "index.js"), agentCode);
1535
+
967
1536
  // Create tarball
968
- const tarPath = path.join(__dirname, `../../.agent-${agent.name}-context.tar`);
969
- await tar.create({
970
- file: tarPath,
971
- cwd: contextDir
972
- }, ['.']);
1537
+ const tarPath = path.join(
1538
+ __dirname,
1539
+ `../../.agent-${agent.name}-context.tar`
1540
+ );
1541
+ await tar.create(
1542
+ {
1543
+ file: tarPath,
1544
+ cwd: contextDir,
1545
+ },
1546
+ ["."]
1547
+ );
973
1548
 
974
1549
  return tarPath;
975
1550
  }
@@ -988,11 +1563,11 @@ module.exports = {
988
1563
  preparePortConfiguration(agent) {
989
1564
  const portConfig = {};
990
1565
  const portBindings = {};
991
-
1566
+
992
1567
  // Always bind the main agent port
993
1568
  const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
994
1569
  portBindings[`${mainPort}/tcp`] = [{ HostPort: mainPort.toString() }];
995
-
1570
+
996
1571
  // Also bind HTTP port if HTTP is enabled and different from main port
997
1572
  if (agent.config.http && agent.config.http.enabled) {
998
1573
  const httpPort = agent.config.http.port;
@@ -1000,11 +1575,11 @@ module.exports = {
1000
1575
  portBindings[`${httpPort}/tcp`] = [{ HostPort: httpPort.toString() }];
1001
1576
  }
1002
1577
  }
1003
-
1578
+
1004
1579
  // Always bind health check port
1005
1580
  const healthPort = DOCKER_CONFIG.healthCheckPort;
1006
1581
  portBindings[`${healthPort}/tcp`] = [{ HostPort: healthPort.toString() }];
1007
-
1582
+
1008
1583
  portConfig.PortBindings = portBindings;
1009
1584
  return portConfig;
1010
1585
  }
@@ -1014,11 +1589,11 @@ module.exports = {
1014
1589
  */
1015
1590
  prepareExposedPorts(agent) {
1016
1591
  const exposedPorts = {};
1017
-
1592
+
1018
1593
  // Always expose the main agent port
1019
1594
  const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
1020
1595
  exposedPorts[`${mainPort}/tcp`] = {};
1021
-
1596
+
1022
1597
  // Also expose HTTP port if HTTP is enabled and different from main port
1023
1598
  if (agent.config.http && agent.config.http.enabled) {
1024
1599
  const httpPort = agent.config.http.port;
@@ -1026,11 +1601,11 @@ module.exports = {
1026
1601
  exposedPorts[`${httpPort}/tcp`] = {};
1027
1602
  }
1028
1603
  }
1029
-
1604
+
1030
1605
  // Always expose health check port
1031
1606
  const healthPort = DOCKER_CONFIG.healthCheckPort;
1032
1607
  exposedPorts[`${healthPort}/tcp`] = {};
1033
-
1608
+
1034
1609
  return { ExposedPorts: exposedPorts };
1035
1610
  }
1036
1611
 
@@ -1046,7 +1621,7 @@ module.exports = {
1046
1621
  this.logger.info(`Creating Docker network: ${this.networkName}`);
1047
1622
  await this.docker.createNetwork({
1048
1623
  Name: this.networkName,
1049
- Driver: 'bridge'
1624
+ Driver: "bridge",
1050
1625
  });
1051
1626
  } else {
1052
1627
  throw error;
@@ -1074,19 +1649,23 @@ module.exports = {
1074
1649
  */
1075
1650
  async followBuildProgress(stream, buildName) {
1076
1651
  return new Promise((resolve, reject) => {
1077
- this.docker.modem.followProgress(stream, (err, res) => {
1078
- if (err) {
1079
- reject(err);
1080
- } else {
1081
- resolve(res);
1082
- }
1083
- }, (event) => {
1084
- if (event.stream) {
1085
- process.stdout.write(event.stream);
1086
- } else if (event.status) {
1087
- this.logger.debug(`${buildName}: ${event.status}`);
1652
+ this.docker.modem.followProgress(
1653
+ stream,
1654
+ (err, res) => {
1655
+ if (err) {
1656
+ reject(err);
1657
+ } else {
1658
+ resolve(res);
1659
+ }
1660
+ },
1661
+ (event) => {
1662
+ if (event.stream) {
1663
+ process.stdout.write(event.stream);
1664
+ } else if (event.status) {
1665
+ this.logger.debug(`${buildName}: ${event.status}`);
1666
+ }
1088
1667
  }
1089
- });
1668
+ );
1090
1669
  });
1091
1670
  }
1092
1671
 
@@ -1095,21 +1674,27 @@ module.exports = {
1095
1674
  */
1096
1675
  async followPullProgress(stream, pullName) {
1097
1676
  return new Promise((resolve, reject) => {
1098
- this.docker.modem.followProgress(stream, (err, res) => {
1099
- if (err) {
1100
- reject(err);
1101
- } else {
1102
- resolve(res);
1103
- }
1104
- }, (event) => {
1105
- if (event.status) {
1106
- if (event.progress) {
1107
- process.stdout.write(`\r${pullName}: ${event.status} ${event.progress}`);
1677
+ this.docker.modem.followProgress(
1678
+ stream,
1679
+ (err, res) => {
1680
+ if (err) {
1681
+ reject(err);
1108
1682
  } else {
1109
- this.logger.info(`${pullName}: ${event.status}`);
1683
+ resolve(res);
1684
+ }
1685
+ },
1686
+ (event) => {
1687
+ if (event.status) {
1688
+ if (event.progress) {
1689
+ process.stdout.write(
1690
+ `\r${pullName}: ${event.status} ${event.progress}`
1691
+ );
1692
+ } else {
1693
+ this.logger.info(`${pullName}: ${event.status}`);
1694
+ }
1110
1695
  }
1111
1696
  }
1112
- });
1697
+ );
1113
1698
  });
1114
1699
  }
1115
1700
 
@@ -1117,26 +1702,28 @@ module.exports = {
1117
1702
  * Clean up Docker resources
1118
1703
  */
1119
1704
  async cleanup(options = {}) {
1120
- this.logger.info('Cleaning up Docker resources...');
1705
+ this.logger.info("Cleaning up Docker resources...");
1121
1706
 
1122
1707
  try {
1123
1708
  if (options.containers || options.all) {
1124
1709
  // Stop and remove all Dank containers
1125
1710
  const containers = await this.docker.listContainers({
1126
1711
  all: true,
1127
- filters: { name: ['dank-'] }
1712
+ filters: { name: ["dank-"] },
1128
1713
  });
1129
1714
 
1130
1715
  for (const containerInfo of containers) {
1131
1716
  const container = this.docker.getContainer(containerInfo.Id);
1132
1717
  try {
1133
- if (containerInfo.State === 'running') {
1718
+ if (containerInfo.State === "running") {
1134
1719
  await container.stop();
1135
1720
  }
1136
1721
  await container.remove();
1137
1722
  this.logger.info(`Removed container: ${containerInfo.Names[0]}`);
1138
1723
  } catch (error) {
1139
- this.logger.warn(`Failed to remove container ${containerInfo.Names[0]}: ${error.message}`);
1724
+ this.logger.warn(
1725
+ `Failed to remove container ${containerInfo.Names[0]}: ${error.message}`
1726
+ );
1140
1727
  }
1141
1728
  }
1142
1729
  }
@@ -1144,14 +1731,16 @@ module.exports = {
1144
1731
  if (options.images || options.all) {
1145
1732
  // Remove Dank images
1146
1733
  const images = await this.docker.listImages({
1147
- filters: { reference: ['dank-*'] }
1734
+ filters: { reference: ["dank-*"] },
1148
1735
  });
1149
1736
 
1150
1737
  for (const imageInfo of images) {
1151
1738
  const image = this.docker.getImage(imageInfo.Id);
1152
1739
  try {
1153
1740
  await image.remove();
1154
- this.logger.info(`Removed image: ${imageInfo.RepoTags?.[0] || imageInfo.Id}`);
1741
+ this.logger.info(
1742
+ `Removed image: ${imageInfo.RepoTags?.[0] || imageInfo.Id}`
1743
+ );
1155
1744
  } catch (error) {
1156
1745
  this.logger.warn(`Failed to remove image: ${error.message}`);
1157
1746
  }
@@ -1163,8 +1752,7 @@ module.exports = {
1163
1752
  await this.cleanupBuildContexts();
1164
1753
  }
1165
1754
 
1166
- this.logger.info('Cleanup completed');
1167
-
1755
+ this.logger.info("Cleanup completed");
1168
1756
  } catch (error) {
1169
1757
  throw new Error(`Cleanup failed: ${error.message}`);
1170
1758
  }
@@ -1174,13 +1762,15 @@ module.exports = {
1174
1762
  * Clean up build context directories and tarballs
1175
1763
  */
1176
1764
  async cleanupBuildContexts() {
1177
- const projectRoot = path.join(__dirname, '../..');
1178
-
1765
+ const projectRoot = path.join(__dirname, "../..");
1766
+
1179
1767
  try {
1180
1768
  // Find all build context directories
1181
1769
  const entries = await fs.readdir(projectRoot);
1182
- const buildContextDirs = entries.filter(entry => entry.startsWith('.build-context-'));
1183
-
1770
+ const buildContextDirs = entries.filter((entry) =>
1771
+ entry.startsWith(".build-context-")
1772
+ );
1773
+
1184
1774
  // Remove build context directories
1185
1775
  for (const dir of buildContextDirs) {
1186
1776
  const dirPath = path.join(projectRoot, dir);
@@ -1188,29 +1778,33 @@ module.exports = {
1188
1778
  await fs.remove(dirPath);
1189
1779
  this.logger.info(`Removed build context directory: ${dir}`);
1190
1780
  } catch (error) {
1191
- this.logger.warn(`Failed to remove build context directory ${dir}: ${error.message}`);
1781
+ this.logger.warn(
1782
+ `Failed to remove build context directory ${dir}: ${error.message}`
1783
+ );
1192
1784
  }
1193
1785
  }
1194
-
1786
+
1195
1787
  // Find and remove tarball files
1196
- const tarballs = entries.filter(entry =>
1197
- entry.endsWith('-context.tar') || entry.endsWith('-build-context.tar')
1788
+ const tarballs = entries.filter(
1789
+ (entry) =>
1790
+ entry.endsWith("-context.tar") || entry.endsWith("-build-context.tar")
1198
1791
  );
1199
-
1792
+
1200
1793
  for (const tarball of tarballs) {
1201
1794
  const tarballPath = path.join(projectRoot, tarball);
1202
1795
  try {
1203
1796
  await fs.remove(tarballPath);
1204
1797
  this.logger.info(`Removed build context tarball: ${tarball}`);
1205
1798
  } catch (error) {
1206
- this.logger.warn(`Failed to remove tarball ${tarball}: ${error.message}`);
1799
+ this.logger.warn(
1800
+ `Failed to remove tarball ${tarball}: ${error.message}`
1801
+ );
1207
1802
  }
1208
1803
  }
1209
-
1804
+
1210
1805
  if (buildContextDirs.length === 0 && tarballs.length === 0) {
1211
- this.logger.info('No build context files found to clean up');
1806
+ this.logger.info("No build context files found to clean up");
1212
1807
  }
1213
-
1214
1808
  } catch (error) {
1215
1809
  this.logger.warn(`Error during build context cleanup: ${error.message}`);
1216
1810
  }