@aifabrix/builder 2.31.1 → 2.32.2

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 (118) 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} +10 -10
  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 +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  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 +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -38,31 +38,76 @@ function formatFormattedError(formatted) {
38
38
  return messages;
39
39
  }
40
40
 
41
+ /**
42
+ * Checks if error is about Docker image not found
43
+ * @function isDockerImageNotFoundError
44
+ * @param {string} errorMsg - Error message
45
+ * @returns {boolean} True if Docker image not found error
46
+ */
47
+ function isDockerImageNotFoundError(errorMsg) {
48
+ return errorMsg.includes('not found locally') ||
49
+ (errorMsg.includes('Docker image') && errorMsg.includes('not found'));
50
+ }
51
+
52
+ /**
53
+ * Checks if error is about Docker not running/installed
54
+ * @function isDockerNotRunningError
55
+ * @param {string} errorMsg - Error message
56
+ * @returns {boolean} True if Docker not running error
57
+ */
58
+ function isDockerNotRunningError(errorMsg) {
59
+ return errorMsg.includes('Docker') &&
60
+ (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'));
61
+ }
62
+
63
+ /**
64
+ * Checks if error is about port conflict
65
+ * @function isPortConflictError
66
+ * @param {string} errorMsg - Error message
67
+ * @returns {boolean} True if port conflict error
68
+ */
69
+ function isPortConflictError(errorMsg) {
70
+ return errorMsg.toLowerCase().includes('port') &&
71
+ (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'));
72
+ }
73
+
74
+ /**
75
+ * Checks if error is about permission denied (excluding permissions field)
76
+ * @function isPermissionDeniedError
77
+ * @param {string} errorMsg - Error message
78
+ * @returns {boolean} True if permission denied error
79
+ */
80
+ function isPermissionDeniedError(errorMsg) {
81
+ return (errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) &&
82
+ !errorMsg.includes('permissions/') &&
83
+ !errorMsg.includes('Field "permissions');
84
+ }
85
+
41
86
  /**
42
87
  * Format Docker-related errors
43
88
  * @param {string} errorMsg - Error message
44
89
  * @returns {string[]|null} Array of error message lines or null if not a Docker error
45
90
  */
46
91
  function formatDockerError(errorMsg) {
47
- if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
92
+ if (isDockerImageNotFoundError(errorMsg)) {
48
93
  return [
49
94
  ' Docker image not found.',
50
95
  ' Run: aifabrix build <app> first'
51
96
  ];
52
97
  }
53
- if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
98
+ if (isDockerNotRunningError(errorMsg)) {
54
99
  return [
55
100
  ' Docker is not running or not installed.',
56
101
  ' Please start Docker Desktop and try again.'
57
102
  ];
58
103
  }
59
- if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
104
+ if (isPortConflictError(errorMsg)) {
60
105
  return [
61
106
  ' Port conflict detected.',
62
107
  ' Run "aifabrix doctor" to check which ports are in use.'
63
108
  ];
64
109
  }
65
- if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
110
+ if (isPermissionDeniedError(errorMsg)) {
66
111
  return [
67
112
  ' Permission denied.',
68
113
  ' Make sure you have the necessary permissions to run Docker commands.'
@@ -179,6 +224,31 @@ function formatValidationError(errorMsg) {
179
224
  * @param {Error} error - The error that occurred
180
225
  * @returns {string[]} Array of error message lines
181
226
  */
227
+ /**
228
+ * Tries to format error using specific formatters
229
+ * @function tryFormatErrorWithFormatters
230
+ * @param {string} errorMsg - Error message
231
+ * @returns {string[]|null} Formatted error messages or null if no formatter matched
232
+ */
233
+ function tryFormatErrorWithFormatters(errorMsg) {
234
+ const formatters = [
235
+ formatDockerError,
236
+ formatAzureError,
237
+ formatSecretsError,
238
+ formatDeploymentError,
239
+ formatValidationError
240
+ ];
241
+
242
+ for (const formatter of formatters) {
243
+ const formatted = formatter(errorMsg);
244
+ if (formatted) {
245
+ return formatted;
246
+ }
247
+ }
248
+
249
+ return null;
250
+ }
251
+
182
252
  function formatError(error) {
183
253
  // If error has formatted message (from API error handler), use it directly
184
254
  if (error.formatted) {
@@ -186,37 +256,15 @@ function formatError(error) {
186
256
  }
187
257
 
188
258
  const errorMsg = error.message || '';
189
- const messages = [];
190
259
 
191
260
  // Try different error formatters in order of specificity
192
- const dockerError = formatDockerError(errorMsg);
193
- if (dockerError) {
194
- return dockerError;
195
- }
196
-
197
- const azureError = formatAzureError(errorMsg);
198
- if (azureError) {
199
- return azureError;
200
- }
201
-
202
- const secretsError = formatSecretsError(errorMsg);
203
- if (secretsError) {
204
- return secretsError;
205
- }
206
-
207
- const deploymentError = formatDeploymentError(errorMsg);
208
- if (deploymentError) {
209
- return deploymentError;
210
- }
211
-
212
- const validationError = formatValidationError(errorMsg);
213
- if (validationError) {
214
- return validationError;
261
+ const formatted = tryFormatErrorWithFormatters(errorMsg);
262
+ if (formatted) {
263
+ return formatted;
215
264
  }
216
265
 
217
266
  // Default: return generic error message
218
- messages.push(` ${errorMsg}`);
219
- return messages;
267
+ return [` ${errorMsg}`];
220
268
  }
221
269
 
222
270
  /**
@@ -13,7 +13,7 @@ const fsSync = require('fs');
13
13
  const fs = require('fs').promises;
14
14
  const path = require('path');
15
15
  const handlebars = require('handlebars');
16
- const config = require('../config');
16
+ const config = require('../core/config');
17
17
  const buildCopy = require('./build-copy');
18
18
 
19
19
  // Register commonly used helpers
@@ -230,19 +230,26 @@ function buildNetworksConfig(config) {
230
230
  * @returns {Promise<Object>} Object with passwords array and lookup map
231
231
  * @throws {Error} If required password variables are missing
232
232
  */
233
- async function readDatabasePasswords(envPath, databases, appKey) {
234
- const passwords = {};
235
- const passwordsArray = [];
236
-
237
- // Read .env file
238
- const envVars = {};
233
+ /**
234
+ * Reads and parses .env file
235
+ * @async
236
+ * @function readEnvFile
237
+ * @param {string} envPath - Path to .env file
238
+ * @returns {Promise<Object>} Object with environment variables
239
+ * @throws {Error} If file not found or read fails
240
+ */
241
+ async function readEnvFile(envPath) {
239
242
  if (!fsSync.existsSync(envPath)) {
240
243
  throw new Error(`.env file not found: ${envPath}`);
241
244
  }
242
245
 
243
246
  try {
244
247
  const envContent = await fs.readFile(envPath, 'utf8');
248
+ if (envContent === undefined || envContent === null) {
249
+ throw new Error('Failed to read .env file: file content is empty or undefined');
250
+ }
245
251
  const lines = envContent.split('\n');
252
+ const envVars = {};
246
253
 
247
254
  for (const line of lines) {
248
255
  const trimmed = line.trim();
@@ -257,46 +264,97 @@ async function readDatabasePasswords(envPath, databases, appKey) {
257
264
  envVars[key] = value;
258
265
  }
259
266
  }
267
+
268
+ return envVars;
260
269
  } catch (error) {
261
270
  throw new Error(`Failed to read .env file: ${error.message}`);
262
271
  }
272
+ }
263
273
 
264
- // Process each database
265
- if (databases && databases.length > 0) {
266
- for (let i = 0; i < databases.length; i++) {
267
- const db = databases[i];
268
- const dbName = db.name || appKey;
269
- const passwordKey = `DB_${i}_PASSWORD`;
270
-
271
- if (!(passwordKey in envVars)) {
272
- throw new Error(`Missing required password variable ${passwordKey} in .env file`);
273
- }
274
+ /**
275
+ * Validates and extracts password from environment variables
276
+ * @function extractPassword
277
+ * @param {Object} envVars - Environment variables
278
+ * @param {string} passwordKey - Password key to look up
279
+ * @returns {string} Password value
280
+ * @throws {Error} If password is missing or empty
281
+ */
282
+ function extractPassword(envVars, passwordKey) {
283
+ if (!(passwordKey in envVars)) {
284
+ throw new Error(`Missing required password variable ${passwordKey} in .env file`);
285
+ }
274
286
 
275
- const password = envVars[passwordKey].trim();
276
- if (!password || password.length === 0) {
277
- throw new Error(`Password variable ${passwordKey} is empty in .env file`);
278
- }
287
+ const password = envVars[passwordKey].trim();
288
+ if (!password || password.length === 0) {
289
+ throw new Error(`Password variable ${passwordKey} is empty in .env file`);
290
+ }
279
291
 
280
- passwords[dbName] = password;
281
- passwordsArray.push(password);
282
- }
283
- } else {
284
- // Single database case - use DB_0_PASSWORD or DB_PASSWORD
285
- const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
292
+ return password;
293
+ }
286
294
 
287
- if (!(passwordKey in envVars)) {
288
- throw new Error(`Missing required password variable ${passwordKey} in .env file. Add DB_0_PASSWORD or DB_PASSWORD to your .env file.`);
289
- }
295
+ /**
296
+ * Processes multiple databases
297
+ * @function processMultipleDatabases
298
+ * @param {Array} databases - Array of database configurations
299
+ * @param {Object} envVars - Environment variables
300
+ * @param {string} appKey - Application key
301
+ * @returns {Object} Object with passwords map and array
302
+ */
303
+ function processMultipleDatabases(databases, envVars, appKey) {
304
+ const passwords = {};
305
+ const passwordsArray = [];
290
306
 
291
- const password = envVars[passwordKey].trim();
292
- if (!password || password.length === 0) {
293
- throw new Error(`Password variable ${passwordKey} is empty in .env file`);
294
- }
307
+ for (let i = 0; i < databases.length; i++) {
308
+ const db = databases[i];
309
+ const dbName = db.name || appKey;
310
+ const passwordKey = `DB_${i}_PASSWORD`;
311
+ const password = extractPassword(envVars, passwordKey);
295
312
 
296
- passwords[appKey] = password;
313
+ passwords[dbName] = password;
297
314
  passwordsArray.push(password);
298
315
  }
299
316
 
317
+ return { passwords, passwordsArray };
318
+ }
319
+
320
+ /**
321
+ * Processes single database case
322
+ * @function processSingleDatabase
323
+ * @param {Object} envVars - Environment variables
324
+ * @param {string} appKey - Application key
325
+ * @returns {Object} Object with passwords map and array
326
+ */
327
+ function processSingleDatabase(envVars, appKey) {
328
+ const passwords = {};
329
+ const passwordsArray = [];
330
+
331
+ // Single database case - use DB_0_PASSWORD or DB_PASSWORD
332
+ const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
333
+
334
+ if (!(passwordKey in envVars)) {
335
+ throw new Error(`Missing required password variable ${passwordKey} in .env file. Add DB_0_PASSWORD or DB_PASSWORD to your .env file.`);
336
+ }
337
+
338
+ const password = extractPassword(envVars, passwordKey);
339
+ passwords[appKey] = password;
340
+ passwordsArray.push(password);
341
+
342
+ return { passwords, passwordsArray };
343
+ }
344
+
345
+ async function readDatabasePasswords(envPath, databases, appKey) {
346
+ const envVars = await readEnvFile(envPath);
347
+
348
+ // Process each database
349
+ if (databases && databases.length > 0) {
350
+ const { passwords, passwordsArray } = processMultipleDatabases(databases, envVars, appKey);
351
+ return {
352
+ map: passwords,
353
+ array: passwordsArray
354
+ };
355
+ }
356
+
357
+ const { passwords, passwordsArray } = processSingleDatabase(envVars, appKey);
300
358
  return {
301
359
  map: passwords,
302
360
  array: passwordsArray
@@ -310,48 +368,71 @@ async function readDatabasePasswords(envPath, databases, appKey) {
310
368
  * @param {Object} options - Run options
311
369
  * @returns {Promise<string>} Generated compose content
312
370
  */
371
+ /**
372
+ * Gets developer ID and calculates numeric ID
373
+ * @async
374
+ * @function getDeveloperIdAndNumeric
375
+ * @returns {Promise<Object>} Object with devId and idNum
376
+ */
377
+ async function getDeveloperIdAndNumeric() {
378
+ const devId = await config.getDeveloperId();
379
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
380
+ return { devId, idNum };
381
+ }
382
+
383
+ /**
384
+ * Builds network and container names
385
+ * @function buildNetworkAndContainerNames
386
+ * @param {string} appName - Application name
387
+ * @param {string|number} devId - Developer ID
388
+ * @param {number} idNum - Numeric developer ID
389
+ * @returns {Object} Object with networkName and containerName
390
+ */
391
+ function buildNetworkAndContainerNames(appName, devId, idNum) {
392
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
393
+ const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
394
+ return { networkName, containerName };
395
+ }
396
+
397
+ /**
398
+ * Reads database passwords if needed
399
+ * @async
400
+ * @function readDatabasePasswordsIfNeeded
401
+ * @param {boolean} requiresDatabase - Whether database is required
402
+ * @param {Array} databases - Array of databases
403
+ * @param {string} envFilePath - Environment file path
404
+ * @param {string} appName - Application name
405
+ * @returns {Promise<Object>} Database passwords object
406
+ */
407
+ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFilePath, appName) {
408
+ if (requiresDatabase || databases.length > 0) {
409
+ return await readDatabasePasswords(envFilePath, databases, appName);
410
+ }
411
+ return { map: {}, array: [] };
412
+ }
413
+
313
414
  async function generateDockerCompose(appName, appConfig, options) {
314
415
  const language = appConfig.build?.language || appConfig.language || 'typescript';
315
416
  const template = loadDockerComposeTemplate(language);
316
-
317
- // Use options.port if provided, otherwise use config.port
318
- // (localPort will be handled in buildServiceConfig)
319
417
  const port = options.port || appConfig.port || 3000;
320
418
 
321
- // Get developer ID and network name
322
- const devId = await config.getDeveloperId();
323
- const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
324
- // Dev 0: infra-aifabrix-network (no dev-0 suffix)
325
- // Dev > 0: infra-dev{id}-aifabrix-network
326
- const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
327
- // Dev 0: aifabrix-{appName} (no dev-0 suffix)
328
- // Dev > 0: aifabrix-dev{id}-{appName}
329
- const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
419
+ const { devId, idNum } = await getDeveloperIdAndNumeric();
420
+ const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
330
421
 
331
422
  const serviceConfig = buildServiceConfig(appName, appConfig, port);
332
423
  const volumesConfig = buildVolumesConfig(appName);
333
424
  const networksConfig = buildNetworksConfig(appConfig);
334
425
 
335
- // Get absolute path to .env file for docker-compose (use dev-specific directory)
336
426
  const devDir = buildCopy.getDevDirectory(appName, devId);
337
427
  const envFilePath = path.join(devDir, '.env');
338
- const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
428
+ const envFileAbsolutePath = envFilePath.replace(/\\/g, '/');
339
429
 
340
- // Read database passwords from .env file only if database is required
341
- const databases = networksConfig.databases || [];
342
- const requiresDatabase = serviceConfig.requiresDatabase || false;
343
- let databasePasswords;
344
-
345
- if (requiresDatabase || databases.length > 0) {
346
- // Only read passwords if database is actually required
347
- databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
348
- } else {
349
- // Return empty passwords when database is not required
350
- databasePasswords = {
351
- map: {},
352
- array: []
353
- };
354
- }
430
+ const databasePasswords = await readDatabasePasswordsIfNeeded(
431
+ serviceConfig.requiresDatabase || false,
432
+ networksConfig.databases || [],
433
+ envFilePath,
434
+ appName
435
+ );
355
436
 
356
437
  const templateData = {
357
438
  ...serviceConfig,
@@ -359,7 +440,6 @@ async function generateDockerCompose(appName, appConfig, options) {
359
440
  ...networksConfig,
360
441
  envFile: envFileAbsolutePath,
361
442
  databasePasswords: databasePasswords,
362
- // IMPORTANT: pass numeric devId to templates so (eq devId 0) works correctly
363
443
  devId: idNum,
364
444
  networkName: networkName,
365
445
  containerName: containerName
@@ -107,6 +107,8 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
107
107
  }
108
108
 
109
109
  module.exports = {
110
+ getPathConfig,
111
+ setPathConfig,
110
112
  createPathConfigFunctions
111
113
  };
112
114
 
@@ -8,7 +8,7 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const auditLogger = require('../audit-logger');
11
+ const auditLogger = require('../core/audit-logger');
12
12
  const { parseErrorResponse } = require('./api-error-handler');
13
13
 
14
14
  /**
@@ -146,41 +146,67 @@ function parseTokenResponse(response) {
146
146
  * @param {Object} response - Full API response object
147
147
  * @returns {Error} Validation error with formattedError and errorData attached
148
148
  */
149
- function createValidationError(response) {
150
- const validationError = new Error('Token polling failed: Validation error');
151
-
152
- // Attach formatted error if available (includes detailed validation info with ANSI colors)
149
+ /**
150
+ * Attaches formatted error to validation error
151
+ * @function attachFormattedError
152
+ * @param {Error} validationError - Validation error object
153
+ * @param {Object} response - API response
154
+ */
155
+ function attachFormattedError(validationError, response) {
153
156
  if (response && response.formattedError) {
154
157
  validationError.formattedError = response.formattedError;
155
158
  validationError.message = `Token polling failed:\n${response.formattedError}`;
156
159
  }
160
+ }
161
+
162
+ /**
163
+ * Builds detailed error message from error data
164
+ * @function buildDetailedErrorMessage
165
+ * @param {Object} errorData - Error data object
166
+ * @returns {string} Detailed error message
167
+ */
168
+ function buildDetailedErrorMessage(errorData) {
169
+ const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
170
+ let errorMsg = `Token polling failed: ${detail}`;
171
+
172
+ if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
173
+ errorMsg += '\n\nValidation errors:';
174
+ errorData.errors.forEach(err => {
175
+ const field = err.field || err.path || 'validation';
176
+ const message = err.message || 'Invalid value';
177
+ if (field === 'validation' || field === 'unknown') {
178
+ errorMsg += `\n • ${message}`;
179
+ } else {
180
+ errorMsg += `\n • ${field}: ${message}`;
181
+ }
182
+ });
183
+ }
184
+
185
+ return errorMsg;
186
+ }
157
187
 
158
- // Attach error data for programmatic access
188
+ /**
189
+ * Attaches error data to validation error
190
+ * @function attachErrorData
191
+ * @param {Error} validationError - Validation error object
192
+ * @param {Object} response - API response
193
+ */
194
+ function attachErrorData(validationError, response) {
159
195
  if (response && response.errorData) {
160
196
  validationError.errorData = response.errorData;
161
197
  validationError.errorType = response.errorType || 'validation';
162
198
 
163
- // Build detailed message if formattedError not available
164
199
  if (!validationError.formattedError) {
165
- const errorData = response.errorData;
166
- const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
167
- let errorMsg = `Token polling failed: ${detail}`;
168
- // Add validation errors if available
169
- if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
170
- errorMsg += '\n\nValidation errors:';
171
- errorData.errors.forEach(err => {
172
- const field = err.field || err.path || 'validation';
173
- const message = err.message || 'Invalid value';
174
- if (field === 'validation' || field === 'unknown') {
175
- errorMsg += `\n • ${message}`;
176
- } else {
177
- errorMsg += `\n • ${field}: ${message}`;
178
- }
179
- });
180
- }
181
- validationError.message = errorMsg;
200
+ validationError.message = buildDetailedErrorMessage(response.errorData);
182
201
  }
183
202
  }
203
+ }
204
+
205
+ function createValidationError(response) {
206
+ const validationError = new Error('Token polling failed: Validation error');
207
+
208
+ attachFormattedError(validationError, response);
209
+ attachErrorData(validationError, response);
184
210
 
185
211
  return validationError;
186
212
  }
@@ -239,36 +265,69 @@ async function waitForNextPoll(interval, slowDown) {
239
265
  * @param {Object} response - API response object
240
266
  * @returns {string} Error code or 'Unknown error'
241
267
  */
242
- function extractPollingError(response) {
243
- // Check for structured error data first (from api-error-handler)
244
- if (response.errorData) {
245
- const errorData = response.errorData;
246
- // For validation errors, return the error type so we can handle it specially
247
- if (response.errorType === 'validation') {
248
- return 'validation_error';
249
- }
250
- // Check if error code indicates validation error (e.g., INVALID_TOKEN)
251
- const errorCode = errorData.error || errorData.code || response.error;
252
- if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
253
- return 'validation_error';
254
- }
255
- // Return the error message from structured error
256
- return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
268
+ /**
269
+ * Checks if error code indicates validation error
270
+ * @function isValidationErrorCode
271
+ * @param {string} errorCode - Error code to check
272
+ * @returns {boolean} True if validation error
273
+ */
274
+ function isValidationErrorCode(errorCode) {
275
+ return errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN';
276
+ }
277
+
278
+ /**
279
+ * Extracts error from structured error data
280
+ * @function extractStructuredError
281
+ * @param {Object} response - API response with errorData
282
+ * @returns {string} Error message or code
283
+ */
284
+ function extractStructuredError(response) {
285
+ const errorData = response.errorData;
286
+
287
+ // For validation errors, return the error type so we can handle it specially
288
+ if (response.errorType === 'validation') {
289
+ return 'validation_error';
257
290
  }
258
291
 
259
- // Fallback to original extraction logic
292
+ // Check if error code indicates validation error (e.g., INVALID_TOKEN)
293
+ const errorCode = errorData.error || errorData.code || response.error;
294
+ if (isValidationErrorCode(errorCode)) {
295
+ return 'validation_error';
296
+ }
297
+
298
+ // Return the error message from structured error
299
+ return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
300
+ }
301
+
302
+ /**
303
+ * Extracts error from fallback response structure
304
+ * @function extractFallbackError
305
+ * @param {Object} response - API response
306
+ * @returns {string} Error code
307
+ */
308
+ function extractFallbackError(response) {
260
309
  const apiResponse = response.data || {};
261
310
  const errorData = typeof apiResponse === 'object' ? apiResponse : {};
262
311
  const errorCode = errorData.error || response.error || 'Unknown error';
263
312
 
264
313
  // Check if error code indicates validation error (e.g., INVALID_TOKEN)
265
- if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
314
+ if (isValidationErrorCode(errorCode)) {
266
315
  return 'validation_error';
267
316
  }
268
317
 
269
318
  return errorCode;
270
319
  }
271
320
 
321
+ function extractPollingError(response) {
322
+ // Check for structured error data first (from api-error-handler)
323
+ if (response.errorData) {
324
+ return extractStructuredError(response);
325
+ }
326
+
327
+ // Fallback to original extraction logic
328
+ return extractFallbackError(response);
329
+ }
330
+
272
331
  /**
273
332
  * Handles successful polling response
274
333
  * @param {Object} response - API response object
@@ -439,4 +498,3 @@ module.exports = {
439
498
  refreshDeviceToken,
440
499
  parseTokenResponse
441
500
  };
442
-
@@ -12,7 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
- const config = require('../config');
15
+ const config = require('../core/config');
16
16
 
17
17
  /**
18
18
  * Loads user env-config file if configured