@aifabrix/builder 2.40.2 → 2.42.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.
Files changed (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -59,6 +59,17 @@
59
59
  "type": "string",
60
60
  "description": "Known platform identifier (for known-platform type)",
61
61
  "enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
62
+ },
63
+ "datasourceKeys": {
64
+ "type": "array",
65
+ "description": "Datasource keys to include (validated against platform; omit for all)",
66
+ "items": { "type": "string", "minLength": 1 },
67
+ "minItems": 1
68
+ },
69
+ "entityName": {
70
+ "type": "string",
71
+ "description": "Entity for multi-entity OpenAPI (validated against discover-entities; for openapi-file and openapi-url)",
72
+ "minLength": 1
62
73
  }
63
74
  },
64
75
  "allOf": [
@@ -194,6 +205,11 @@
194
205
  "type": "boolean",
195
206
  "description": "Enable Role-Based Access Control",
196
207
  "default": false
208
+ },
209
+ "debug": {
210
+ "type": "boolean",
211
+ "description": "When true, capture detailed generation steps and save to debug.log (dataplane returns debugLog)",
212
+ "default": false
197
213
  }
198
214
  }
199
215
  },
package/lib/utils/api.js CHANGED
@@ -13,8 +13,8 @@
13
13
  const { parseErrorResponse } = require('./api-error-handler');
14
14
  const auditLogger = require('../core/audit-logger');
15
15
 
