@aifabrix/builder 2.3.0 → 2.3.3

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.
package/lib/config.js CHANGED
@@ -35,10 +35,24 @@ async function getConfig() {
35
35
  config = {};
36
36
  }
37
37
 
38
- // Ensure developerId defaults to 0 if not set
39
- if (typeof config['developer-id'] === 'undefined') {
40
- config['developer-id'] = 0;
38
+ // Ensure developer-id exists as a digit-only string (default "0") and validate
39
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
40
+ if (typeof config['developer-id'] === 'undefined' || config['developer-id'] === null) {
41
+ config['developer-id'] = '0';
42
+ } else if (typeof config['developer-id'] === 'number') {
43
+ // Convert numeric to string to preserve type consistency
44
+ if (config['developer-id'] < 0 || !Number.isFinite(config['developer-id'])) {
45
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must be a non-negative digit string.`);
46
+ }
47
+ config['developer-id'] = String(config['developer-id']);
48
+ } else if (typeof config['developer-id'] === 'string') {
49
+ if (!DEV_ID_DIGITS_REGEX.test(config['developer-id'])) {
50
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must contain only digits 0-9.`);
51
+ }
52
+ } else {
53
+ throw new Error(`Invalid developer-id value type: ${typeof config['developer-id']}. Must be a non-negative digit string.`);
41
54
  }
55
+
42
56
  // Ensure environment defaults to 'dev' if not set
