@aifabrix/builder 2.31.0 → 2.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +34 -3
  111. package/scripts/install-local.js +210 -0
  112. package/templates/external-system/deploy.ps1.hbs +34 -0
  113. package/templates/external-system/deploy.sh.hbs +34 -0
  114. package/templates/external-system/external-datasource.json.hbs +31 -12
  115. package/lib/app.js +0 -467
  116. package/lib/datasource-list.js +0 -141
  117. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  118. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  119. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -24,40 +24,91 @@ const execAsync = promisify(exec);
24
24
  * @param {string} appName - Application name
25
25
  * @throws {Error} If db-init fails
26
26
  */
27
- async function waitForDbInit(appName) {
28
- const dbInitContainer = `aifabrix-${appName}-db-init`;
27
+ /**
28
+ * Checks if db-init container exists
29
+ * @async
30
+ * @function checkDbInitContainerExists
31
+ * @param {string} dbInitContainer - Container name
32
+ * @returns {Promise<boolean>} True if container exists
33
+ */
34
+ async function checkDbInitContainerExists(dbInitContainer) {
29
35
  try {
30
36
  const { stdout } = await execAsync(`docker ps -a --filter "name=${dbInitContainer}" --format "{{.Names}}"`);
31
- if (stdout.trim() !== dbInitContainer) {
32
- return;
37
+ return stdout.trim() === dbInitContainer;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Gets container exit code
45
+ * @async
46
+ * @function getContainerExitCode
47
+ * @param {string} dbInitContainer - Container name
48
+ * @returns {Promise<string>} Exit code
49
+ */
50
+ async function getContainerExitCode(dbInitContainer) {
51
+ const { stdout: exitCode } = await execAsync(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
52
+ return exitCode.trim();
53
+ }
54
+
55
+ /**
56
+ * Handles exited container status
57
+ * @async
58
+ * @function handleExitedContainer
59
+ * @param {string} dbInitContainer - Container name
60
+ * @returns {Promise<boolean>} True if handled (container already exited)
61
+ */
62
+ async function handleExitedContainer(dbInitContainer) {
63
+ const { stdout: status } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
64
+ if (status.trim() === 'exited') {
65
+ const exitCode = await getContainerExitCode(dbInitContainer);
66
+ if (exitCode === '0') {
67
+ logger.log(chalk.green('✓ Database initialization already completed'));
68
+ } else {
69
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
33
70
  }
71
+ return true;
72
+ }
73
+ return false;
74
+ }
34
75
 
35
- const { stdout: status } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
36
- if (status.trim() === 'exited') {
37
- const { stdout: exitCode } = await execAsync(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
38
- if (exitCode.trim() === '0') {
39
- logger.log(chalk.green('✓ Database initialization already completed'));
76
+ /**
77
+ * Waits for container to exit
78
+ * @async
79
+ * @function waitForContainerExit
80
+ * @param {string} dbInitContainer - Container name
81
+ * @param {number} maxAttempts - Maximum attempts
82
+ */
83
+ async function waitForContainerExit(dbInitContainer, maxAttempts) {
84
+ for (let attempts = 0; attempts < maxAttempts; attempts++) {
85
+ const { stdout: currentStatus } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
86
+ if (currentStatus.trim() === 'exited') {
87
+ const exitCode = await getContainerExitCode(dbInitContainer);
88
+ if (exitCode === '0') {
89
+ logger.log(chalk.green('✓ Database initialization completed'));
40
90
  } else {
41
- logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode.trim()}`));
91
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
42
92
  }
43
93
  return;
44
94
  }
95
+ await new Promise(resolve => setTimeout(resolve, 1000));
96
+ }
97
+ }
45
98
 
46
- logger.log(chalk.blue('Waiting for database initialization to complete...'));
47
- const maxDbInitAttempts = 30;
48
- for (let dbInitAttempts = 0; dbInitAttempts < maxDbInitAttempts; dbInitAttempts++) {
49
- const { stdout: currentStatus } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
50
- if (currentStatus.trim() === 'exited') {
51
- const { stdout: exitCode } = await execAsync(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
52
- if (exitCode.trim() === '0') {
53
- logger.log(chalk.green('✓ Database initialization completed'));
54
- } else {
55
- logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode.trim()}`));
56
- }
57
- return;
58
- }
59
- await new Promise(resolve => setTimeout(resolve, 1000));
99
+ async function waitForDbInit(appName) {
100
+ const dbInitContainer = `aifabrix-${appName}-db-init`;
101
+ try {
102
+ if (!(await checkDbInitContainerExists(dbInitContainer))) {
103
+ return;
104
+ }
105
+
106
+ if (await handleExitedContainer(dbInitContainer)) {
107
+ return;
60
108
  }
109
+
110
+ logger.log(chalk.blue('Waiting for database initialization to complete...'));
111
+ await waitForContainerExit(dbInitContainer, 30);
61
112
  } catch (error) {
62
113
  // db-init container might not exist, which is fine
63
114
  }
@@ -71,56 +122,80 @@ async function waitForDbInit(appName) {
71
122
  * @param {boolean} [debug=false] - Enable debug logging
72
123
  * @returns {Promise<number>} Container port
73
124
  */
125
+ /**
126
+ * Gets port from docker inspect
127
+ * @async
128
+ * @function getPortFromDockerInspect
129
+ * @param {string} appName - Application name
130
+ * @param {boolean} debug - Debug flag
131
+ * @returns {Promise<number|null>} Port number or null
132
+ */
133
+ async function getPortFromDockerInspect(appName, debug) {
134
+ const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`;
135
+ if (debug) {
136
+ logger.log(chalk.gray(`[DEBUG] Executing: ${inspectCmd}`));
137
+ }
138
+ const { stdout: portMapping } = await execAsync(inspectCmd);
139
+ const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
140
+ if (ports.length > 0) {
141
+ const port = parseInt(ports[0], 10);
142
+ if (!isNaN(port) && port > 0) {
143
+ if (debug) {
144
+ logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker inspect`));
145
+ }
146
+ return port;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Gets port from docker ps (fallback)
154
+ * @async
155
+ * @function getPortFromDockerPs
156
+ * @param {string} appName - Application name
157
+ * @param {boolean} debug - Debug flag
158
+ * @returns {Promise<number|null>} Port number or null
159
+ */
160
+ async function getPortFromDockerPs(appName, debug) {
161
+ const psCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
162
+ if (debug) {
163
+ logger.log(chalk.gray(`[DEBUG] Fallback: Executing: ${psCmd}`));
164
+ }
165
+ const { stdout: psOutput } = await execAsync(psCmd);
166
+ const portMatch = psOutput.match(/:(\d+)->/);
167
+ if (!portMatch) {
168
+ return null;
169
+ }
170
+ const port = parseInt(portMatch[1], 10);
171
+ if (isNaN(port) || port <= 0) {
172
+ return null;
173
+ }
174
+ if (debug) {
175
+ logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
176
+ }
177
+ return port;
178
+ }
179
+
74
180
  async function getContainerPort(appName, debug = false) {
75
181
  try {
76
- // Try to get the actual mapped host port from Docker
77
- // First try docker inspect for the container port mapping
78
- const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`;
79
- if (debug) {
80
- logger.log(chalk.gray(`[DEBUG] Executing: ${inspectCmd}`));
81
- }
82
- const { stdout: portMapping } = await execAsync(inspectCmd);
83
- const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
84
- if (ports.length > 0) {
85
- const port = parseInt(ports[0], 10);
86
- if (!isNaN(port) && port > 0) {
87
- if (debug) {
88
- logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker inspect`));
89
- }
90
- return port;
91
- }
182
+ const port = await getPortFromDockerInspect(appName, debug);
183
+ if (port !== null) {
184
+ return port;
92
185
  }
93
186
 
94
- // Fallback: try docker ps to get port mapping (format: "0.0.0.0:3010->3000/tcp")
187
+ // Fallback: try docker ps
95
188
  try {
96
- const psCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
97
- if (debug) {
98
- logger.log(chalk.gray(`[DEBUG] Fallback: Executing: ${psCmd}`));
99
- }
100
- const { stdout: psOutput } = await execAsync(psCmd);
101
- const portMatch = psOutput.match(/:(\d+)->/);
102
- if (!portMatch) {
103
- return null;
104
- }
105
- const port = parseInt(portMatch[1], 10);
106
- if (isNaN(port) || port <= 0) {
107
- return null;
108
- }
109
- if (debug) {
110
- logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
111
- }
112
- return port;
189
+ return await getPortFromDockerPs(appName, debug);
113
190
  } catch (error) {
114
191
  if (debug) {
115
192
  logger.log(chalk.gray(`[DEBUG] Fallback port detection failed: ${error.message}`));
116
193
  }
117
- // Fall through
118
194
  }
119
195
  } catch (error) {
120
196
  if (debug) {
121
197
  logger.log(chalk.gray(`[DEBUG] Port detection failed: ${error.message}`));
122
198
  }
123
- // Fall through to default
124
199
  }
125
200
  if (debug) {
126
201
  logger.log(chalk.gray('[DEBUG] Using default port 3000'));
@@ -250,17 +325,33 @@ async function checkHealthEndpoint(healthCheckUrl, debug = false) {
250
325
  * @returns {Promise<void>} Resolves when health check passes
251
326
  * @throws {Error} If health check times out
252
327
  */
253
- async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false) {
254
- await waitForDbInit(appName);
255
-
256
- // Use provided port if given, otherwise detect from Docker
257
- // Port provided should be the host port (CLI --port or config.port, NOT localPort)
328
+ /**
329
+ * Determines health check port
330
+ * @async
331
+ * @function determineHealthCheckPort
332
+ * @param {number|null} port - Provided port
333
+ * @param {string} appName - Application name
334
+ * @param {boolean} debug - Debug flag
335
+ * @returns {Promise<number>} Health check port
336
+ */
337
+ async function determineHealthCheckPort(port, appName, debug) {
258
338
  const healthCheckPort = port !== null && port !== undefined ? port : await getContainerPort(appName, debug);
259
-
260
339
  if (debug) {
261
340
  logger.log(chalk.gray(`[DEBUG] Health check port: ${healthCheckPort} (${port !== null && port !== undefined ? 'provided' : 'auto-detected'})`));
262
341
  }
342
+ return healthCheckPort;
343
+ }
263
344
 
345
+ /**
346
+ * Builds health check configuration
347
+ * @function buildHealthCheckConfig
348
+ * @param {number} healthCheckPort - Health check port
349
+ * @param {Object|null} config - Configuration object
350
+ * @param {number} timeout - Timeout in seconds
351
+ * @param {boolean} debug - Debug flag
352
+ * @returns {Object} Health check configuration
353
+ */
354
+ function buildHealthCheckConfig(healthCheckPort, config, timeout, debug) {
264
355
  const healthCheckPath = config?.healthCheck?.path || '/health';
265
356
  const healthCheckUrl = `http://localhost:${healthCheckPort}${healthCheckPath}`;
266
357
  const maxAttempts = timeout / 2;
@@ -270,25 +361,50 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
270
361
  logger.log(chalk.gray(`[DEBUG] Timeout: ${timeout} seconds, Max attempts: ${maxAttempts}`));
271
362
  }
272
363
 
273
- for (let attempts = 0; attempts < maxAttempts; attempts++) {
274
- try {
275
- if (debug) {
276
- logger.log(chalk.gray(`[DEBUG] Health check attempt ${attempts + 1}/${maxAttempts}`));
277
- }
278
- const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl, debug);
279
- if (healthCheckPassed) {
280
- logger.log(chalk.green('✓ Application is healthy'));
281
- if (debug) {
282
- logger.log(chalk.gray(`[DEBUG] Health check passed after ${attempts + 1} attempt(s)`));
283
- }
284
- return;
285
- }
286
- } catch (error) {
364
+ return { healthCheckUrl, maxAttempts };
365
+ }
366
+
367
+ /**
368
+ * Performs a single health check attempt
369
+ * @async
370
+ * @function performHealthCheckAttempt
371
+ * @param {string} healthCheckUrl - Health check URL
372
+ * @param {number} attempt - Attempt number
373
+ * @param {number} maxAttempts - Maximum attempts
374
+ * @param {boolean} debug - Debug flag
375
+ * @returns {Promise<boolean>} True if health check passed
376
+ */
377
+ async function performHealthCheckAttempt(healthCheckUrl, attempt, maxAttempts, debug) {
378
+ try {
379
+ if (debug) {
380
+ logger.log(chalk.gray(`[DEBUG] Health check attempt ${attempt + 1}/${maxAttempts}`));
381
+ }
382
+ const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl, debug);
383
+ if (healthCheckPassed) {
384
+ logger.log(chalk.green('✓ Application is healthy'));
287
385
  if (debug) {
288
- logger.log(chalk.gray(`[DEBUG] Health check exception on attempt ${attempts + 1}: ${error.message}`));
386
+ logger.log(chalk.gray(`[DEBUG] Health check passed after ${attempt + 1} attempt(s)`));
289
387
  }
290
- // If exception occurs, continue retrying until timeout
291
- // The error will be handled by timeout error below
388
+ return true;
389
+ }
390
+ } catch (error) {
391
+ if (debug) {
392
+ logger.log(chalk.gray(`[DEBUG] Health check exception on attempt ${attempt + 1}: ${error.message}`));
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+
398
+ async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false) {
399
+ await waitForDbInit(appName);
400
+
401
+ const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
402
+ const { healthCheckUrl, maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
403
+
404
+ for (let attempts = 0; attempts < maxAttempts; attempts++) {
405
+ const passed = await performHealthCheckAttempt(healthCheckUrl, attempts, maxAttempts, debug);
406
+ if (passed) {
407
+ return;
292
408
  }
293
409
 
294
410
  if (attempts < maxAttempts - 1) {
@@ -11,7 +11,7 @@
11
11
 
12
12
  const { exec } = require('child_process');
13
13
  const { promisify } = require('util');
14
- const config = require('../config');
14
+ const config = require('../core/config');
15
15
 
16
16
  const execAsync = promisify(exec);
17
17
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  const { exec } = require('child_process');
13
13
  const { promisify } = require('util');
14
- const config = require('../config');
14
+ const config = require('../core/config');
15
15
  const devConfig = require('./dev-config');
16
16
  const containerUtils = require('./infra-containers');
17
17
 
@@ -74,6 +74,64 @@ async function getInfraStatus() {
74
74
  return status;
75
75
  }
76
76
 
77
+ /**
78
+ * Gets infrastructure container names for a developer ID
79
+ * @param {number} devIdNum - Developer ID number
80
+ * @param {string} devId - Developer ID string
81
+ * @returns {Array<string>} Array of infrastructure container names
82
+ */
83
+ function getInfraContainerNames(devIdNum, devId) {
84
+ if (devIdNum === 0) {
85
+ return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander'];
86
+ }
87
+ return [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
88
+ }
89
+
90
+ /**
91
+ * Extracts app name from container name
92
+ * @param {string} containerName - Container name
93
+ * @param {number} devIdNum - Developer ID number
94
+ * @param {string} devId - Developer ID string
95
+ * @returns {string|null} App name or null if not matched
96
+ */
97
+ function extractAppName(containerName, devIdNum, devId) {
98
+ const pattern = devIdNum === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
99
+ const match = containerName.match(pattern);
100
+ return match ? match[1] : null;
101
+ }
102
+
103
+ /**
104
+ * Extracts host port from docker ports string
105
+ * @param {string} ports - Docker ports string
106
+ * @returns {string} Host port or 'unknown'
107
+ */
108
+ function extractHostPort(ports) {
109
+ const portMatch = ports.match(/:(\d+)->\d+\//);
110
+ return portMatch ? portMatch[1] : 'unknown';
111
+ }
112
+
113
+ /**
114
+ * Parses container line and creates app status object
115
+ * @param {string} line - Container line from docker ps
116
+ * @param {Array<string>} infraContainers - Infrastructure container names
117
+ * @param {number} devIdNum - Developer ID number
118
+ * @param {string} devId - Developer ID string
119
+ * @returns {Object|null} App status object or null if not an app container
120
+ */
121
+ function parseContainerLine(line, infraContainers, devIdNum, devId) {
122
+ const [containerName, ports, status] = line.split('\t');
123
+ if (infraContainers.includes(containerName)) {
124
+ return null;
125
+ }
126
+ const appName = extractAppName(containerName, devIdNum, devId);
127
+ if (!appName) {
128
+ return null;
129
+ }
130
+ const hostPort = extractHostPort(ports);
131
+ const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
132
+ return { name: appName, container: containerName, port: ports, status: status.trim(), url: url };
133
+ }
134
+
77
135
  /**
78
136
  * Gets status of running application containers
79
137
  * Finds all containers matching pattern aifabrix-dev{id}-* (excluding infrastructure)
@@ -91,23 +149,16 @@ async function getAppStatus() {
91
149
  const apps = [];
92
150
 
93
151
  try {
94
- const filterPattern = devId === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
152
+ const devIdNum = parseInt(devId, 10);
153
+ const filterPattern = devIdNum === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
95
154
  const { stdout } = await execAsync(`docker ps --filter "name=${filterPattern}" --format "{{.Names}}\t{{.Ports}}\t{{.Status}}"`);
96
155
  const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
97
- const infraContainers = devId === 0
98
- ? ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander']
99
- : [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
156
+ const infraContainers = getInfraContainerNames(devIdNum, devId);
100
157
  for (const line of lines) {
101
- const [containerName, ports, status] = line.split('\t');
102
- if (infraContainers.includes(containerName)) continue;
103
- const pattern = devId === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
104
- const appNameMatch = containerName.match(pattern);
105
- if (!appNameMatch) continue;
106
- const appName = appNameMatch[1];
107
- const portMatch = ports.match(/:(\d+)->\d+\//);
108
- const hostPort = portMatch ? portMatch[1] : 'unknown';
109
- const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
110
- apps.push({ name: appName, container: containerName, port: ports, status: status.trim(), url: url });
158
+ const appStatus = parseContainerLine(line, infraContainers, devIdNum, devId);
159
+ if (appStatus) {
160
+ apps.push(appStatus);
161
+ }
111
162
  }
112
163
  } catch (error) {
113
164
  return [];
@@ -93,53 +93,73 @@ async function saveLocalSecret(key, value) {
93
93
  * @example
94
94
  * await saveSecret('myapp-client-idKeyVault', 'client-id-value', '/path/to/secrets.yaml');
95
95
  */
96
- async function saveSecret(key, value, secretsPath) {
96
+ /**
97
+ * Validates save secret parameters
98
+ * @function validateSaveSecretParams
99
+ * @param {string} key - Secret key
100
+ * @param {*} value - Secret value
101
+ * @param {string} secretsPath - Secrets path
102
+ * @throws {Error} If validation fails
103
+ */
104
+ function validateSaveSecretParams(key, value, secretsPath) {
97
105
  if (!key || typeof key !== 'string') {
98
106
  throw new Error('Secret key is required and must be a string');
99
107
  }
100
-
101
108
  if (value === undefined || value === null) {
102
109
  throw new Error('Secret value is required');
103
110
  }
104
-
105
111
  if (!secretsPath || typeof secretsPath !== 'string') {
106
112
  throw new Error('Secrets path is required and must be a string');
107
113
  }
114
+ }
108
115
 
109
- // Resolve path (handle absolute vs relative)
116
+ /**
117
+ * Resolves and prepares secrets path
118
+ * @function resolveAndPrepareSecretsPath
119
+ * @param {string} secretsPath - Secrets path
120
+ * @returns {string} Resolved path
121
+ */
122
+ function resolveAndPrepareSecretsPath(secretsPath) {
110
123
  const resolvedPath = path.isAbsolute(secretsPath)
111
124
  ? secretsPath
112
125
  : path.resolve(process.cwd(), secretsPath);
113
126
 
114
127
  const secretsDir = path.dirname(resolvedPath);
115
-
116
- // Create directory if needed
117
128
  if (!fs.existsSync(secretsDir)) {
118
129
  fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
119
130
  }
120
131
 
121
- // Load existing secrets
122
- let existingSecrets = {};
123
- if (fs.existsSync(resolvedPath)) {
124
- try {
125
- const content = fs.readFileSync(resolvedPath, 'utf8');
126
- existingSecrets = yaml.load(content) || {};
127
- if (typeof existingSecrets !== 'object') {
128
- existingSecrets = {};
129
- }
130
- } catch (error) {
131
- logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
132
- existingSecrets = {};
133
- }
132
+ return resolvedPath;
133
+ }
134
+
135
+ /**
136
+ * Loads existing secrets from file
137
+ * @function loadExistingSecrets
138
+ * @param {string} resolvedPath - Resolved secrets path
139
+ * @returns {Object} Existing secrets object
140
+ */
141
+ function loadExistingSecrets(resolvedPath) {
142
+ if (!fs.existsSync(resolvedPath)) {
143
+ return {};
134
144
  }
135
145
 
136
- // Merge with new secret
137
- const updatedSecrets = {
138
- ...existingSecrets,
139
- [key]: value
140
- };
146
+ try {
147
+ const content = fs.readFileSync(resolvedPath, 'utf8');
148
+ const existingSecrets = yaml.load(content) || {};
149
+ return typeof existingSecrets === 'object' ? existingSecrets : {};
150
+ } catch (error) {
151
+ logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
152
+ return {};
153
+ }
154
+ }
141
155
 
142
- // Save to file
156
+ async function saveSecret(key, value, secretsPath) {
157
+ validateSaveSecretParams(key, value, secretsPath);
158
+
159
+ const resolvedPath = resolveAndPrepareSecretsPath(secretsPath);
160
+ const existingSecrets = loadExistingSecrets(resolvedPath);
161
+
162
+ const updatedSecrets = { ...existingSecrets, [key]: value };
143
163
  const yamlContent = yaml.dump(updatedSecrets, {
144
164
  indent: 2,
145
165
  lineWidth: 120,
@@ -144,66 +144,78 @@ function getFallbackProjectRoot() {
144
144
  * Works reliably in all environments including Jest tests and CI
145
145
  * @returns {string} Absolute path to project root
146
146
  */
147
- function getProjectRoot() {
148
- // SIMPLIFIED: Always use __dirname walking - most reliable in all environments
149
- // This works in: local dev, CI, tests, temp directories, etc.
150
- // __dirname is lib/utils/, so we walk up to find package.json
147
+ /**
148
+ * Checks if global PROJECT_ROOT is valid
149
+ * @function checkGlobalProjectRoot
150
+ * @returns {string|null} Valid global root or null
151
+ */
152
+ function checkGlobalProjectRoot() {
153
+ if (typeof global === 'undefined' || !global.PROJECT_ROOT) {
154
+ return null;
155
+ }
151
156
 
152
- // Return cached value if available and valid
153
- if (cachedProjectRoot && hasPackageJson(cachedProjectRoot)) {
154
- return cachedProjectRoot;
157
+ const globalRoot = global.PROJECT_ROOT;
158
+ if (!hasPackageJson(globalRoot)) {
159
+ return null;
155
160
  }
156
161
 
157
- // Strategy 1: Check global.PROJECT_ROOT if set and valid (for test isolation)
158
- // BUT: In CI simulation, the project is copied, so global.PROJECT_ROOT might point to original
159
- // We need to verify it's actually the correct root by checking if __dirname is within it
160
- if (typeof global !== 'undefined' && global.PROJECT_ROOT) {
161
- const globalRoot = global.PROJECT_ROOT;
162
- if (hasPackageJson(globalRoot)) {
163
- // Verify that __dirname is actually within globalRoot (or they're the same)
164
- // This ensures we're using the correct project root in CI simulation
165
- const dirnameNormalized = path.resolve(__dirname);
166
- const globalRootNormalized = path.resolve(globalRoot);
167
- const isWithinGlobalRoot = dirnameNormalized.startsWith(globalRootNormalized + path.sep) ||
168
- dirnameNormalized === globalRootNormalized;
169
-
170
- if (isWithinGlobalRoot) {
171
- cachedProjectRoot = globalRoot;
172
- return cachedProjectRoot;
173
- }
174
- // If global.PROJECT_ROOT doesn't contain __dirname, it's wrong (e.g., original project in CI)
175
- // Clear it and continue with other strategies
176
- }
162
+ // Verify that __dirname is actually within globalRoot
163
+ const dirnameNormalized = path.resolve(__dirname);
164
+ const globalRootNormalized = path.resolve(globalRoot);
165
+ const isWithinGlobalRoot = dirnameNormalized.startsWith(globalRootNormalized + path.sep) ||
166
+ dirnameNormalized === globalRootNormalized;
167
+
168
+ return isWithinGlobalRoot ? globalRoot : null;
169
+ }
170
+
171
+ /**
172
+ * Tries different strategies to find project root
173
+ * @function tryFindProjectRoot
174
+ * @returns {string} Found project root
175
+ */
176
+ function tryFindProjectRoot() {
177
+ // Strategy 1: Check global.PROJECT_ROOT
178
+ const globalRoot = checkGlobalProjectRoot();
179
+ if (globalRoot) {
180
+ cachedProjectRoot = globalRoot;
181
+ return cachedProjectRoot;
177
182
  }
178
183
 
179
- // Strategy 2: Walk up from __dirname (lib/utils/) - MOST RELIABLE
180
- // This always works because __dirname is always correct relative to the code
184
+ // Strategy 2: Walk up from __dirname
181
185
  const foundRoot = findProjectRootByWalkingUp(__dirname);
182
186
  if (foundRoot && hasPackageJson(foundRoot)) {
183
187
  cachedProjectRoot = foundRoot;
184
188
  return cachedProjectRoot;
185
189
  }
186
190
 
187
- // Strategy 3: Try process.cwd() (works in most cases)
191
+ // Strategy 3: Try process.cwd()
188
192
  const cwdRoot = findProjectRootFromCwd();
189
193
  if (cwdRoot && hasPackageJson(cwdRoot)) {
190
194
  cachedProjectRoot = cwdRoot;
191
195
  return cachedProjectRoot;
192
196
  }
193
197
 
194
- // Strategy 4: Fallback to __dirname relative (lib/utils/ -> project root)
198
+ // Strategy 4: Fallback
195
199
  const fallbackRoot = getFallbackProjectRoot();
196
200
  if (hasPackageJson(fallbackRoot)) {
197
201
  cachedProjectRoot = fallbackRoot;
198
202
  return cachedProjectRoot;
199
203
  }
200
204
 
201
- // Last resort: return fallback even if package.json not found
202
- // This prevents crashes but should rarely happen
205
+ // Last resort
203
206
  cachedProjectRoot = fallbackRoot;
204
207
  return cachedProjectRoot;
205
208
  }
206
209
 
210
+ function getProjectRoot() {
211
+ // Return cached value if available and valid
212
+ if (cachedProjectRoot && hasPackageJson(cachedProjectRoot)) {
213
+ return cachedProjectRoot;
214
+ }
215
+
216
+ return tryFindProjectRoot();
217
+ }
218
+
207
219
  /**
208
220
  * Returns the applications base directory for a developer.
209
221
  * Dev 0: <home>/applications