@aifabrix/builder 2.33.5 → 2.36.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.
@@ -12,35 +12,41 @@ const fs = require('fs').promises;
12
12
  * Prompt for wizard mode selection
13
13
  * @async
14
14
  * @function promptForMode
15
+ * @param {string} [defaultMode] - Default value ('create-system' | 'add-datasource')
15
16
  * @returns {Promise<string>} Selected mode ('create-system' | 'add-datasource')
16
17
  */
17
- async function promptForMode() {
18
+ async function promptForMode(defaultMode) {
19
+ const choices = [
20
+ { name: 'Create a new external system', value: 'create-system' },
21
+ { name: 'Add datasource to existing system', value: 'add-datasource' }
22
+ ];
18
23
  const { mode } = await inquirer.prompt([
19
24
  {
20
25
  type: 'list',
21
26
  name: 'mode',
22
27
  message: 'What would you like to do?',
23
- choices: [
24
- { name: 'Create a new external system', value: 'create-system' },
25
- { name: 'Add datasource to existing system', value: 'add-datasource' }
26
- ]
28
+ choices,
29
+ default: defaultMode && choices.some(c => c.value === defaultMode) ? defaultMode : undefined
27
30
  }
28
31
  ]);
29
32
  return mode;
30
33
  }
31
34
 
32
35
  /**
33
- * Prompt for existing system ID or key (for add-datasource mode)
36
+ * Prompt for existing system ID or key (for add-datasource mode).
37
+ * Only external systems (OpenAPI, MCP, custom) support add-datasource; webapps do not.
34
38
  * @async
35
39
  * @function promptForSystemIdOrKey
40
+ * @param {string} [defaultValue] - Default value (e.g. from loaded wizard.yaml)
36
41
  * @returns {Promise<string>} System ID or key
37
42
  */
38
- async function promptForSystemIdOrKey() {
43
+ async function promptForSystemIdOrKey(defaultValue) {
39
44
  const { systemIdOrKey } = await inquirer.prompt([
40
45
  {
41
46
  type: 'input',
42
47
  name: 'systemIdOrKey',
43
- message: 'Enter the existing system ID or key:',
48
+ message: 'Enter the existing external system ID or key (not a webapp):',
49
+ default: defaultValue,
44
50
  validate: (input) => {
45
51
  if (!input || typeof input !== 'string' || input.trim().length === 0) {
46
52
  return 'System ID or key is required';
@@ -55,20 +61,24 @@ async function promptForSystemIdOrKey() {
55
61
  * Prompt for source type selection
56
62
  * @async
57
63
  * @function promptForSourceType
64
+ * @param {Array<{key: string, displayName?: string}>} [platforms] - If provided and non-empty, include "Known platform"; otherwise omit it
58
65
  * @returns {Promise<string>} Selected source type
59
66
  */
60
- async function promptForSourceType() {
67
+ async function promptForSourceType(platforms = []) {
68
+ const choices = [
69
+ { name: 'OpenAPI file (local file)', value: 'openapi-file' },
70
+ { name: 'OpenAPI URL (remote URL)', value: 'openapi-url' },
71
+ { name: 'MCP server', value: 'mcp-server' }
72
+ ];
73
+ if (Array.isArray(platforms) && platforms.length > 0) {
74
+ choices.push({ name: 'Known platform (pre-configured)', value: 'known-platform' });
75
+ }
61
76
  const { sourceType } = await inquirer.prompt([
62
77
  {
63
78
  type: 'list',
64
79
  name: 'sourceType',
65
80
  message: 'What is your source type?',
66
- choices: [
67
- { name: 'OpenAPI file (local file)', value: 'openapi-file' },
68
- { name: 'OpenAPI URL (remote URL)', value: 'openapi-url' },
69
- { name: 'MCP server', value: 'mcp-server' },
70
- { name: 'Known platform (pre-configured)', value: 'known-platform' }
71
- ]
81
+ choices
72
82
  }
73
83
  ]);
74
84
  return sourceType;
@@ -176,11 +186,74 @@ async function promptForMcpServer() {
176
186
  };
177
187
  }
178
188
 
189
+ /**
190
+ * Prompt for credential action (skip / create new / use existing).
191
+ * Choose Skip if you don't have credentials yet; you can add them later in env.template.
192
+ * @async
193
+ * @function promptForCredentialAction
194
+ * @returns {Promise<Object>} Object with action ('skip'|'create'|'select') and optional credentialIdOrKey
195
+ */
196
+ async function promptForCredentialAction() {
197
+ const { action } = await inquirer.prompt([
198
+ {
199
+ type: 'list',
200
+ name: 'action',
201
+ message: 'Credential (optional; choose Skip if you don\'t have credentials yet):',
202
+ choices: [
203
+ { name: 'Skip - configure credentials later', value: 'skip' },
204
+ { name: 'Create new', value: 'create' },
205
+ { name: 'Use existing', value: 'select' }
206
+ ]
207
+ }
208
+ ]);
209
+ if (action === 'select') {
210
+ const { credentialIdOrKey } = await inquirer.prompt([
211
+ {
212
+ type: 'input',
213
+ name: 'credentialIdOrKey',
214
+ message: 'Enter credential ID or key (must exist on the dataplane):',
215
+ validate: (input) => {
216
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
217
+ return 'Credential ID or key is required (or choose Skip at the previous step)';
218
+ }
219
+ return true;
220
+ }
221
+ }
222
+ ]);
223
+ return { action, credentialIdOrKey: credentialIdOrKey.trim() };
224
+ }
225
+ return { action };
226
+ }
227
+
228
+ /**
229
+ * Re-prompt for credential ID/key when validation failed (e.g. not found on dataplane).
230
+ * Empty input means skip.
231
+ * @async
232
+ * @function promptForCredentialIdOrKeyRetry
233
+ * @param {string} [previousError] - Error message from dataplane (e.g. "Credential not found")
234
+ * @returns {Promise<Object>} { credentialIdOrKey: string } or { skip: true } if user leaves empty
235
+ */
236
+ async function promptForCredentialIdOrKeyRetry(previousError) {
237
+ const msg = previousError
238
+ ? `Credential not found or invalid (${String(previousError).slice(0, 60)}). Enter ID/key or leave empty to skip:`
239
+ : 'Enter credential ID or key (or leave empty to skip):';
240
+ const { credentialIdOrKey } = await inquirer.prompt([
241
+ {
242
+ type: 'input',
243
+ name: 'credentialIdOrKey',
244
+ message: msg,
245
+ default: ''
246
+ }
247
+ ]);
248
+ const trimmed = (credentialIdOrKey && credentialIdOrKey.trim()) || '';
249
+ return trimmed ? { credentialIdOrKey: trimmed } : { skip: true };
250
+ }
251
+
179
252
  /**
180
253
  * Prompt for known platform selection
181
254
  * @async
182
255
  * @function promptForKnownPlatform
183
- * @param {string[]} [platforms] - List of available platforms (if provided)
256
+ * @param {Array<{key: string, displayName?: string}>} [platforms] - List of available platforms (if provided)
184
257
  * @returns {Promise<string>} Selected platform key
185
258
  */
186
259
  async function promptForKnownPlatform(platforms = []) {
@@ -297,7 +370,6 @@ async function promptForConfigReview(systemConfig, datasourceConfigs) {
297
370
  message: 'What would you like to do?',
298
371
  choices: [
299
372
  { name: 'Accept and save', value: 'accept' },
300
- { name: 'Edit configuration manually', value: 'edit' },
301
373
  { name: 'Cancel', value: 'cancel' }
302
374
  ]
303
375
  }
@@ -307,32 +379,6 @@ async function promptForConfigReview(systemConfig, datasourceConfigs) {
307
379
  return { action: 'cancel' };
308
380
  }
309
381
 
310
- if (action === 'edit') {
311
- const { editedConfig } = await inquirer.prompt([
312
- {
313
- type: 'editor',
314
- name: 'editedConfig',
315
- message: 'Edit the configuration (JSON format):',
316
- default: JSON.stringify({ systemConfig, datasourceConfigs }, null, 2),
317
- validate: (input) => {
318
- try {
319
- JSON.parse(input);
320
- return true;
321
- } catch (error) {
322
- return `Invalid JSON: ${error.message}`;
323
- }
324
- }
325
- }
326
- ]);
327
-
328
- const parsed = JSON.parse(editedConfig);
329
- return {
330
- action: 'edit',
331
- systemConfig: parsed.systemConfig,
332
- datasourceConfigs: parsed.datasourceConfigs
333
- };
334
- }
335
-
336
382
  return { action: 'accept' };
337
383
  }
338
384
 
@@ -364,6 +410,24 @@ async function promptForAppName(defaultName) {
364
410
  return appName.trim();
365
411
  }
366
412
 
413
+ /**
414
+ * Prompt: Run with saved config? (Y/n). Used when resuming from existing wizard.yaml.
415
+ * @async
416
+ * @function promptForRunWithSavedConfig
417
+ * @returns {Promise<boolean>} True to run with saved config, false to exit
418
+ */
419
+ async function promptForRunWithSavedConfig() {
420
+ const { run } = await inquirer.prompt([
421
+ {
422
+ type: 'confirm',
423
+ name: 'run',
424
+ message: 'Run with saved config?',
425
+ default: true
426
+ }
427
+ ]);
428
+ return run;
429
+ }
430
+
367
431
  module.exports = {
368
432
  promptForMode,
369
433
  promptForSystemIdOrKey,
@@ -371,10 +435,13 @@ module.exports = {
371
435
  promptForOpenApiFile,
372
436
  promptForOpenApiUrl,
373
437
  promptForMcpServer,
438
+ promptForCredentialAction,
439
+ promptForCredentialIdOrKeyRetry,
374
440
  promptForKnownPlatform,
375
441
  promptForUserIntent,
376
442
  promptForUserPreferences,
377
443
  promptForConfigReview,
378
- promptForAppName
444
+ promptForAppName,
445
+ promptForRunWithSavedConfig
379
446
  };
380
447
 
@@ -132,6 +132,8 @@ async function waitForServices(devId = null) {
132
132
  * @async
133
133
  * @function checkInfraHealth
134
134
  * @param {number|string|null} [devId] - Developer ID (null = use current)
135
+ * @param {Object} [options] - Options
136
+ * @param {boolean} [options.strict=false] - When true, only consider current dev's containers (no fallback to dev 0); use for up-miso and status consistency
135
137
  * @returns {Promise<Object>} Health status of each service
136
138
  * @throws {Error} If health check fails
137
139
  *
@@ -139,20 +141,21 @@ async function waitForServices(devId = null) {
139
141
  * const health = await checkInfraHealth();
140
142
  * // Returns: { postgres: 'healthy', redis: 'healthy', pgadmin: 'healthy', redis-commander: 'healthy' }
141
143
  */
142
- async function checkInfraHealth(devId = null) {
144
+ async function checkInfraHealth(devId = null, options = {}) {
143
145
  const developerId = devId || await config.getDeveloperId();
144
146
  const servicesWithHealthCheck = ['postgres', 'redis'];
145
147
  const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
146
148
  const health = {};
149
+ const lookupOptions = options.strict ? { strict: true } : {};
147
150
 
148
151
  // Check health status for services with health checks
149
152
  for (const service of servicesWithHealthCheck) {
150
- health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
153
+ health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId, lookupOptions);
151
154
  }
152
155
 
153
156
  // Check if services without health checks are running
154
157
  for (const service of servicesWithoutHealthCheck) {
155
- health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
158
+ health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId, lookupOptions);
156
159
  }
157
160
 
158
161
  return health;
@@ -138,6 +138,7 @@ async function tryFindDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
138
138
 
139
139
  /**
140
140
  * Creates error data for authentication failure
141
+ * Deduplicates attemptedUrls so the same URL is not shown twice (e.g. when controller URL and device config URL match).
141
142
  * @function createAuthErrorData
142
143
  * @param {Error|null} lastError - Last error encountered
143
144
  * @param {string|null} controllerUrl - Original controller URL
@@ -145,10 +146,16 @@ async function tryFindDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
145
146
  * @returns {Object} Error data object
146
147
  */
147
148
  function createAuthErrorData(lastError, controllerUrl, attemptedUrls) {
149
+ const seen = new Set();
150
+ const uniqueUrls = attemptedUrls.filter(u => {
151
+ if (seen.has(u)) return false;
152
+ seen.add(u);
153
+ return true;
154
+ });
148
155
  return {
149
156
  message: lastError ? lastError.message : 'No valid authentication found',
150
- controllerUrl: controllerUrl || (attemptedUrls.length > 0 ? attemptedUrls[0] : undefined),
151
- attemptedUrls: attemptedUrls.length > 1 ? attemptedUrls : undefined,
157
+ controllerUrl: controllerUrl || (uniqueUrls.length > 0 ? uniqueUrls[0] : undefined),
158
+ attemptedUrls: uniqueUrls.length > 1 ? uniqueUrls : undefined,
152
159
  correlationId: undefined
153
160
  };
154
161
  }
@@ -8,6 +8,8 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const path = require('path');
12
+ const fs = require('fs').promises;
11
13
  const logger = require('./logger');
12
14
 
13
15
  /**
@@ -289,10 +291,47 @@ function logError(command, errorMessages) {
289
291
  function handleCommandError(error, command) {
290
292
  const errorMessages = formatError(error);
291
293
  logError(command, errorMessages);
294
+ if (error.wizardResumeMessage) {
295
+ logger.log(error.wizardResumeMessage);
296
+ }
297
+ }
298
+
299
+ /** Strip ANSI escape codes for plain-text logging (ESC [...] m) */
300
+ // eslint-disable-next-line no-control-regex -- intentional: match ANSI CSI sequences
301
+ const ANSI_CODE_RE = /\x1b\[[\d;]*m/g;
302
+ function stripAnsi(str) {
303
+ return typeof str === 'string' ? str.replace(ANSI_CODE_RE, '') : str;
304
+ }
305
+
306
+ /**
307
+ * Appends a wizard error to integration/<appKey>/error.log (timestamp + message only; no stack or secrets).
308
+ * Uses full formatted message (with validation details) when error.formatted is set, stripped of ANSI.
309
+ * Does not throw; logs and ignores write failures.
310
+ * @param {string} appKey - Application/integration key (e.g. app name or system key)
311
+ * @param {Error} error - The error that occurred
312
+ * @returns {Promise<void>}
313
+ */
314
+ async function appendWizardError(appKey, error) {
315
+ if (!appKey || typeof appKey !== 'string' || !/^[a-z0-9-_]+$/.test(appKey)) {
316
+ return;
317
+ }
318
+ const dir = path.join(process.cwd(), 'integration', appKey);
319
+ const logPath = path.join(dir, 'error.log');
320
+ const rawMessage = (error && error.message) ? String(error.message) : String(error);
321
+ const fullPlain = (error && error.formatted) ? stripAnsi(error.formatted) : null;
322
+ const message = fullPlain && fullPlain.length > rawMessage.length ? fullPlain : rawMessage;
323
+ const line = `${new Date().toISOString()} ${message}\n`;
324
+ try {
325
+ await fs.mkdir(dir, { recursive: true });
326
+ await fs.appendFile(logPath, line, 'utf8');
327
+ } catch (e) {
328
+ logger.warn(`Could not write wizard error.log: ${e.message}`);
329
+ }
292
330
  }
293
331
 
294
332
  module.exports = {
295
333
  validateCommand,
296
- handleCommandError
334
+ handleCommandError,
335
+ appendWizardError
297
336
  };
298
337
 
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
+ const { getPermissionDetailLines } = require('./permission-errors');
12
13
 
13
14
  /**
14
15
  * Formats authentication error
@@ -108,6 +109,13 @@ function formatAuthenticationError(errorData) {
108
109
  addControllerUrlInfo(lines, errorData);
109
110
  addAttemptedUrlsInfo(lines, errorData);
110
111
  addErrorMessageIfNotGeneric(lines, errorData);
112
+
113
+ // Show permission details when API returns them (401 "insufficient permissions" etc.)
114
+ const permissionLines = getPermissionDetailLines(errorData);
115
+ if (permissionLines.length > 0) {
116
+ lines.push(...permissionLines);
117
+ }
118
+
111
119
  addAuthenticationGuidance(lines, errorData);
112
120
 
113
121
  return lines.join('\n');
@@ -12,31 +12,44 @@ const chalk = require('chalk');
12
12
 
13
13
  /**
14
14
  * Extracts missing permissions from error data
15
+ * Supports: missingPermissions, missing.permissions, data.missing.permissions
15
16
  * @param {Object} errorData - Error response data
16
17
  * @returns {Array<string>} Array of missing permissions
17
18
  */
18
19
  function extractMissingPermissions(errorData) {
20
+ if (!errorData || typeof errorData !== 'object') return [];
19
21
  if (errorData.missingPermissions && Array.isArray(errorData.missingPermissions)) {
20
22
  return errorData.missingPermissions;
21
23
  }
22
24
  if (errorData.missing && errorData.missing.permissions && Array.isArray(errorData.missing.permissions)) {
23
25
  return errorData.missing.permissions;
24
26
  }
27
+ if (errorData.data?.missing?.permissions && Array.isArray(errorData.data.missing.permissions)) {
28
+ return errorData.data.missing.permissions;
29
+ }
25
30
  return [];
26
31
  }
27
32
 
28
33
  /**
29
34
  * Extracts required permissions from error data
35
+ * Supports: requiredPermissions, required.permissions, permissions (array), data.required.permissions
30
36
  * @param {Object} errorData - Error response data
31
37
  * @returns {Array<string>} Array of required permissions
32
38
  */
33
39
  function extractRequiredPermissions(errorData) {
40
+ if (!errorData || typeof errorData !== 'object') return [];
34
41
  if (errorData.requiredPermissions && Array.isArray(errorData.requiredPermissions)) {
35
42
  return errorData.requiredPermissions;
36
43
  }
37
44
  if (errorData.required && errorData.required.permissions && Array.isArray(errorData.required.permissions)) {
38
45
  return errorData.required.permissions;
39
46
  }
47
+ if (errorData.permissions && Array.isArray(errorData.permissions)) {
48
+ return errorData.permissions;
49
+ }
50
+ if (errorData.data?.required?.permissions && Array.isArray(errorData.data.required.permissions)) {
51
+ return errorData.data.required.permissions;
52
+ }
40
53
  return [];
41
54
  }
42
55
 
@@ -56,6 +69,33 @@ function addPermissionList(lines, perms, label) {
56
69
  }
57
70
  }
58
71
 
72
+ /**
73
+ * Returns permission detail lines for appending to any error formatter (401, 403).
74
+ * Use when the API returns required/missing permissions in the error body.
75
+ * @param {Object} errorData - Error response data (may include required.permissions, missing.permissions, etc.)
76
+ * @returns {string[]} Array of formatted lines to append (may be empty)
77
+ */
78
+ function getPermissionDetailLines(errorData) {
79
+ const lines = [];
80
+ const missingPerms = extractMissingPermissions(errorData);
81
+ const requiredPerms = extractRequiredPermissions(errorData);
82
+ if (missingPerms.length > 0) {
83
+ lines.push(chalk.yellow('Missing permissions:'));
84
+ missingPerms.forEach(perm => {
85
+ lines.push(chalk.gray(` - ${perm}`));
86
+ });
87
+ lines.push('');
88
+ }
89
+ if (requiredPerms.length > 0) {
90
+ lines.push(chalk.yellow('Required permissions:'));
91
+ requiredPerms.forEach(perm => {
92
+ lines.push(chalk.gray(` - ${perm}`));
93
+ });
94
+ lines.push('');
95
+ }
96
+ return lines;
97
+ }
98
+
59
99
  /**
60
100
  * Formats permission error with missing and required permissions
61
101
  * @param {Object} errorData - Error response data
@@ -89,6 +129,9 @@ function formatPermissionError(errorData) {
89
129
  }
90
130
 
91
131
  module.exports = {
92
- formatPermissionError
132
+ formatPermissionError,
133
+ getPermissionDetailLines,
134
+ extractMissingPermissions,
135
+ extractRequiredPermissions
93
136
  };
94
137
 
@@ -21,24 +21,25 @@ const execAsync = promisify(exec);
21
21
  * @async
22
22
  * @param {string} serviceName - Service name
23
23
  * @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
24
+ * @param {Object} [options] - Options
25
+ * @param {boolean} [options.strict=false] - When true, only match current dev's container (no fallback to dev 0 / infra-*); use for status display
24
26
  * @returns {Promise<string|null>} Container name or null if not found
25
27
  */
26
- async function findContainer(serviceName, devId = null) {
28
+ async function findContainer(serviceName, devId = null, options = {}) {
27
29
  try {
28
30
  const developerId = devId || await config.getDeveloperId();
29
31
  const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
30
32
  // Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
31
33
  const primaryPattern = idNum === 0 ? `aifabrix-${serviceName}` : `aifabrix-dev${developerId}-${serviceName}`;
32
34
 
33
- // Search order expected by tests:
34
- // 1) primaryPattern
35
- // 2) infra-{serviceName} (old pattern)
36
- // 3) aifabrix-{serviceName} (base pattern)
37
- const patternsToTry = [
38
- primaryPattern,
39
- `infra-${serviceName}`,
40
- `aifabrix-${serviceName}`
41
- ];
35
+ // When strict (e.g. status command), only show this developer's infra; no fallback to dev 0
36
+ const patternsToTry = options.strict
37
+ ? [primaryPattern]
38
+ : [
39
+ primaryPattern,
40
+ `infra-${serviceName}`,
41
+ `aifabrix-${serviceName}`
42
+ ];
42
43
 
43
44
  for (const pattern of patternsToTry) {
44
45
  const { stdout } = await execAsync(`docker ps --filter "name=${pattern}" --format "{{.Names}}"`);
@@ -64,12 +65,13 @@ async function findContainer(serviceName, devId = null) {
64
65
  * @private
65
66
  * @async
66
67
  * @param {string} serviceName - Service name
67
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
68
+ * @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
69
+ * @param {Object} [options] - Options (e.g. { strict: true } for current dev only)
68
70
  * @returns {Promise<string>} Health status
69
71
  */
70
- async function checkServiceWithHealthCheck(serviceName, devId = null) {
72
+ async function checkServiceWithHealthCheck(serviceName, devId = null, options = {}) {
71
73
  try {
72
- const containerName = await findContainer(serviceName, devId);
74
+ const containerName = await findContainer(serviceName, devId, options);
73
75
  if (!containerName) {
74
76
  return 'unknown';
75
77
  }
@@ -87,12 +89,13 @@ async function checkServiceWithHealthCheck(serviceName, devId = null) {
87
89
  * @private
88
90
  * @async
89
91
  * @param {string} serviceName - Service name
90
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
92
+ * @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
93
+ * @param {Object} [options] - Options (e.g. { strict: true } for current dev only)
91
94
  * @returns {Promise<string>} Health status
92
95
  */
93
- async function checkServiceWithoutHealthCheck(serviceName, devId = null) {
96
+ async function checkServiceWithoutHealthCheck(serviceName, devId = null, options = {}) {
94
97
  try {
95
- const containerName = await findContainer(serviceName, devId);
98
+ const containerName = await findContainer(serviceName, devId, options);
96
99
  if (!containerName) {
97
100
  return 'unknown';
98
101
  }
@@ -40,7 +40,7 @@ async function getInfraStatus() {
40
40
  pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
41
41
  'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` },
42
42
  traefik: {
43
- port: `${ports.traefikHttp}/${ports.traefikHttps}`,
43
+ port: `${ports.traefikHttp}, ${ports.traefikHttps}`,
44
44
  url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
45
45
  }
46
46
  };
@@ -49,7 +49,8 @@ async function getInfraStatus() {
49
49
 
50
50
  for (const [serviceName, serviceConfig] of Object.entries(services)) {
51
51
  try {
52
- const containerName = await containerUtils.findContainer(serviceName, devId);
52
+ // Strict: only this developer's infra (no fallback to dev 0), so status reflects reality
53
+ const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
53
54
  if (containerName) {
54
55
  const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
55
56
  // Normalize status value (trim whitespace and remove quotes)
@@ -97,6 +98,9 @@ function getInfraContainerNames(devIdNum, devId) {
97
98
  ];
98
99
  }
99
100
 
101
+ /** Suffixes for init/helper containers to exclude from "Running Applications" (e.g. keycloak-db-init) */
102
+ const INIT_CONTAINER_SUFFIXES = ['-db-init', '-init'];
103
+
100
104
  /**
101
105
  * Extracts app name from container name
102
106
  * @param {string} containerName - Container name
@@ -107,7 +111,12 @@ function getInfraContainerNames(devIdNum, devId) {
107
111
  function extractAppName(containerName, devIdNum, devId) {
108
112
  const pattern = devIdNum === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
109
113
  const match = containerName.match(pattern);
110
- return match ? match[1] : null;
114
+ if (!match) return null;
115
+ const appName = match[1];
116
+ if (INIT_CONTAINER_SUFFIXES.some(suffix => appName.endsWith(suffix))) {
117
+ return null;
118
+ }
119
+ return appName;
111
120
  }
112
121
 
113
122
  /**
@@ -114,6 +114,39 @@ async function loadWizardConfig(configPath) {
114
114
  }
115
115
  }
116
116
 
117
+ /**
118
+ * Write wizard configuration to wizard.yaml (creates directory if needed).
119
+ * @async
120
+ * @function writeWizardConfig
121
+ * @param {string} configPath - Path to wizard.yaml file
122
+ * @param {Object} config - Configuration object to write (will be dumped as YAML)
123
+ * @returns {Promise<void>}
124
+ * @throws {Error} If write fails
125
+ */
126
+ async function writeWizardConfig(configPath, config) {
127
+ const resolvedPath = path.resolve(configPath);
128
+ const dir = path.dirname(resolvedPath);
129
+ await fs.mkdir(dir, { recursive: true });
130
+ const content = yaml.dump(config, { lineWidth: -1 });
131
+ await fs.writeFile(resolvedPath, content, 'utf8');
132
+ }
133
+
134
+ /**
135
+ * Check if wizard.yaml exists at path
136
+ * @async
137
+ * @function wizardConfigExists
138
+ * @param {string} configPath - Path to wizard.yaml file
139
+ * @returns {Promise<boolean>}
140
+ */
141
+ async function wizardConfigExists(configPath) {
142
+ try {
143
+ await fs.access(path.resolve(configPath));
144
+ return true;
145
+ } catch (error) {
146
+ return error.code === 'ENOENT' ? false : Promise.reject(error);
147
+ }
148
+ }
149
+
117
150
  /**
118
151
  * Validate wizard configuration against schema
119
152
  * @function validateWizardConfigSchema
@@ -258,6 +291,8 @@ function displayValidationResults(result) {
258
291
 
259
292
  module.exports = {
260
293
  loadWizardConfig,
294
+ writeWizardConfig,
295
+ wizardConfigExists,
261
296
  validateWizardConfig,
262
297
  validateWizardConfigSchema,
263
298
  resolveEnvVar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.33.5",
3
+ "version": "2.36.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "node tests/scripts/test-wrapper.js",
12
12
  "test:ci": "bash tests/scripts/ci-simulate.sh",
13
- "test:coverage": "jest --config jest.config.coverage.js --coverage --runInBand",
13
+ "test:coverage": "cross-env RUN_COVERAGE=1 node tests/scripts/test-wrapper.js",
14
14
  "test:coverage:nyc": "nyc --reporter=text --reporter=lcov --reporter=html jest --config jest.config.coverage.js --runInBand",
15
15
  "test:watch": "jest --watch",
16
16
  "test:integration": "jest --config jest.config.integration.js --runInBand",