@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/app-run-helpers.js +385 -0
- package/lib/app-run.js +17 -392
- package/lib/build.js +2 -1
- package/lib/cli.js +13 -10
- package/lib/commands/secure.js +17 -35
- package/lib/config.js +48 -19
- package/lib/infra.js +24 -13
- package/lib/secrets.js +3 -1
- package/lib/utils/build-copy.js +7 -5
- package/lib/utils/compose-generator.js +3 -2
- package/lib/utils/dev-config.js +8 -7
- package/lib/utils/docker.js +73 -0
- package/lib/utils/environment-checker.js +3 -6
- package/lib/utils/infra-containers.js +3 -2
- package/lib/utils/yaml-preserve.js +214 -0
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -35,10 +35,24 @@ async function getConfig() {
|
|
|
35
35
|
config = {};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Ensure
|
|
39
|
-
|
|
40
|
-
|
|
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 (
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
138
|
-
|
|
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'] =
|
|
172
|
+
config['developer-id'] = devIdString;
|
|
146
173
|
// Update cache before saving
|
|
147
|
-
cachedDeveloperId =
|
|
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
|
-
|
|
158
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
package/lib/utils/build-copy.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|
package/lib/utils/dev-config.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
46
|
-
if (
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
26
|
-
|
|
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 =
|
|
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}}"`);
|