16
- /** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
17
- const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
16
+ /** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
17
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
18
18
 
19
19
  /**
20
20
  * Logs API request performance metrics and errors to audit log
@@ -121,10 +121,22 @@ async function handleSuccessResponse(response, url, options, duration) {
121
121
  success: true
122
122
  });
123
123
 
124
+ // 204 No Content or empty body: nothing to parse (avoids "Unexpected end of JSON input")
125
+ if (response.status === 204) {
126
+ return { success: true, data: null, status: response.status };
127
+ }
128
+
124
129
  const contentType = response.headers.get('content-type');
125
130
  if (contentType && contentType.includes('application/json')) {
126
- const data = await response.json();
127
- return { success: true, data, status: response.status };
131
+ try {
132
+ const data = await response.json();
133
+ return { success: true, data, status: response.status };
134
+ } catch (e) {
135
+ if (e instanceof SyntaxError && e.message && e.message.includes('JSON')) {
136
+ return { success: true, data: null, status: response.status };
137
+ }
138
+ throw e;
139
+ }
128
140
  }
129
141
 
130
142
  const text = await response.text();
@@ -229,7 +241,7 @@ async function handleNetworkError(error, url, options, duration) {
229
241
 
230
242
  /**
231
243
  * Make an API call with proper error handling
232
- * Uses a 15s timeout to avoid hanging when the controller is unreachable.
244
+ * Uses a 30s timeout to avoid hanging when the controller is unreachable (Azure cold start can exceed 5s).
233
245
  * @param {string} url - API endpoint URL
234
246
  * @param {Object} options - Fetch options (signal, method, headers, body, etc.)
235
247
  * @returns {Promise<Object>} Response object with success flag
@@ -286,12 +298,28 @@ function extractControllerUrl(url) {
286
298
  }
287
299
 
288
300
  /**
289
- * Make an authenticated API call with bearer token
290
- * Automatically refreshes device token on 401 errors if refresh token is available
301
+ * Set auth header on headers object: Bearer for user token, x-client-token for application token.
302
+ * @param {Object} headers - Headers object to mutate
303
+ * @param {string} token - Token value
304
+ * @param {string} authType - 'bearer' or 'client-token'
305
+ */
306
+ function setAuthHeader(headers, token, authType) {
307
+ if (!token) return;
308
+ if (authType === 'client-token') {
309
+ headers['x-client-token'] = token;
310
+ } else {
311
+ headers['Authorization'] = `Bearer ${token}`;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Make an authenticated API call with user token (Bearer) or application token (x-client-token).
317
+ * Automatically refreshes device token on 401 when user Bearer was used.
291
318
  * @param {string} url - API endpoint URL
292
319
  * @param {Object} options - Fetch options
293
- * @param {string|Object} tokenOrAuthConfig - Bearer token string or authConfig object
294
- * @param {string} [tokenOrAuthConfig.token] - Bearer token (if object)
320
+ * @param {string|Object} tokenOrAuthConfig - User token string (Bearer), or authConfig object with type 'bearer'|'client-token'
321
+ * @param {string} [tokenOrAuthConfig.type] - 'bearer' (user token) or 'client-token' (application token)
322
+ * @param {string} [tokenOrAuthConfig.token] - Token (if object)
295
323
  * @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
296
324
  * @returns {Promise<Object>} Response object
297
325
  */
@@ -299,22 +327,22 @@ function extractControllerUrl(url) {
299
327
  async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
300
328
  const isStringToken = typeof tokenOrAuthConfig === 'string';
301
329
  const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
330
+ const authType = isStringToken ? 'bearer' : tokenOrAuthConfig?.type;
302
331
  const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
303
332
  const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
304
333
  const headers = { ...options.headers };
305
334
  if (!isFormData && !headers['Content-Type']) {
306
335
  headers['Content-Type'] = 'application/json';
307
336
  }
308
- if (token) {
309
- headers['Authorization'] = `Bearer ${token}`;
310
- }
337
+ setAuthHeader(headers, token, authType);
311
338
 
312
339
  const response = await makeApiCall(url, {
313
340
  ...options,
314
341
  headers
315
342
  });
316
343
 
317
- if (!response.success && response.status === 401) {
344
+ // Only attempt device token refresh on 401 when user Bearer token was used (not for client-token)
345
+ if (!response.success && response.status === 401 && authType !== 'client-token') {
318
346
  try {
319
347
  const { forceRefreshDeviceToken } = require('./token-manager');
320
348
  const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
@@ -257,7 +257,22 @@ async function attemptGetAuthenticationToken(normalizedControllerUrl, config, at
257
257
  return { token, finalControllerUrl, lastError };
258
258
  }
259
259
 
260
- async function checkAuthentication(controllerUrl, _environment) {
260
+ /**
261
+ * When auth fails and throwOnFailure is true, throws an error with controllerUrl so caller can run login and retry.
262
+ * @param {string|null} controllerUrl - Controller URL to attach to the thrown error
263
+ * @param {Error|null} lastError - Last error from token attempt
264
+ * @param {string[]} attemptedUrls - URLs that were tried
265
+ * @throws {Error} Error with .controllerUrl and .authFailure set
266
+ */
267
+ function throwAuthFailureError(controllerUrl, lastError, attemptedUrls) {
268
+ const message = lastError ? lastError.message : 'No valid authentication found';
269
+ const err = new Error(message);
270
+ err.controllerUrl = controllerUrl || (attemptedUrls && attemptedUrls[0]) || null;
271
+ err.authFailure = true;
272
+ throw err;
273
+ }
274
+
275
+ async function checkAuthentication(controllerUrl, _environment, options = {}) {
261
276
  try {
262
277
  const config = await getConfig();
263
278
  const normalizedControllerUrl = normalizeControllerUrlIfProvided(controllerUrl);
@@ -269,8 +284,12 @@ async function checkAuthentication(controllerUrl, _environment) {
269
284
  attemptedUrls
270
285
  );
271
286
 
272
- // If no token found, display error with attempted URLs
273
- validateAuthenticationResult(token, finalControllerUrl, lastError, controllerUrl, attemptedUrls);
287
+ if (!token || !finalControllerUrl) {
288
+ if (options.throwOnFailure) {
289
+ throwAuthFailureError(controllerUrl || finalControllerUrl, lastError, attemptedUrls);
290
+ }
291
+ validateAuthenticationResult(token, finalControllerUrl, lastError, controllerUrl, attemptedUrls);
292
+ }
274
293
 
275
294
  return {
276
295
  apiUrl: finalControllerUrl,
@@ -278,6 +297,9 @@ async function checkAuthentication(controllerUrl, _environment) {
278
297
  controllerUrl: finalControllerUrl
279
298
  };
280
299
  } catch (error) {
300
+ if (error.authFailure) {
301
+ throw error;
302
+ }
281
303
  displayAuthenticationError(error, { controllerUrl: controllerUrl });
282
304
  }
283
305
  }
@@ -25,11 +25,12 @@ function createBearerTokenHeaders(token) {
25
25
  }
26
26
 
27
27
  /**
28
- * Creates authentication headers for Client Credentials flow (legacy support)
28
+ * Creates authentication headers for the token-issuing endpoint only (e.g. POST /api/v1/auth/token).
29
+ * Do not use for Controller or Dataplane app endpoints—those require Bearer token (use createBearerTokenHeaders).
29
30
  *
30
31
  * @param {string} clientId - Application client ID
31
32
  * @param {string} clientSecret - Application client secret
32
- * @returns {Object} Headers object with authentication
33
+ * @returns {Object} Headers object with x-client-id and x-client-secret
33
34
  * @throws {Error} If credentials are missing
34
35
  */
35
36
  function createClientCredentialsHeaders(clientId, clientSecret) {
@@ -43,14 +44,14 @@ function createClientCredentialsHeaders(clientId, clientSecret) {
43
44
  }
44
45
 
45
46
  /**
46
- * Creates authentication headers based on auth configuration
47
- * Supports both Bearer token and client credentials authentication
47
+ * Creates authentication headers based on auth configuration.
48
+ * For app endpoints use type 'bearer' only. Use 'client-credentials' only when calling the token-issuing endpoint (e.g. /api/v1/auth/token).
48
49
  *
49
50
  * @param {Object} authConfig - Authentication configuration
50
- * @param {string} authConfig.type - Auth type: 'bearer' or 'client-credentials'
51
+ * @param {string} authConfig.type - Auth type: 'bearer' (for app endpoints) or 'client-credentials' (token endpoint only)
51
52
  * @param {string} [authConfig.token] - Bearer token (for type 'bearer')
52
- * @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials')
53
- * @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials')
53
+ * @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials', token endpoint only)
54
+ * @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials', token endpoint only)
54
55
  * @returns {Object} Headers object with authentication
55
56
  * @throws {Error} If auth config is invalid
56
57
  */
@@ -106,6 +106,10 @@ function isDockerPermissionDeniedError(errorMsg) {
106
106
  */
107
107
  function formatDockerError(errorMsg) {
108
108
  if (isDockerImageNotFoundError(errorMsg)) {
109
+ // Preserve custom hint for template apps (keycloak, miso-controller) from up-miso/up-dataplane
110
+ if (errorMsg.includes('use --image') || errorMsg.includes('Pull the image')) {
111
+ return errorMsg.split('\n').map(line => (line.trim() ? ` ${line.trim()}` : ''));
112
+ }
109
113
  return [
110
114
  ' Docker image not found.',
111
115
  ' Run: aifabrix build <app> first'
@@ -326,6 +330,21 @@ function logOfflinePathWhenType(appPath, options) {
326
330
  logger.log(chalk.gray(`Using: ${displayPath}`));
327
331
  }
328
332
 
333
+ /**
334
+ * Returns true if the error is likely due to authentication failure (e.g. 401, token expired, login required).
335
+ * Used to show "Run: aifabrix login" for commands that require Controller auth (e.g. up-dataplane).
336
+ * @param {Error} error - The error that occurred
337
+ * @returns {boolean} True if the error appears to be auth-related
338
+ */
339
+ function isAuthenticationError(error) {
340
+ if (!error) return false;
341
+ if (error.authFailure === true) return true;
342
+ const msg = (error.message || '').toLowerCase();
343
+ const formatted = (typeof error.formatted === 'string' ? error.formatted : '').toLowerCase();
344
+ const combined = `${msg} ${formatted}`;
345
+ return /401|unauthorized|authentication|token expired|login required|aifabrix login|no authentication|device token|refresh token/.test(combined);
346
+ }
347
+
329
348
  /**
330
349
  * Handles command errors with user-friendly messages
331
350
  * @param {Error} error - The error that occurred
@@ -375,6 +394,7 @@ async function appendWizardError(appKey, error) {
375
394
  module.exports = {
376
395
  validateCommand,
377
396
  handleCommandError,
397
+ isAuthenticationError,
378
398
  appendWizardError,
379
399
  logOfflinePathWhenType
380
400
  };
@@ -18,58 +18,12 @@ const buildCopy = require('./build-copy');
18
18
  const { formatMissingDbPasswordError } = require('./error-formatter');
19
19
  const { getContainerPort } = require('./port-resolver');
20
20
  const { parseImageOverride } = require('./parse-image-ref');
21
+ const { registerComposeHelpers } = require('./compose-handlebars-helpers');
22
+ const { isVectorDatabaseName } = require('./compose-vector-helper');
23
+ const paths = require('./paths');
24
+ const { getInfraDirName } = require('../infrastructure/helpers');
21
25
 
22
- // Register commonly used helpers
23
- handlebars.registerHelper('eq', (a, b) => a === b);
24
-
25
- // Register Handlebars helper for quoting PostgreSQL identifiers
26
- // PostgreSQL requires identifiers with hyphens or special characters to be quoted
27
- handlebars.registerHelper('pgQuote', (identifier) => {
28
- if (!identifier) {
29
- return '';
30
- }
31
- // Always quote identifiers to handle hyphens and special characters
32
- // Return SafeString to prevent HTML escaping
33
- return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
34
- });
35
-
36
- // Helper to generate quoted PostgreSQL user name from database name
37
- // User names must use underscores (not hyphens) for PostgreSQL compatibility
38
- handlebars.registerHelper('pgUser', (dbName) => {
39
- if (!dbName) {
40
- return '';
41
- }
42
- // Replace hyphens with underscores in user name (database names can have hyphens, but user names should not)
43
- const userName = `${String(dbName).replace(/-/g, '_')}_user`;
44
- // Return SafeString to prevent HTML escaping
45
- return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
46
- });
47
-
48
- // Helper to generate old user name format (for migration - drops old users with hyphens)
49
- // This is used to drop legacy users that were created with hyphens before the fix
50
- // Returns unquoted name (quotes should be added in template where needed)
51
- handlebars.registerHelper('pgUserOld', (dbName) => {
52
- if (!dbName) {
53
- return '';
54
- }
55
- // Old format: database name + _user (preserving hyphens)
56
- const userName = `${String(dbName)}_user`;
57
- // Return unquoted name - template will add quotes where needed
58
- return new handlebars.SafeString(userName);
59
- });
60
-
61
- // Helper to generate unquoted PostgreSQL user name (for SQL WHERE clauses)
62
- // Returns the user name without quotes for use in SQL queries
63
- handlebars.registerHelper('pgUserName', (dbName) => {
64
- if (!dbName) {
65
- return '';
66
- }
67
- // Replace hyphens with underscores in user name
68
- const userName = `${String(dbName).replace(/-/g, '_')}_user`;
69
- // Return unquoted name for SQL queries
70
- return new handlebars.SafeString(userName);
71
- });
72
-
26
+ registerComposeHelpers();
73
27
  /**
74
28
  * Loads and compiles Docker Compose template
75
29
  * @param {string} language - Language type
@@ -95,7 +49,6 @@ function loadDockerComposeTemplate(language) {
95
49
  const templateContent = fsSync.readFileSync(templatePath, 'utf8');
96
50
  return handlebars.compile(templateContent);
97
51
  }
98
-
99
52
  /**
100
53
  * Extracts image name from configuration (same logic as build.js)
101
54
  * @param {Object} config - Application configuration
@@ -112,7 +65,6 @@ function getImageName(config, appName) {
112
65
  }
113
66
  return appName;
114
67
  }
115
-
116
68
  /**
117
69
  * Builds app configuration section
118
70
  * @param {string} appName - Application name
@@ -125,7 +77,6 @@ function buildAppConfig(appName, config) {
125
77
  name: config.displayName || appName
126
78
  };
127
79
  }
128
-
129
80
  /**
130
81
  * Builds image configuration section
131
82
  * @param {Object} config - Application configuration
@@ -255,9 +206,6 @@ function buildServiceConfig(appName, config, port, devId, imageOverride) {
255
206
  port: containerPortValue, // Container port (for health check and template)
256
207
  containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
257
208
  hostPort: hostPort, // Host port (options.port if provided, else config.port)
258
- build: {
259
- localPort: config.build?.localPort || null // Only used for .env file PORT variable, not for Docker Compose
260
- },
261
209
  healthCheck: buildHealthCheckConfig(config),
262
210
  traefik: buildTraefikConfig(config, devId),
263
211
  ...buildRequiresConfig(config)
@@ -276,7 +224,7 @@ function buildVolumesConfig(appName) {
276
224
  /**
277
225
  * Builds networks configuration for template data
278
226
  * @param {Object} config - Application configuration
279
- * @returns {Object} Networks configuration
227
+ * @returns {Object} Networks configuration with databases array
280
228
  */
281
229
  function buildNetworksConfig(config) {
282
230
  return { databases: config.requires?.databases || config.databases || [] };
@@ -445,6 +393,52 @@ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFil
445
393
  return { map: {}, array: [] };
446
394
  }
447
395
 
396
+ /**
397
+ * Resolves image override from options (--image, --tag, or null).
398
+ * @param {Object} options - Run options
399
+ * @param {Object} appConfig - Application configuration
400
+ * @param {string} appName - Application name
401
+ * @returns {string|null} Full image reference or null
402
+ */
403
+ function resolveImageOverride(options, appConfig, appName) {
404
+ if (options.image) return options.image;
405
+ if (options.imageOverride) return options.imageOverride;
406
+ if (options.tag) return `${getImageName(appConfig, appName)}:${options.tag}`;
407
+ return null;
408
+ }
409
+
410
+ /**
411
+ * Resolves Miso environment from options (tst, pro, or dev).
412
+ * @param {Object} options - Run options
413
+ * @returns {string} 'dev' | 'tst' | 'pro'
414
+ */
415
+ function resolveMisoEnvironment(options) {
416
+ const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
417
+ return (env === 'tst' || env === 'pro') ? env : 'dev';
418
+ }
419
+
420
+ /**
421
+ * Resolves dev mount path from options.
422
+ * @param {Object} options - Run options
423
+ * @returns {string|null} Trimmed path or null
424
+ */
425
+ function resolveDevMountPath(options) {
426
+ return (options.devMountPath && typeof options.devMountPath === 'string') ? options.devMountPath.trim() : null;
427
+ }
428
+
429
+ /**
430
+ * Resolves env file path from options or default dev dir (forward slashes).
431
+ * @param {Object} options - Run options
432
+ * @param {string} devDir - Default dev directory
433
+ * @returns {string} Absolute env file path
434
+ */
435
+ function resolveEnvFilePath(options, devDir) {
436
+ const envFilePath = (options.envFilePath && typeof options.envFilePath === 'string')
437
+ ? path.resolve(options.envFilePath)
438
+ : path.join(devDir, '.env');
439
+ return envFilePath.replace(/\\/g, '/');
440
+ }
441
+
448
442
  /**
449
443
  * Generates Docker Compose configuration from template
450
444
  * @async
@@ -458,8 +452,7 @@ async function generateDockerCompose(appName, appConfig, options) {
458
452
  const language = appConfig.build?.language || appConfig.language || 'typescript';
459
453
  const template = loadDockerComposeTemplate(language);
460
454
  const port = options.port || appConfig.port || 3000;
461
- const imageOverride = options.image || options.imageOverride ||
462
- (options.tag ? `${getImageName(appConfig, appName)}:${options.tag}` : null);
455
+ const imageOverride = resolveImageOverride(options, appConfig, appName);
463
456
  const { devId, idNum } = await getDeveloperIdAndNumeric();
464
457
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
465
458
  const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
@@ -467,33 +460,41 @@ async function generateDockerCompose(appName, appConfig, options) {
467
460
  const networksConfig = buildNetworksConfig(appConfig);
468
461
 
469
462
  const devDir = buildCopy.getDevDirectory(appName, devId);
470
- const envFilePath = path.join(devDir, '.env');
471
- const envFileAbsolutePath = envFilePath.replace(/\\/g, '/');
463
+ const envFileAbsolutePath = resolveEnvFilePath(options, devDir);
464
+ const dbInitEnvFileAbsolutePath = (options.dbInitEnvFilePath && typeof options.dbInitEnvFilePath === 'string')
465
+ ? path.resolve(options.dbInitEnvFilePath).replace(/\\/g, '/')
466
+ : null;
472
467
 
473
468
  const databasePasswords = await readDatabasePasswordsIfNeeded(
474
469
  serviceConfig.requiresDatabase || false,
475
470
  networksConfig.databases || [],
476
- envFilePath,
471
+ envFileAbsolutePath,
477
472
  appName
478
473
  );
479
-
480
- const templateData = {
474
+ const devMountPath = resolveDevMountPath(options);
475
+ const reloadStartRaw = appConfig.build?.reloadStart;
476
+ const reloadStartCommand =
477
+ devMountPath && typeof reloadStartRaw === 'string' && reloadStartRaw.trim().length > 0
478
+ ? reloadStartRaw.trim()
479
+ : null;
480
+
481
+ const infraPgpassPath = path.join(paths.getAifabrixHome(), getInfraDirName(devId), 'pgpass');
482
+ const useInfraPgpass = serviceConfig.requiresDatabase && fsSync.existsSync(infraPgpassPath);
483
+ return template({
481
484
  ...serviceConfig,
482
485
  ...volumesConfig,
483
486
  ...networksConfig,
484
487
  envFile: envFileAbsolutePath,
488
+ dbInitEnvFile: dbInitEnvFileAbsolutePath,
485
489
  databasePasswords,
486
490
  devId: idNum,
487
491
  networkName,
488
- containerName
489
- };
490
- return template(templateData);
492
+ containerName,
493
+ misoEnvironment: resolveMisoEnvironment(options),
494
+ devMountPath,
495
+ reloadStartCommand,
496
+ infraPgpassPath: useInfraPgpass ? infraPgpassPath : null,
497
+ useInfraPgpass: !!useInfraPgpass
498
+ });
491
499
  }
492
-
493
- module.exports = {
494
- generateDockerCompose,
495
- getImageName,
496
- derivePathFromPattern,
497
- buildTraefikConfig,
498
- buildDevUsername
499
- };
500
+ module.exports = { generateDockerCompose, getImageName, derivePathFromPattern, buildTraefikConfig, buildDevUsername, isVectorDatabaseName };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Handlebars helpers for Docker Compose templates.
3
+ * @fileoverview Compose template helpers (pgQuote, pgUser, isVectorDatabase, etc.)
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const handlebars = require('handlebars');
9
+ const { isVectorDatabaseName } = require('./compose-vector-helper');
10
+
11
+ /**
12
+ * Registers Handlebars helpers used by Docker Compose templates.
13
+ */
14
+ function registerComposeHelpers() {
15
+ handlebars.registerHelper('eq', (a, b) => a === b);
16
+
17
+ handlebars.registerHelper('pgQuote', (identifier) => {
18
+ if (!identifier) return '';
19
+ return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
20
+ });
21
+
22
+ handlebars.registerHelper('pgUser', (dbName) => {
23
+ if (!dbName) return '';
24
+ const userName = `${String(dbName).replace(/-/g, '_')}_user`;
25
+ return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
26
+ });
27
+
28
+ handlebars.registerHelper('pgUserOld', (dbName) => {
29
+ if (!dbName) return '';
30
+ const userName = `${String(dbName)}_user`;
31
+ return new handlebars.SafeString(userName);
32
+ });
33
+
34
+ handlebars.registerHelper('pgUserName', (dbName) => {
35
+ if (!dbName) return '';
36
+ const userName = `${String(dbName).replace(/-/g, '_')}_user`;
37
+ return new handlebars.SafeString(userName);
38
+ });
39
+
40
+ handlebars.registerHelper('isVectorDatabase', (name) => isVectorDatabaseName(name));
41
+
42
+ /** Returns list of extension names for this database (config extensions + vector if name ends with "vector"). */
43
+ handlebars.registerHelper('extensionsForDb', (db) => {
44
+ if (!db) return [];
45
+ const explicit = Array.isArray(db.extensions) ? db.extensions : [];
46
+ const list = [...explicit];
47
+ if (isVectorDatabaseName(db.name) && !list.includes('vector')) {
48
+ list.push('vector');
49
+ }
50
+ return list;
51
+ });
52
+ }
53
+
54
+ module.exports = { registerComposeHelpers };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Vector database name predicate for Docker Compose generation.
3
+ * Used so db-init can run CREATE EXTENSION vector on vector-store databases.
4
+ * @fileoverview Vector database name helper for compose-generator
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ /**
10
+ * Returns true when the database name ends with "vector" (case-insensitive).
11
+ * @param {string} name - Database name
12
+ * @returns {boolean}
13
+ */
14
+ function isVectorDatabaseName(name) {
15
+ return name !== null && name !== undefined && String(name).toLowerCase().endsWith('vector');
16
+ }
17
+
18
+ module.exports = { isVectorDatabaseName };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Config format preference utilities (json/yaml)
3
+ *
4
+ * @fileoverview Format preference get/set for config
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ /**
10
+ * Validate and normalize format (json or yaml)
11
+ * @param {*} format - Format value
12
+ * @returns {string} Normalized format ('json' or 'yaml')
13
+ * @throws {Error} If format is invalid
14
+ */
15
+ function validateAndNormalizeFormat(format) {
16
+ if (!format || typeof format !== 'string') {
17
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
18
+ }
19
+ const normalized = format.trim().toLowerCase();
20
+ if (normalized !== 'json' && normalized !== 'yaml') {
21
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ /**
27
+ * Create format preference functions
28
+ * @param {Function} getConfigFn - Async function to get config
29
+ * @param {Function} saveConfigFn - Async function to save config
30
+ * @returns {{ getFormat: Function, setFormat: Function, validateAndNormalizeFormat: Function }}
31
+ */
32
+ function createFormatFunctions(getConfigFn, saveConfigFn) {
33
+ return {
34
+ async getFormat() {
35
+ const config = await getConfigFn();
36
+ const raw = config.format;
37
+ if (!raw || typeof raw !== 'string') return null;
38
+ const normalized = raw.trim().toLowerCase();
39
+ return normalized === 'json' || normalized === 'yaml' ? normalized : null;
40
+ },
41
+ async setFormat(format) {
42
+ const normalized = validateAndNormalizeFormat(format);
43
+ const config = await getConfigFn();
44
+ config.format = normalized;
45
+ await saveConfigFn(config);
46
+ },
47
+ validateAndNormalizeFormat
48
+ };
49
+ }
50
+
51
+ module.exports = { createFormatFunctions, validateAndNormalizeFormat };
@@ -14,6 +14,7 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const yaml = require('js-yaml');
17
+ const YAML = require('yaml');
17
18
 
18
19
  const YAML_EXTENSIONS = ['.yaml', '.yml'];
19
20
  const JSON_EXTENSIONS = ['.json'];
@@ -144,11 +145,46 @@ function writeConfigFile(filePath, object, format) {
144
145
  fs.writeFileSync(filePath, content, 'utf8');
145
146
  }
146
147
 
148
+ /**
149
+ * Writes application config YAML by updating only repaired keys in the original content,
150
+ * so comments and formatting on other keys are preserved. Use for repair flows that
151
+ * only change externalIntegration and/or app.key.
152
+ *
153
+ * @param {string} filePath - Absolute path to the YAML file
154
+ * @param {string} originalContent - Original file content (with comments)
155
+ * @param {Object} repairedVariables - Repaired config object; only externalIntegration and app are written
156
+ * @throws {Error} If parsing or write fails
157
+ */
158
+ function writeYamlPreservingComments(filePath, originalContent, repairedVariables) {
159
+ if (!filePath || typeof filePath !== 'string') {
160
+ throw new Error('writeYamlPreservingComments requires a non-empty file path');
161
+ }
162
+ if (typeof originalContent !== 'string') {
163
+ throw new Error('writeYamlPreservingComments requires original content string');
164
+ }
165
+ const doc = YAML.parseDocument(originalContent);
166
+ if (doc.errors && doc.errors.length > 0) {
167
+ const first = doc.errors[0];
168
+ throw new Error(`Invalid YAML: ${first.message}`);
169
+ }
170
+ if (!doc.contents) {
171
+ doc.contents = doc.createNode({});
172
+ }
173
+ if (repairedVariables.externalIntegration !== undefined) {
174
+ doc.set('externalIntegration', doc.createNode(repairedVariables.externalIntegration));
175
+ }
176
+ if (repairedVariables.app !== undefined) {
177
+ doc.set('app', doc.createNode(repairedVariables.app));
178
+ }
179
+ fs.writeFileSync(filePath, String(doc), 'utf8');
180
+ }
181
+
147
182
  module.exports = {
148
183
  yamlToJson,
149
184
  jsonToYaml,
150
185
  loadConfigFile,
151
186
  writeConfigFile,
187
+ writeYamlPreservingComments,
152
188
  isYamlPath,
153
189
  isJsonPath
154
190
  };