43
57
  if (typeof config.environment === 'undefined') {
44
58
  config.environment = 'dev';
@@ -51,15 +65,15 @@ async function getConfig() {
51
65
  if (typeof config.device !== 'object' || config.device === null) {
52
66
  config.device = {};
53
67
  }
54
- // Cache developer ID as property for easy access (use nullish coalescing to allow 0)
55
- cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : 0;
68
+ // Cache developer ID as property for easy access (string, default "0")
69
+ cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : '0';
56
70
  return config;
57
71
  } catch (error) {
58
72
  if (error.code === 'ENOENT') {
59
- // Default developer ID is 0, default environment is 'dev'
60
- cachedDeveloperId = 0;
73
+ // Default developer ID is "0", default environment is 'dev'
74
+ cachedDeveloperId = '0';
61
75
  return {
62
- 'developer-id': 0,
76
+ 'developer-id': '0',
63
77
  environment: 'dev',
64
78
  environments: {},
65
79
  device: {}
@@ -80,7 +94,8 @@ async function saveConfig(data) {
80
94
  await fs.mkdir(CONFIG_DIR, { recursive: true });
81
95
 
82
96
  // Set secure permissions
83
- const configContent = yaml.dump(data);
97
+ // Force quotes to ensure numeric-like strings (e.g., "01") remain strings in YAML
98
+ const configContent = yaml.dump(data, { forceQuotes: true });
84
99
  // Write file first
85
100
  await fs.writeFile(CONFIG_FILE, configContent, {
86
101
  mode: 0o600,
@@ -117,7 +132,7 @@ async function clearConfig() {
117
132
  * Get developer ID from configuration
118
133
  * Loads config if not already cached, then returns cached developer ID
119
134
  * Developer ID: 0 = default infra, > 0 = developer-specific
120
- * @returns {Promise<number>} Developer ID (defaults to 0)
135
+ * @returns {Promise<string>} Developer ID as string (defaults to "0")
121
136
  */
122
137
  async function getDeveloperId() {
123
138
  // Always reload from file to ensure we have the latest value
@@ -130,21 +145,33 @@ async function getDeveloperId() {
130
145
 
131
146
  /**
132
147
  * Set developer ID in configuration
133
- * @param {number} developerId - Developer ID to set (0 = default infra, > 0 = developer-specific)
148
+ * @param {number|string} developerId - Developer ID to set (digit-only string preserved, or number). "0" = default infra, > "0" = developer-specific
134
149
  * @returns {Promise<void>}
135
150
  */
136
151
  async function setDeveloperId(developerId) {
137
- if (typeof developerId !== 'number' || developerId < 0) {
138
- throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
152
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
153
+ let devIdString;
154
+ if (typeof developerId === 'number') {
155
+ if (!Number.isFinite(developerId) || developerId < 0) {
156
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
157
+ }
158
+ devIdString = String(developerId);
159
+ } else if (typeof developerId === 'string') {
160
+ if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
161
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
162
+ }
163
+ devIdString = developerId;
164
+ } else {
165
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
139
166
  }
140
167
  // Clear cache first to ensure we get fresh data from file
141
168
  cachedDeveloperId = null;
142
169
  // Read file directly to avoid any caching issues
143
170
  const config = await getConfig();
144
171
  // Update developer ID
145
- config['developer-id'] = developerId;
172
+ config['developer-id'] = devIdString;
146
173
  // Update cache before saving
147
- cachedDeveloperId = developerId;
174
+ cachedDeveloperId = devIdString;
148
175
  // Save the entire config object to ensure all fields are preserved
149
176
  await saveConfig(config);
150
177
  // Verify the file was saved correctly by reading it back
@@ -154,8 +181,10 @@ async function setDeveloperId(developerId) {
154
181
  // Read file again with fresh file handle to avoid OS caching
155
182
  const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
156
183
  const savedConfig = yaml.load(savedContent);
157
- if (savedConfig['developer-id'] !== developerId) {
158
- throw new Error(`Failed to save developer ID: expected ${developerId}, got ${savedConfig['developer-id']}. File content: ${savedContent.substring(0, 200)}`);
184
+ // YAML may parse numbers as numbers, so convert to string for comparison
185
+ const savedDevIdString = String(savedConfig['developer-id']);
186
+ if (savedDevIdString !== devIdString) {
187
+ throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
159
188
  }
160
189
  // Clear the cache to force reload from file on next getDeveloperId() call
161
190
  // This ensures we get the value that was actually saved to disk
@@ -288,7 +317,7 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
288
317
  /**
289
318
  * Initialize and load developer ID
290
319
  * Call this to ensure developerId is loaded and cached
291
- * @returns {Promise<number>} Developer ID
320
+ * @returns {Promise<string>} Developer ID (string)
292
321
  */
293
322
  async function loadDeveloperId() {
294
323
  if (cachedDeveloperId === null) {
@@ -379,7 +408,7 @@ const exportsObj = {
379
408
  // Developer ID: 0 = default infra, > 0 = developer-specific
380
409
  Object.defineProperty(exportsObj, 'developerId', {
381
410
  get() {
382
- return cachedDeveloperId !== null ? cachedDeveloperId : 0; // Default to 0 if not loaded yet
411
+ return cachedDeveloperId !== null ? cachedDeveloperId : '0'; // Default to "0" if not loaded yet
383
412
  },
384
413
  enumerable: true,
385
414
  configurable: true
package/lib/infra.js CHANGED
@@ -20,6 +20,7 @@ const config = require('./config');
20
20
  const devConfig = require('./utils/dev-config');
21
21
  const logger = require('./utils/logger');
22
22
  const containerUtils = require('./utils/infra-containers');
23
+ const dockerUtils = require('./utils/docker');
23
24
 
24
25
  // Register Handlebars helper for equality check
25
26
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -28,21 +29,23 @@ const execAsync = promisify(exec);
28
29
  /**
29
30
  * Gets infrastructure directory name based on developer ID
30
31
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
31
- * @param {number} devId - Developer ID
32
+ * @param {number|string} devId - Developer ID
32
33
  * @returns {string} Infrastructure directory name
33
34
  */
34
35
  function getInfraDirName(devId) {
35
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
36
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
37
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
36
38
  }
37
39
 
38
40
  /**
39
41
  * Gets Docker Compose project name based on developer ID
40
42
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
41
- * @param {number} devId - Developer ID
43
+ * @param {number|string} devId - Developer ID
42
44
  * @returns {string} Docker Compose project name
43
45
  */
44
46
  function getInfraProjectName(devId) {
45
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
47
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
48
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
46
49
  }
47
50
 
48
51
  // Wrapper to support cwd option
@@ -74,8 +77,7 @@ function execAsyncWithCwd(command, options = {}) {
74
77
  */
75
78
  async function checkDockerAvailability() {
76
79
  try {
77
- await execAsync('docker --version');
78
- await execAsync('docker-compose --version');
80
+ await dockerUtils.ensureDockerAndCompose();
79
81
  } catch (error) {
80
82
  throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
81
83
  }
@@ -96,7 +98,10 @@ async function startInfra(developerId = null) {
96
98
 
97
99
  // Get developer ID from parameter or config
98
100
  const devId = developerId || await config.getDeveloperId();
99
- const ports = devConfig.getDevPorts(devId);
101
+ // Convert to number for getDevPorts (it expects numbers)
102
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
103
+ const ports = devConfig.getDevPorts(devIdNum);
104
+ const idNum = devIdNum;
100
105
 
101
106
  // Load compose template (Handlebars)
102
107
  const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
@@ -116,7 +121,7 @@ async function startInfra(developerId = null) {
116
121
  const templateContent = fs.readFileSync(templatePath, 'utf8');
117
122
  const template = handlebars.compile(templateContent);
118
123
  // Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
119
- const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
124
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
120
125
  const composeContent = template({
121
126
  devId: devId,
122
127
  postgresPort: ports.postgres,
@@ -133,7 +138,8 @@ async function startInfra(developerId = null) {
133
138
  logger.log(`Using compose file: ${composePath}`);
134
139
  logger.log(`Starting infrastructure services for developer ${devId}...`);
135
140
  const projectName = getInfraProjectName(devId);
136
- await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
141
+ const composeCmd = await dockerUtils.getComposeCommand();
142
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
137
143
  logger.log('Infrastructure services started successfully');
138
144
 
139
145
  await waitForServices(devId);
@@ -172,7 +178,8 @@ async function stopInfra() {
172
178
  try {
173
179
  logger.log('Stopping infrastructure services...');
174
180
  const projectName = getInfraProjectName(devId);
175
- await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down`, { cwd: infraDir });
181
+ const composeCmd = await dockerUtils.getComposeCommand();
182
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down`, { cwd: infraDir });
176
183
  logger.log('Infrastructure services stopped');
177
184
  } finally {
178
185
  // Keep the compose file for future use
@@ -208,7 +215,8 @@ async function stopInfraWithVolumes() {
208
215
  try {
209
216
  logger.log('Stopping infrastructure services and removing all data...');
210
217
  const projectName = getInfraProjectName(devId);
211
- await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down -v`, { cwd: infraDir });
218
+ const composeCmd = await dockerUtils.getComposeCommand();
219
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down -v`, { cwd: infraDir });
212
220
  logger.log('Infrastructure services stopped and all data removed');
213
221
  } finally {
214
222
  // Keep the compose file for future use
@@ -261,7 +269,9 @@ async function checkInfraHealth(devId = null) {
261
269
  */
262
270
  async function getInfraStatus() {
263
271
  const devId = await config.getDeveloperId();
264
- const ports = devConfig.getDevPorts(devId);
272
+ // Convert string developer ID to number for getDevPorts
273
+ const devIdNum = parseInt(devId, 10);
274
+ const ports = devConfig.getDevPorts(devIdNum);
265
275
  const services = {
266
276
  postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
267
277
  redis: { port: ports.redis, url: `localhost:${ports.redis}` },
@@ -340,7 +350,8 @@ async function restartService(serviceName) {
340
350
  try {
341
351
  logger.log(`Restarting ${serviceName} service...`);
342
352
  const projectName = getInfraProjectName(devId);
343
- await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" restart ${serviceName}`, { cwd: infraDir });
353
+ const composeCmd = await dockerUtils.getComposeCommand();
354
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" restart ${serviceName}`, { cwd: infraDir });
344
355
  logger.log(`${serviceName} service restarted successfully`);
345
356
  } finally {
346
357
  // Keep the compose file for future use
package/lib/secrets.js CHANGED
@@ -357,7 +357,9 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
357
357
  // For local environment, update infrastructure ports to use dev-specific ports
358
358
  if (environment === 'local') {
359
359
  const devId = await config.getDeveloperId();
360
- const ports = devConfig.getDevPorts(devId);
360
+ // Convert string developer ID to number for getDevPorts
361
+ const devIdNum = parseInt(devId, 10);
362
+ const ports = devConfig.getDevPorts(devIdNum);
361
363
 
362
364
  // Update DATABASE_PORT if present
363
365
  resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
@@ -22,7 +22,7 @@ const os = require('os');
22
22
  * @async
23
23
  * @function copyBuilderToDevDirectory
24
24
  * @param {string} appName - Application name
25
- * @param {number} developerId - Developer ID
25
+ * @param {number|string} developerId - Developer ID
26
26
  * @returns {Promise<string>} Path to developer-specific directory
27
27
  * @throws {Error} If copying fails
28
28
  *
@@ -39,7 +39,8 @@ async function copyBuilderToDevDirectory(appName, developerId) {
39
39
  }
40
40
 
41
41
  // Get base directory (applications or applications-dev-{id})
42
- const baseDir = developerId === 0
42
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
43
+ const baseDir = idNum === 0
43
44
  ? path.join(os.homedir(), '.aifabrix', 'applications')
44
45
  : path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`);
45
46
 
@@ -64,7 +65,7 @@ async function copyBuilderToDevDirectory(appName, developerId) {
64
65
  await fs.mkdir(devDir, { recursive: true });
65
66
 
66
67
  // Copy files based on developer ID
67
- if (developerId === 0) {
68
+ if (idNum === 0) {
68
69
  // Dev 0: Copy contents from builder/{appName}/ directly to applications/
69
70
  await copyDirectory(builderPath, devDir);
70
71
  } else {
@@ -112,11 +113,12 @@ async function copyDirectory(sourceDir, targetDir) {
112
113
  /**
113
114
  * Gets developer-specific directory path for an application
114
115
  * @param {string} appName - Application name
115
- * @param {number} developerId - Developer ID
116
+ * @param {number|string} developerId - Developer ID
116
117
  * @returns {string} Path to developer-specific directory
117
118
  */
118
119
  function getDevDirectory(appName, developerId) {
119
- if (developerId === 0) {
120
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
121
+ if (idNum === 0) {
120
122
  // Dev 0: all apps go directly to applications/ (no subdirectory)
121
123
  return path.join(os.homedir(), '.aifabrix', 'applications');
122
124
  }
@@ -307,12 +307,13 @@ async function generateDockerCompose(appName, appConfig, options) {
307
307
 
308
308
  // Get developer ID and network name
309
309
  const devId = await config.getDeveloperId();
310
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
310
311
  // Dev 0: infra-aifabrix-network (no dev-0 suffix)
311
312
  // Dev > 0: infra-dev{id}-aifabrix-network
312
- const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
313
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
313
314
  // Dev 0: aifabrix-{appName} (no dev-0 suffix)
314
315
  // Dev > 0: aifabrix-dev{id}-{appName}
315
- const containerName = devId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
316
+ const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
316
317
 
317
318
  const serviceConfig = buildServiceConfig(appName, appConfig, port);
318
319
  const volumesConfig = buildVolumesConfig(appName);
@@ -27,7 +27,7 @@ const BASE_PORTS = {
27
27
  * Developer ID: 0 = default infra (base ports), > 0 = developer-specific (offset ports)
28
28
  *
29
29
  * @function getDevPorts
30
- * @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific)
30
+ * @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific). Must be a number.
31
31
  * @returns {Object} Object with calculated ports for all services
32
32
  *
33
33
  * @example
@@ -37,27 +37,28 @@ const BASE_PORTS = {
37
37
  * // Returns: { app: 3100, postgres: 5532, redis: 6479, pgadmin: 5150, redisCommander: 8181 }
38
38
  */
39
39
  function getDevPorts(developerId) {
40
- // Validate type first - must be a number
40
+ // Only accept numbers, reject strings and other types
41
41
  if (typeof developerId !== 'number') {
42
42
  throw new Error('Developer ID must be a positive number');
43
43
  }
44
44
 
45
- // Handle NaN, undefined, null - throw error (don't default)
46
- if (isNaN(developerId) || developerId === undefined || developerId === null) {
45
+ // Handle invalids
46
+ if (developerId === undefined || developerId === null || Number.isNaN(developerId)) {
47
47
  throw new Error('Developer ID must be a positive number');
48
48
  }
49
-
50
49
  if (developerId < 0 || !Number.isInteger(developerId)) {
51
50
  throw new Error('Developer ID must be a positive number');
52
51
  }
53
52
 
53
+ const idNum = developerId;
54
+
54
55
  // Developer ID 0 = default infra (base ports, no offset)
55
- if (developerId === 0) {
56
+ if (idNum === 0) {
56
57
  return { ...BASE_PORTS };
57
58
  }
58
59
 
59
60
  // Developer ID > 0 = developer-specific (add offset)
60
- const offset = developerId * 100;
61
+ const offset = idNum * 100;
61
62
 
62
63
  return {
63
64
  app: BASE_PORTS.app + offset,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Docker CLI Utilities
3
+ *
4
+ * Detects availability of Docker and determines the correct Docker Compose command
5
+ * across environments (Compose v2 plugin: "docker compose", Compose v1: "docker-compose").
6
+ *
7
+ * @fileoverview Docker/Compose detection helpers for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { exec } = require('child_process');
15
+ const { promisify } = require('util');
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ /**
20
+ * Checks that Docker CLI is available.
21
+ * @async
22
+ * @function checkDockerCli
23
+ * @returns {Promise<void>} Resolves if docker is available
24
+ * @throws {Error} If docker is unavailable
25
+ */
26
+ async function checkDockerCli() {
27
+ await execAsync('docker --version');
28
+ }
29
+
30
+ /**
31
+ * Determines the correct Docker Compose command on this system.
32
+ * Tries Docker Compose v2 plugin first: "docker compose", then falls back to v1: "docker-compose".
33
+ *
34
+ * @async
35
+ * @function getComposeCommand
36
+ * @returns {Promise<string>} The compose command to use ("docker compose" or "docker-compose")
37
+ * @throws {Error} If neither v2 nor v1 is available
38
+ */
39
+ async function getComposeCommand() {
40
+ // Prefer Compose v2 plugin if present
41
+ try {
42
+ await execAsync('docker compose version');
43
+ return 'docker compose';
44
+ } catch (_) {
45
+ // Fall back to legacy docker-compose
46
+ }
47
+
48
+ try {
49
+ await execAsync('docker-compose --version');
50
+ return 'docker-compose';
51
+ } catch (_) {
52
+ throw new Error('Docker Compose is not available (neither "docker compose" nor "docker-compose" found).');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Ensures Docker and Docker Compose are available, returning the compose command to use.
58
+ * @async
59
+ * @function ensureDockerAndCompose
60
+ * @returns {Promise<string>} The compose command to use
61
+ * @throws {Error} If docker or compose is not available
62
+ */
63
+ async function ensureDockerAndCompose() {
64
+ await checkDockerCli();
65
+ return await getComposeCommand();
66
+ }
67
+
68
+ module.exports = {
69
+ checkDockerCli,
70
+ getComposeCommand,
71
+ ensureDockerAndCompose
72
+ };
73
+
@@ -12,6 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
+ const dockerUtils = require('./docker');
15
16
 
16
17
  /**
17
18
  * Checks if Docker is installed and available
@@ -22,12 +23,8 @@ const os = require('os');
22
23
  */
23
24
  async function checkDocker() {
24
25
  try {
25
- const { exec } = require('child_process');
26
- const { promisify } = require('util');
27
- const execAsync = promisify(exec);
28
-
29
- await execAsync('docker --version');
30
- await execAsync('docker-compose --version');
26
+ await dockerUtils.checkDockerCli();
27
+ await dockerUtils.getComposeCommand();
31
28
  return 'ok';
32
29
  } catch (error) {
33
30
  return 'error';
@@ -20,14 +20,15 @@ const execAsync = promisify(exec);
20
20
  * @private
21
21
  * @async
22
22
  * @param {string} serviceName - Service name
23
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
23
+ * @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
24
24
  * @returns {Promise<string|null>} Container name or null if not found
25
25
  */
26
26
  async function findContainer(serviceName, devId = null) {
27
27
  try {
28
28
  const developerId = devId || await config.getDeveloperId();
29
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
29
30
  // Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
30
- const containerNamePattern = developerId === 0
31
+ const containerNamePattern = idNum === 0
31
32
  ? `aifabrix-${serviceName}`
32
33
  : `aifabrix-dev${developerId}-${serviceName}`;
33
34
  let { stdout } = await execAsync(`docker ps --filter "name=${containerNamePattern}" --format "{{.Names}}"`);