@aifabrix/builder 2.1.7 → 2.3.0
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-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +155 -42
- package/lib/cli.js +104 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +315 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/push.js +34 -7
- package/lib/secrets.js +89 -23
- package/lib/templates.js +1 -1
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +162 -0
- package/lib/utils/cli-utils.js +49 -3
- package/lib/utils/compose-generator.js +57 -16
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/token-manager.js +381 -0
- package/package.json +2 -2
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
- package/test-output.txt +0 -5431
package/lib/infra.js
CHANGED
|
@@ -14,11 +14,37 @@ const { promisify } = require('util');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const os = require('os');
|
|
17
|
+
const handlebars = require('handlebars');
|
|
17
18
|
const secrets = require('./secrets');
|
|
19
|
+
const config = require('./config');
|
|
20
|
+
const devConfig = require('./utils/dev-config');
|
|
18
21
|
const logger = require('./utils/logger');
|
|
22
|
+
const containerUtils = require('./utils/infra-containers');
|
|
19
23
|
|
|
24
|
+
// Register Handlebars helper for equality check
|
|
25
|
+
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
20
26
|
const execAsync = promisify(exec);
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Gets infrastructure directory name based on developer ID
|
|
30
|
+
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
31
|
+
* @param {number} devId - Developer ID
|
|
32
|
+
* @returns {string} Infrastructure directory name
|
|
33
|
+
*/
|
|
34
|
+
function getInfraDirName(devId) {
|
|
35
|
+
return devId === 0 ? 'infra' : `infra-dev${devId}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets Docker Compose project name based on developer ID
|
|
40
|
+
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
41
|
+
* @param {number} devId - Developer ID
|
|
42
|
+
* @returns {string} Docker Compose project name
|
|
43
|
+
*/
|
|
44
|
+
function getInfraProjectName(devId) {
|
|
45
|
+
return devId === 0 ? 'infra' : `infra-dev${devId}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
// Wrapper to support cwd option
|
|
23
49
|
function execAsyncWithCwd(command, options = {}) {
|
|
24
50
|
return new Promise((resolve, reject) => {
|
|
@@ -64,33 +90,53 @@ async function ensureAdminSecrets() {
|
|
|
64
90
|
return adminSecretsPath;
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
async function startInfra() {
|
|
93
|
+
async function startInfra(developerId = null) {
|
|
68
94
|
await checkDockerAvailability();
|
|
69
95
|
const adminSecretsPath = await ensureAdminSecrets();
|
|
70
96
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
97
|
+
// Get developer ID from parameter or config
|
|
98
|
+
const devId = developerId || await config.getDeveloperId();
|
|
99
|
+
const ports = devConfig.getDevPorts(devId);
|
|
100
|
+
|
|
101
|
+
// Load compose template (Handlebars)
|
|
102
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
|
|
73
103
|
if (!fs.existsSync(templatePath)) {
|
|
74
104
|
throw new Error(`Compose template not found: ${templatePath}`);
|
|
75
105
|
}
|
|
76
106
|
|
|
77
|
-
// Create infra directory in ~/.aifabrix
|
|
107
|
+
// Create infra directory in ~/.aifabrix with dev ID
|
|
78
108
|
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
79
|
-
const
|
|
109
|
+
const infraDirName = getInfraDirName(devId);
|
|
110
|
+
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
80
111
|
if (!fs.existsSync(infraDir)) {
|
|
81
112
|
fs.mkdirSync(infraDir, { recursive: true });
|
|
82
113
|
}
|
|
83
114
|
|
|
115
|
+
// Generate compose file from template
|
|
116
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
117
|
+
const template = handlebars.compile(templateContent);
|
|
118
|
+
// 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`;
|
|
120
|
+
const composeContent = template({
|
|
121
|
+
devId: devId,
|
|
122
|
+
postgresPort: ports.postgres,
|
|
123
|
+
redisPort: ports.redis,
|
|
124
|
+
pgadminPort: ports.pgadmin,
|
|
125
|
+
redisCommanderPort: ports.redisCommander,
|
|
126
|
+
networkName: networkName
|
|
127
|
+
});
|
|
128
|
+
|
|
84
129
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
85
|
-
fs.writeFileSync(composePath,
|
|
130
|
+
fs.writeFileSync(composePath, composeContent);
|
|
86
131
|
|
|
87
132
|
try {
|
|
88
133
|
logger.log(`Using compose file: ${composePath}`);
|
|
89
|
-
logger.log(
|
|
90
|
-
|
|
134
|
+
logger.log(`Starting infrastructure services for developer ${devId}...`);
|
|
135
|
+
const projectName = getInfraProjectName(devId);
|
|
136
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
|
|
91
137
|
logger.log('Infrastructure services started successfully');
|
|
92
138
|
|
|
93
|
-
await waitForServices();
|
|
139
|
+
await waitForServices(devId);
|
|
94
140
|
logger.log('All services are healthy and ready');
|
|
95
141
|
} finally {
|
|
96
142
|
// Keep the compose file for stop commands
|
|
@@ -111,8 +157,11 @@ async function startInfra() {
|
|
|
111
157
|
* // All infrastructure containers are stopped and removed
|
|
112
158
|
*/
|
|
113
159
|
async function stopInfra() {
|
|
160
|
+
const devId = await config.getDeveloperId();
|
|
114
161
|
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
115
|
-
const
|
|
162
|
+
const infraDirName = getInfraDirName(devId);
|
|
163
|
+
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
164
|
+
const composePath = path.join(infraDir, 'compose.yaml');
|
|
116
165
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
117
166
|
|
|
118
167
|
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
@@ -120,11 +169,10 @@ async function stopInfra() {
|
|
|
120
169
|
return;
|
|
121
170
|
}
|
|
122
171
|
|
|
123
|
-
const infraDir = path.join(aifabrixDir, 'infra');
|
|
124
|
-
|
|
125
172
|
try {
|
|
126
173
|
logger.log('Stopping infrastructure services...');
|
|
127
|
-
|
|
174
|
+
const projectName = getInfraProjectName(devId);
|
|
175
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down`, { cwd: infraDir });
|
|
128
176
|
logger.log('Infrastructure services stopped');
|
|
129
177
|
} finally {
|
|
130
178
|
// Keep the compose file for future use
|
|
@@ -145,8 +193,11 @@ async function stopInfra() {
|
|
|
145
193
|
* // All infrastructure containers and data are removed
|
|
146
194
|
*/
|
|
147
195
|
async function stopInfraWithVolumes() {
|
|
196
|
+
const devId = await config.getDeveloperId();
|
|
148
197
|
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
149
|
-
const
|
|
198
|
+
const infraDirName = getInfraDirName(devId);
|
|
199
|
+
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
200
|
+
const composePath = path.join(infraDir, 'compose.yaml');
|
|
150
201
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
151
202
|
|
|
152
203
|
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
@@ -154,84 +205,16 @@ async function stopInfraWithVolumes() {
|
|
|
154
205
|
return;
|
|
155
206
|
}
|
|
156
207
|
|
|
157
|
-
const infraDir = path.join(aifabrixDir, 'infra');
|
|
158
|
-
|
|
159
208
|
try {
|
|
160
209
|
logger.log('Stopping infrastructure services and removing all data...');
|
|
161
|
-
|
|
210
|
+
const projectName = getInfraProjectName(devId);
|
|
211
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" down -v`, { cwd: infraDir });
|
|
162
212
|
logger.log('Infrastructure services stopped and all data removed');
|
|
163
213
|
} finally {
|
|
164
214
|
// Keep the compose file for future use
|
|
165
215
|
}
|
|
166
216
|
}
|
|
167
217
|
|
|
168
|
-
/**
|
|
169
|
-
* Finds container by name pattern
|
|
170
|
-
* @private
|
|
171
|
-
* @async
|
|
172
|
-
* @param {string} serviceName - Service name
|
|
173
|
-
* @returns {Promise<string|null>} Container name or null if not found
|
|
174
|
-
*/
|
|
175
|
-
async function findContainer(serviceName) {
|
|
176
|
-
try {
|
|
177
|
-
// Try both naming patterns: infra-* (dynamic names) and aifabrix-* (hardcoded names)
|
|
178
|
-
let { stdout } = await execAsync(`docker ps --filter "name=infra-${serviceName}" --format "{{.Names}}"`);
|
|
179
|
-
let containerName = stdout.trim();
|
|
180
|
-
if (!containerName) {
|
|
181
|
-
// Fallback to hardcoded names
|
|
182
|
-
({ stdout } = await execAsync(`docker ps --filter "name=aifabrix-${serviceName}" --format "{{.Names}}"`));
|
|
183
|
-
containerName = stdout.trim();
|
|
184
|
-
}
|
|
185
|
-
return containerName;
|
|
186
|
-
} catch (error) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Checks health status for a service with health checks
|
|
193
|
-
* @private
|
|
194
|
-
* @async
|
|
195
|
-
* @param {string} serviceName - Service name
|
|
196
|
-
* @returns {Promise<string>} Health status
|
|
197
|
-
*/
|
|
198
|
-
async function checkServiceWithHealthCheck(serviceName) {
|
|
199
|
-
try {
|
|
200
|
-
const containerName = await findContainer(serviceName);
|
|
201
|
-
if (!containerName) {
|
|
202
|
-
return 'unknown';
|
|
203
|
-
}
|
|
204
|
-
const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`);
|
|
205
|
-
const status = stdout.trim().replace(/['"]/g, '');
|
|
206
|
-
// Accept both 'healthy' and 'starting' as healthy (starting means it's initializing)
|
|
207
|
-
return (status === 'healthy' || status === 'starting') ? 'healthy' : status;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
return 'unknown';
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Checks health status for a service without health checks
|
|
215
|
-
* @private
|
|
216
|
-
* @async
|
|
217
|
-
* @param {string} serviceName - Service name
|
|
218
|
-
* @returns {Promise<string>} Health status
|
|
219
|
-
*/
|
|
220
|
-
async function checkServiceWithoutHealthCheck(serviceName) {
|
|
221
|
-
try {
|
|
222
|
-
const containerName = await findContainer(serviceName);
|
|
223
|
-
if (!containerName) {
|
|
224
|
-
return 'unknown';
|
|
225
|
-
}
|
|
226
|
-
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
227
|
-
const status = stdout.trim().replace(/['"]/g, '');
|
|
228
|
-
// Treat 'running' or 'healthy' as 'healthy' for services without health checks
|
|
229
|
-
return (status === 'running' || status === 'healthy') ? 'healthy' : 'unhealthy';
|
|
230
|
-
} catch (error) {
|
|
231
|
-
return 'unknown';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
218
|
/**
|
|
236
219
|
* Checks if infrastructure services are running
|
|
237
220
|
* Validates that all required services are healthy and accessible
|
|
@@ -245,19 +228,20 @@ async function checkServiceWithoutHealthCheck(serviceName) {
|
|
|
245
228
|
* const health = await checkInfraHealth();
|
|
246
229
|
* // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
|
|
247
230
|
*/
|
|
248
|
-
async function checkInfraHealth() {
|
|
231
|
+
async function checkInfraHealth(devId = null) {
|
|
232
|
+
const developerId = devId || await config.getDeveloperId();
|
|
249
233
|
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
250
234
|
const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
|
|
251
235
|
const health = {};
|
|
252
236
|
|
|
253
237
|
// Check health status for services with health checks
|
|
254
238
|
for (const service of servicesWithHealthCheck) {
|
|
255
|
-
health[service] = await checkServiceWithHealthCheck(service);
|
|
239
|
+
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
|
|
256
240
|
}
|
|
257
241
|
|
|
258
242
|
// Check if services without health checks are running
|
|
259
243
|
for (const service of servicesWithoutHealthCheck) {
|
|
260
|
-
health[service] = await checkServiceWithoutHealthCheck(service);
|
|
244
|
+
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
|
|
261
245
|
}
|
|
262
246
|
|
|
263
247
|
return health;
|
|
@@ -276,39 +260,41 @@ async function checkInfraHealth() {
|
|
|
276
260
|
* // Returns: { postgres: { status: 'running', port: 5432, url: 'localhost:5432' }, ... }
|
|
277
261
|
*/
|
|
278
262
|
async function getInfraStatus() {
|
|
263
|
+
const devId = await config.getDeveloperId();
|
|
264
|
+
const ports = devConfig.getDevPorts(devId);
|
|
279
265
|
const services = {
|
|
280
|
-
postgres: { port:
|
|
281
|
-
redis: { port:
|
|
282
|
-
pgadmin: { port:
|
|
283
|
-
'redis-commander': { port:
|
|
266
|
+
postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
|
|
267
|
+
redis: { port: ports.redis, url: `localhost:${ports.redis}` },
|
|
268
|
+
pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
|
|
269
|
+
'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` }
|
|
284
270
|
};
|
|
285
271
|
|
|
286
272
|
const status = {};
|
|
287
273
|
|
|
288
|
-
for (const [serviceName,
|
|
274
|
+
for (const [serviceName, serviceConfig] of Object.entries(services)) {
|
|
289
275
|
try {
|
|
290
|
-
const containerName = await findContainer(serviceName);
|
|
276
|
+
const containerName = await containerUtils.findContainer(serviceName, devId);
|
|
291
277
|
if (containerName) {
|
|
292
278
|
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
293
279
|
// Normalize status value (trim whitespace and remove quotes)
|
|
294
280
|
const normalizedStatus = stdout.trim().replace(/['"]/g, '');
|
|
295
281
|
status[serviceName] = {
|
|
296
282
|
status: normalizedStatus,
|
|
297
|
-
port:
|
|
298
|
-
url:
|
|
283
|
+
port: serviceConfig.port,
|
|
284
|
+
url: serviceConfig.url
|
|
299
285
|
};
|
|
300
286
|
} else {
|
|
301
287
|
status[serviceName] = {
|
|
302
288
|
status: 'not running',
|
|
303
|
-
port:
|
|
304
|
-
url:
|
|
289
|
+
port: serviceConfig.port,
|
|
290
|
+
url: serviceConfig.url
|
|
305
291
|
};
|
|
306
292
|
}
|
|
307
293
|
} catch (error) {
|
|
308
294
|
status[serviceName] = {
|
|
309
295
|
status: 'not running',
|
|
310
|
-
port:
|
|
311
|
-
url:
|
|
296
|
+
port: serviceConfig.port,
|
|
297
|
+
url: serviceConfig.url
|
|
312
298
|
};
|
|
313
299
|
}
|
|
314
300
|
}
|
|
@@ -340,19 +326,21 @@ async function restartService(serviceName) {
|
|
|
340
326
|
throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
|
|
341
327
|
}
|
|
342
328
|
|
|
329
|
+
const devId = await config.getDeveloperId();
|
|
343
330
|
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
344
|
-
const
|
|
331
|
+
const infraDirName = getInfraDirName(devId);
|
|
332
|
+
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
333
|
+
const composePath = path.join(infraDir, 'compose.yaml');
|
|
345
334
|
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
346
335
|
|
|
347
336
|
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
348
337
|
throw new Error('Infrastructure not properly configured');
|
|
349
338
|
}
|
|
350
339
|
|
|
351
|
-
const infraDir = path.join(aifabrixDir, 'infra');
|
|
352
|
-
|
|
353
340
|
try {
|
|
354
341
|
logger.log(`Restarting ${serviceName} service...`);
|
|
355
|
-
|
|
342
|
+
const projectName = getInfraProjectName(devId);
|
|
343
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" restart ${serviceName}`, { cwd: infraDir });
|
|
356
344
|
logger.log(`${serviceName} service restarted successfully`);
|
|
357
345
|
} finally {
|
|
358
346
|
// Keep the compose file for future use
|
|
@@ -362,13 +350,14 @@ async function restartService(serviceName) {
|
|
|
362
350
|
/**
|
|
363
351
|
* Waits for services to be healthy
|
|
364
352
|
* @private
|
|
353
|
+
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
365
354
|
*/
|
|
366
|
-
async function waitForServices() {
|
|
355
|
+
async function waitForServices(devId = null) {
|
|
367
356
|
const maxAttempts = 30;
|
|
368
357
|
const delay = 2000; // 2 seconds
|
|
369
358
|
|
|
370
359
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
371
|
-
const health = await checkInfraHealth();
|
|
360
|
+
const health = await checkInfraHealth(devId);
|
|
372
361
|
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
373
362
|
|
|
374
363
|
if (allHealthy) {
|
|
@@ -386,12 +375,88 @@ async function waitForServices() {
|
|
|
386
375
|
throw new Error('Services failed to become healthy within timeout period');
|
|
387
376
|
}
|
|
388
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Gets status of running application containers
|
|
380
|
+
* Finds all containers matching pattern aifabrix-dev{id}-* (excluding infrastructure)
|
|
381
|
+
*
|
|
382
|
+
* @async
|
|
383
|
+
* @function getAppStatus
|
|
384
|
+
* @returns {Promise<Array>} Array of application status objects
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* const apps = await getAppStatus();
|
|
388
|
+
* // Returns: [{ name: 'myapp', container: 'aifabrix-dev1-myapp', port: '3100:3000', status: 'running', url: 'http://localhost:3100' }]
|
|
389
|
+
*/
|
|
390
|
+
async function getAppStatus() {
|
|
391
|
+
const devId = await config.getDeveloperId();
|
|
392
|
+
const apps = [];
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
// Find all containers with pattern
|
|
396
|
+
// Dev 0: aifabrix-* (but exclude infrastructure containers)
|
|
397
|
+
// Dev > 0: aifabrix-dev{id}-*
|
|
398
|
+
const filterPattern = devId === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
|
|
399
|
+
const { stdout } = await execAsync(`docker ps --filter "name=${filterPattern}" --format "{{.Names}}\t{{.Ports}}\t{{.Status}}"`);
|
|
400
|
+
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
401
|
+
|
|
402
|
+
// Infrastructure container names to exclude
|
|
403
|
+
// Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
|
|
404
|
+
const infraContainers = devId === 0
|
|
405
|
+
? [
|
|
406
|
+
'aifabrix-postgres',
|
|
407
|
+
'aifabrix-redis',
|
|
408
|
+
'aifabrix-pgadmin',
|
|
409
|
+
'aifabrix-redis-commander'
|
|
410
|
+
]
|
|
411
|
+
: [
|
|
412
|
+
`aifabrix-dev${devId}-postgres`,
|
|
413
|
+
`aifabrix-dev${devId}-redis`,
|
|
414
|
+
`aifabrix-dev${devId}-pgadmin`,
|
|
415
|
+
`aifabrix-dev${devId}-redis-commander`
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
const [containerName, ports, status] = line.split('\t');
|
|
420
|
+
|
|
421
|
+
// Skip infrastructure containers
|
|
422
|
+
if (infraContainers.includes(containerName)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Extract app name from container name
|
|
427
|
+
// Dev 0: aifabrix-{appName}, Dev > 0: aifabrix-dev{id}-{appName}
|
|
428
|
+
const pattern = devId === 0
|
|
429
|
+
? /^aifabrix-(.+)$/
|
|
430
|
+
: new RegExp(`^aifabrix-dev${devId}-(.+)$`);
|
|
431
|
+
const appNameMatch = containerName.match(pattern);
|
|
432
|
+
if (!appNameMatch) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const appName = appNameMatch[1];
|
|
437
|
+
|
|
438
|
+
// Extract host port from ports string (e.g., "0.0.0.0:3100->3000/tcp")
|
|
439
|
+
const portMatch = ports.match(/:(\d+)->\d+\//);
|
|
440
|
+
const hostPort = portMatch ? portMatch[1] : 'unknown';
|
|
441
|
+
const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
|
|
442
|
+
|
|
443
|
+
apps.push({
|
|
444
|
+
name: appName,
|
|
445
|
+
container: containerName,
|
|
446
|
+
port: ports,
|
|
447
|
+
status: status.trim(),
|
|
448
|
+
url: url
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
// If no containers found, return empty array
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return apps;
|
|
457
|
+
}
|
|
458
|
+
|
|
389
459
|
module.exports = {
|
|
390
|
-
startInfra,
|
|
391
|
-
|
|
392
|
-
stopInfraWithVolumes,
|
|
393
|
-
checkInfraHealth,
|
|
394
|
-
getInfraStatus,
|
|
395
|
-
restartService,
|
|
396
|
-
ensureAdminSecrets
|
|
460
|
+
startInfra, stopInfra, stopInfraWithVolumes, checkInfraHealth,
|
|
461
|
+
getInfraStatus, getAppStatus, restartService, ensureAdminSecrets
|
|
397
462
|
};
|
package/lib/push.js
CHANGED
|
@@ -21,12 +21,35 @@ const execAsync = promisify(exec);
|
|
|
21
21
|
* @returns {Promise<boolean>} True if Azure CLI is available
|
|
22
22
|
*/
|
|
23
23
|
async function checkAzureCLIInstalled() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
25
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
26
|
+
|
|
27
|
+
// Try multiple methods to detect Azure CLI (commands that don't require authentication)
|
|
28
|
+
const commands = process.platform === 'win32'
|
|
29
|
+
? ['az --version', 'az.cmd --version']
|
|
30
|
+
: ['az --version'];
|
|
31
|
+
|
|
32
|
+
for (const command of commands) {
|
|
33
|
+
try {
|
|
34
|
+
// Use a timeout to avoid hanging if command doesn't exist
|
|
35
|
+
await execAsync(command, { ...options, timeout: 5000 });
|
|
36
|
+
return true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Log the error for debugging (only in development)
|
|
39
|
+
if (process.env.DEBUG) {
|
|
40
|
+
logger.log(chalk.gray(`[DEBUG] Command '${command}' failed: ${error.message}`));
|
|
41
|
+
}
|
|
42
|
+
// Continue to next command if this one fails
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If all commands failed, Azure CLI is not available
|
|
48
|
+
// Log for debugging if enabled
|
|
49
|
+
if (process.env.DEBUG) {
|
|
50
|
+
logger.log(chalk.gray('[DEBUG] All Azure CLI detection methods failed'));
|
|
29
51
|
}
|
|
52
|
+
return false;
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
/**
|
|
@@ -103,7 +126,9 @@ function validateRegistryURL(registryUrl) {
|
|
|
103
126
|
async function checkACRAuthentication(registry) {
|
|
104
127
|
try {
|
|
105
128
|
const registryName = extractRegistryName(registry);
|
|
106
|
-
|
|
129
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
130
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
131
|
+
await execAsync(`az acr show --name ${registryName}`, options);
|
|
107
132
|
return true;
|
|
108
133
|
} catch (error) {
|
|
109
134
|
return false;
|
|
@@ -119,7 +144,9 @@ async function authenticateACR(registry) {
|
|
|
119
144
|
try {
|
|
120
145
|
const registryName = extractRegistryName(registry);
|
|
121
146
|
logger.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
122
|
-
|
|
147
|
+
// On Windows, use shell option to ensure proper command resolution
|
|
148
|
+
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
149
|
+
await execAsync(`az acr login --name ${registryName}`, options);
|
|
123
150
|
logger.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
124
151
|
} catch (error) {
|
|
125
152
|
throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
|