@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
@@ -95,9 +95,19 @@ function handleDockerClose(code, ctx) {
95
95
  }
96
96
 
97
97
  function runDockerBuildProcess(buildOpts) {
98
- const { imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject } = buildOpts;
99
- const dockerProcess = spawn('docker', ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath, contextPath], {
100
- shell: process.platform === 'win32'
98
+ const { imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject, env = {}, buildArgs = {} } = buildOpts;
99
+ const spawnEnv = { ...process.env, ...env };
100
+ const args = ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath];
101
+ // Pass NPM_TOKEN/PYPI_TOKEN etc. so private registry auth works during RUN npm install / pip install
102
+ for (const [key, value] of Object.entries(buildArgs)) {
103
+ if (value !== null && value !== undefined && String(value).length > 0) {
104
+ args.push('--build-arg', `${key}=${String(value)}`);
105
+ }
106
+ }
107
+ args.push(contextPath);
108
+ const dockerProcess = spawn('docker', args, {
109
+ shell: process.platform === 'win32',
110
+ env: spawnEnv
101
111
  });
102
112
  let stdoutBuffer = '';
103
113
  let stderrBuffer = '';
@@ -138,13 +148,15 @@ function runDockerBuildProcess(buildOpts) {
138
148
  * @param {string} dockerfilePath - Path to Dockerfile
139
149
  * @param {string} contextPath - Build context path
140
150
  * @param {string} tag - Image tag
151
+ * @param {Object} [buildArgs={}] - Optional build args (e.g. NPM_TOKEN, PYPI_TOKEN) for private registries
141
152
  * @returns {Promise<void>} Resolves when build completes
142
153
  * @throws {Error} If build fails
143
154
  */
144
- async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
155
+ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {}) {
145
156
  const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
146
157
  const fsSync = require('fs');
147
158
  const path = require('path');
159
+ const { getRemoteDockerEnv } = require('./remote-docker-env');
148
160
  dockerfilePath = path.resolve(dockerfilePath);
149
161
  contextPath = path.resolve(contextPath);
150
162
 
@@ -162,8 +174,21 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
162
174
  }
163
175
  }
164
176
 
177
+ const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
178
+ const remoteEnv = isTest ? {} : await getRemoteDockerEnv();
179
+ const resolvedBuildArgs = buildArgs && typeof buildArgs === 'object' ? buildArgs : {};
165
180
  return new Promise((resolve, reject) => {
166
- runDockerBuildProcess({ imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject });
181
+ runDockerBuildProcess({
182
+ imageName,
183
+ tag,
184
+ dockerfilePath,
185
+ contextPath,
186
+ spinner,
187
+ resolve,
188
+ reject,
189
+ env: remoteEnv,
190
+ buildArgs: resolvedBuildArgs
191
+ });
167
192
  });
168
193
  }
169
194
 
@@ -179,14 +204,18 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
179
204
  * @throws {Error} If build fails
180
205
  */
181
206
  async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
182
- await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
207
+ const buildArgs = (options && options.buildArgs) || {};
208
+ await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs);
183
209
 
184
210
  // Tag image if additional tag provided
185
211
  if (options && options.tag && options.tag !== 'latest') {
186
212
  const { promisify } = require('util');
187
213
  const { exec } = require('child_process');
188
214
  const run = promisify(exec);
189
- await run(`docker tag ${imageName}:${tag} ${imageName}:latest`);
215
+ const { getRemoteDockerEnv } = require('./remote-docker-env');
216
+ const remoteEnv = await getRemoteDockerEnv();
217
+ const env = { ...process.env, ...remoteEnv };
218
+ await run(`docker tag ${imageName}:${tag} ${imageName}:latest`, { env });
190
219
  }
191
220
  }
192
221
 
@@ -215,7 +244,10 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
215
244
  const { promisify } = require('util');
216
245
  const { exec } = require('child_process');
217
246
  const run = promisify(exec);
218
- await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
247
+ const { getRemoteDockerEnv } = require('./remote-docker-env');
248
+ const remoteEnv = await getRemoteDockerEnv();
249
+ const env = { ...process.env, ...remoteEnv };
250
+ await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`, { env });
219
251
  logger.log(chalk.green(`āœ“ Tagged image: ${imageName}:${tag}`));
220
252
  } catch (err) {
221
253
  logger.log(chalk.yellow(`āš ļø Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const fs = require('fs');
12
+ const fsp = require('fs').promises;
12
13
  const path = require('path');
13
14
  const yaml = require('js-yaml');
14
15
  const chalk = require('chalk');
@@ -48,6 +49,22 @@ function readDeveloperIdFromConfig(config) {
48
49
  return null;
49
50
  }
50
51
 
52
+ /**
53
+ * Substitute /mnt/data with local mount path for local .env and ensure mount dir exists on disk.
54
+ * Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
55
+ * @param {string} content - Env file content
56
+ * @param {string} outputPath - Resolved path of the .env file being written
57
+ * @returns {string} Content with /mnt/data replaced by path to mount directory
58
+ */
59
+ function substituteMntDataForLocal(content, outputPath) {
60
+ const outputDir = path.dirname(outputPath);
61
+ const localMountPath = path.resolve(outputDir, 'mount');
62
+ if (!fs.existsSync(localMountPath)) {
63
+ fs.mkdirSync(localMountPath, { recursive: true });
64
+ }
65
+ return content.replace(/\/mnt\/data/g, localMountPath);
66
+ }
67
+
51
68
  /**
52
69
  * Resolve output path for env file
53
70
  * @param {string} rawOutputPath - Raw output path from application config
@@ -72,6 +89,45 @@ function resolveEnvOutputPath(rawOutputPath, variablesPath) {
72
89
  return outputPath;
73
90
  }
74
91
 
92
+ /**
93
+ * Writes .env to envOutputPath for reload path: merge run .env into existing file.
94
+ * @async
95
+ * @param {string} outputPath - Resolved output path
96
+ * @param {string} runEnvPath - Path to .env.run
97
+ */
98
+ async function writeEnvOutputForReload(outputPath, runEnvPath) {
99
+ const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
100
+ const runContent = await fsp.readFile(runEnvPath, 'utf8');
101
+ const runMap = parseEnvContentToMap(runContent);
102
+ let toWrite = runContent;
103
+ if (fs.existsSync(outputPath)) {
104
+ const existingContent = await fsp.readFile(outputPath, 'utf8');
105
+ toWrite = mergeEnvMapIntoContent(existingContent, runMap);
106
+ }
107
+ await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
108
+ logger.log(chalk.green(`āœ“ Wrote .env to envOutputPath (same as container, for --reload): ${outputPath}`));
109
+ }
110
+
111
+ /**
112
+ * Writes local .env to envOutputPath (no reload).
113
+ * @async
114
+ * @param {string} appName - Application name
115
+ * @param {string} outputPath - Resolved output path
116
+ */
117
+ async function writeEnvOutputForLocal(appName, outputPath) {
118
+ const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
119
+ let localContent = await generateEnvContent(appName, null, 'local', false);
120
+ localContent = substituteMntDataForLocal(localContent, outputPath);
121
+ let toWrite = localContent;
122
+ if (fs.existsSync(outputPath)) {
123
+ const existingContent = await fsp.readFile(outputPath, 'utf8');
124
+ const localMap = parseEnvContentToMap(localContent);
125
+ toWrite = mergeEnvMapIntoContent(existingContent, localMap);
126
+ }
127
+ await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
128
+ logger.log(chalk.green(`āœ“ Wrote .env to envOutputPath (localPort): ${outputPath}`));
129
+ }
130
+
75
131
  /**
76
132
  * Calculate developer-specific app port
77
133
  * @param {number} baseAppPort - Base application port
@@ -180,6 +236,44 @@ async function patchEnvContentForLocal(envContent, variables) {
180
236
  return envContent;
181
237
  }
182
238
 
239
+ /**
240
+ * Write regenerated local .env to output path (merge with existing if present).
241
+ * @async
242
+ * @param {string} outputPath - Resolved output path
243
+ * @param {string} appName - Application name
244
+ * @param {string} [secretsPath] - Path to secrets file (optional)
245
+ * @param {string} envOutputPathLabel - Label for log message (e.g. variables.build.envOutputPath)
246
+ */
247
+ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel) {
248
+ const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
249
+ let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
250
+ localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
251
+ let toWrite = localEnvContent;
252
+ if (fs.existsSync(outputPath)) {
253
+ const existingContent = fs.readFileSync(outputPath, 'utf8');
254
+ const localMap = parseEnvContentToMap(localEnvContent);
255
+ toWrite = mergeEnvMapIntoContent(existingContent, localMap);
256
+ }
257
+ fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
258
+ logger.log(chalk.green(`āœ“ Generated local .env at: ${envOutputPathLabel}`));
259
+ }
260
+
261
+ /**
262
+ * Write patched .env to output path (fallback when appName not provided).
263
+ * @async
264
+ * @param {string} envPath - Path to generated .env file
265
+ * @param {string} outputPath - Resolved output path
266
+ * @param {Object} variables - Loaded variables config
267
+ * @param {string} envOutputPathLabel - Label for log message
268
+ */
269
+ async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
270
+ const envContent = fs.readFileSync(envPath, 'utf8');
271
+ let patchedContent = await patchEnvContentForLocal(envContent, variables);
272
+ patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
273
+ fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
274
+ logger.log(chalk.green(`āœ“ Copied .env to: ${envOutputPathLabel}`));
275
+ }
276
+
183
277
  /**
184
278
  * Process and optionally copy env file to envOutputPath if configured
185
279
  * Regenerates .env file with env=local for local development (apps/.env)
@@ -191,7 +285,7 @@ async function patchEnvContentForLocal(envContent, variables) {
191
285
  * @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
192
286
  */
193
287
  async function processEnvVariables(envPath, variablesPath, appName, secretsPath) {
194
- if (!fs.existsSync(variablesPath)) {
288
+ if (!variablesPath || !fs.existsSync(variablesPath)) {
195
289
  return;
196
290
  }
197
291
  const variablesContent = fs.readFileSync(variablesPath, 'utf8');
@@ -200,29 +294,25 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
200
294
  return;
201
295
  }
202
296
 
203
- // Resolve output path
204
297
  const outputPath = resolveEnvOutputPath(variables.build.envOutputPath, variablesPath);
205
298
  const outputDir = path.dirname(outputPath);
206
299
  if (!fs.existsSync(outputDir)) {
207
300
  fs.mkdirSync(outputDir, { recursive: true });
208
301
  }
209
302
 
210
- // Regenerate .env file with env=local instead of copying docker-generated file
303
+ const label = variables.build.envOutputPath;
211
304
  if (appName) {
212
- const { generateEnvContent } = require('../core/secrets');
213
- const localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
214
- fs.writeFileSync(outputPath, localEnvContent, { mode: 0o600 });
215
- logger.log(chalk.green(`āœ“ Generated local .env at: ${variables.build.envOutputPath}`));
305
+ await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label);
216
306
  } else {
217
- // Fallback: if appName not provided, use old patching approach
218
- const envContent = fs.readFileSync(envPath, 'utf8');
219
- const patchedContent = await patchEnvContentForLocal(envContent, variables);
220
- fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
221
- logger.log(chalk.green(`āœ“ Copied .env to: ${variables.build.envOutputPath}`));
307
+ await writePatchedEnvToOutputPath(envPath, outputPath, variables, label);
222
308
  }
223
309
  }
224
310
 
225
311
  module.exports = {
226
- processEnvVariables
312
+ processEnvVariables,
313
+ resolveEnvOutputPath,
314
+ substituteMntDataForLocal,
315
+ writeEnvOutputForReload,
316
+ writeEnvOutputForLocal
227
317
  };
228
318
 
@@ -144,6 +144,27 @@ function handlePlainValue(result, key, rawVal, options) {
144
144
  result[key] = val;
145
145
  }
146
146
 
147
+ /** Placeholder in env-config values; replaced with application or default port */
148
+ const PORT_PLACEHOLDER = '${PORT}';
149
+
150
+ /**
151
+ * Replaces ${PORT} in all string values of env object (in-place).
152
+ * Used so env-config.yaml docker/local values resolve correctly (e.g. PORT: ${PORT} -> application port).
153
+ *
154
+ * @param {Object} envObj - Flat key-value object (e.g. merged env-config environments.docker)
155
+ * @param {number} [portNumber=3000] - Port to substitute when value contains ${PORT}
156
+ */
157
+ function resolvePortInEnvValues(envObj, portNumber = 3000) {
158
+ if (!envObj || typeof envObj !== 'object') return;
159
+ const portStr = String(portNumber);
160
+ for (const key of Object.keys(envObj)) {
161
+ const val = envObj[key];
162
+ if (typeof val === 'string' && val.includes(PORT_PLACEHOLDER)) {
163
+ envObj[key] = val.split(PORT_PLACEHOLDER).join(portStr);
164
+ }
165
+ }
166
+ }
167
+
147
168
  /**
148
169
  * Normalize environment variable map by splitting host:port values
149
170
  * @function normalizeEnvVars
@@ -270,30 +291,38 @@ function calculateDockerPublicPorts(result, devIdNum, schemaBaseVars = {}) {
270
291
  /**
271
292
  * Build environment variable map for interpolation based on env-config.yaml
272
293
  * - Supports values like "host:port" by splitting into *_HOST (host) and *_PORT (port)
294
+ * - Resolves ${PORT} in env-config values using options.appPort or default 3000 (so docker/local values are correct)
273
295
  * - Merges overrides from ~/.aifabrix/config.yaml under environments.{env}
274
296
  * - Applies aifabrix-localhost override for local context if configured
275
297
  * - Applies developer-id adjustment to port variables for local context
276
- * - Calculates *_PUBLIC_PORT for docker context (basePort + developer-id * 100)
298
+ * - Calculates *_PUBLIC_PORT for both local and docker context (basePort + developer-id * 100)
277
299
  * @async
278
300
  * @function buildEnvVarMap
279
301
  * @param {'docker'|'local'} context - Environment context
280
302
  * @param {Object} [osModule] - Optional os module (for testing). If not provided, requires 'os'
281
303
  * @param {number|null} [developerId] - Optional developer ID for port adjustment. If not provided, will be fetched from config for local context.
304
+ * @param {Object} [options] - Optional options
305
+ * @param {number} [options.appPort] - Port to use when resolving ${PORT} in env-config values (e.g. from application.yaml)
282
306
  * @returns {Promise<Object>} Map of variables for interpolation
283
307
  */
284
- async function buildEnvVarMap(context, osModule = null, developerId = null) {
308
+ async function buildEnvVarMap(context, osModule = null, developerId = null, options = null) {
285
309
  const baseVars = await loadBaseVars(context);
286
310
  const os = osModule || require('os');
287
311
  const overrideVars = loadOverrideVars(context, os);
288
312
  const localhostOverride = context === 'local' ? getLocalhostOverride(os) : null;
289
313
  const merged = { ...baseVars, ...overrideVars };
314
+ const appPort = (options && options.appPort !== undefined && options.appPort !== null && Number.isFinite(Number(options.appPort)))
315
+ ? Number(options.appPort) : 3000;
316
+ resolvePortInEnvValues(merged, appPort);
290
317
  const result = normalizeEnvVars(merged, context, localhostOverride);
291
318
 
319
+ const devIdNum = await getDeveloperIdNumber(developerId);
292
320
  if (context === 'local') {
293
- const devIdNum = await getDeveloperIdNumber(developerId);
294
321
  applyLocalPortAdjustment(result, devIdNum);
322
+ const schemaCfg = loadSchemaEnvConfig();
323
+ const schemaBaseVars = (schemaCfg && schemaCfg.environments && schemaCfg.environments.local) ? schemaCfg.environments.local : {};
324
+ calculateDockerPublicPorts(result, devIdNum, schemaBaseVars);
295
325
  } else if (context === 'docker') {
296
- const devIdNum = await getDeveloperIdNumber(developerId);
297
326
  const schemaCfg = loadSchemaEnvConfig();
298
327
  const schemaBaseVars = (schemaCfg && schemaCfg.environments && schemaCfg.environments[context]) ? schemaCfg.environments[context] : {};
299
328
  calculateDockerPublicPorts(result, devIdNum, schemaBaseVars);
@@ -304,6 +333,7 @@ async function buildEnvVarMap(context, osModule = null, developerId = null) {
304
333
 
305
334
  module.exports = {
306
335
  buildEnvVarMap,
307
- getDeveloperIdNumber
336
+ getDeveloperIdNumber,
337
+ resolvePortInEnvValues
308
338
  };
309
339
 
@@ -15,7 +15,8 @@ const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
16
 
17
17
  /**
18
- * Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL entries
18
+ * Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and optionally MISO_CONTROLLER_URL.
19
+ * When MISO_CONTROLLER_URL already exists, its value is left unchanged (e.g. http://${MISO_HOST}:${MISO_PORT}).
19
20
  * @async
20
21
  * @param {string} appKey - Application key
21
22
  * @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
@@ -39,7 +40,9 @@ function checkMisoEntries(content) {
39
40
  }
40
41
 
41
42
  /**
42
- * Updates existing MISO entries
43
+ * Updates existing MISO entries.
44
+ * MISO_CONTROLLER_URL is never overwritten when present so that template form
45
+ * (e.g. http://${MISO_HOST}:${MISO_PORT}) or custom URLs are preserved.
43
46
  * @function updateExistingMisoEntries
44
47
  * @param {string} content - File content
45
48
  * @param {string} clientIdKey - Client ID key
@@ -54,9 +57,7 @@ function updateExistingMisoEntries(content, clientIdKey, clientSecretKey, entrie
54
57
  if (entries.hasClientSecret) {
55
58
  content = content.replace(/^MISO_CLIENTSECRET\s*=.*$/m, `MISO_CLIENTSECRET=kv://${clientSecretKey}`);
56
59
  }
57
- if (entries.hasControllerUrl) {
58
- content = content.replace(/^MISO_CONTROLLER_URL\s*=.*$/m, 'MISO_CONTROLLER_URL=http://${MISO_HOST}:${MISO_PORT}');
59
- }
60
+ // Do not change existing MISO_CONTROLLER_URL (preserve template or custom value)
60
61
  return content;
61
62
  }
62
63
 
@@ -79,12 +79,31 @@ function addErrorMessageIfNotGeneric(lines, errorData) {
79
79
  }
80
80
 
81
81
  /**
82
- * Adds authentication guidance
82
+ * Returns true when the error is about Builder Server client certificate (not Controller token).
83
+ * @param {Object} errorData - Error data
84
+ * @returns {boolean}
85
+ */
86
+ function isBuilderServerCertError(errorData) {
87
+ const msg = (errorData.message || errorData.error || errorData.detail || '').toLowerCase();
88
+ return msg.includes('client certificate') || msg.includes('issue-cert') || msg.includes('x-client-cert');
89
+ }
90
+
91
+ /**
92
+ * Adds authentication guidance (Controller token login). Skipped for Builder Server cert errors.
83
93
  * @function addAuthenticationGuidance
84
94
  * @param {string[]} lines - Error message lines
85
95
  * @param {Object} errorData - Error data
86
96
  */
87
97
  function addAuthenticationGuidance(lines, errorData) {
98
+ if (isBuilderServerCertError(errorData)) {
99
+ lines.push(chalk.gray('Use a certificate from: aifabrix dev init --developer-id <id> --server <url> --pin <pin>'));
100
+ if (errorData.correlationId) {
101
+ lines.push('');
102
+ lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
103
+ }
104
+ return;
105
+ }
106
+
88
107
  lines.push(chalk.gray('Your authentication token is invalid or has expired.'));
89
108
  lines.push('');
90
109
  lines.push(chalk.gray('To authenticate, run:'));
@@ -304,4 +323,3 @@ module.exports = {
304
323
  formatNotFoundError,
305
324
  formatGenericError
306
325
  };
307
-
@@ -134,4 +134,3 @@ module.exports = {
134
134
  extractMissingPermissions,
135
135
  extractRequiredPermissions
136
136
  };
137
-
@@ -130,4 +130,3 @@ function formatValidationError(errorData) {
130
130
  module.exports = {
131
131
  formatValidationError
132
132
  };
133
-
@@ -30,42 +30,65 @@ function formatDisplayName(key) {
30
30
  .join(' ');
31
31
  }
32
32
 
33
+ /**
34
+ * Derives suffix from datasource key for filename generation
35
+ * @param {string} key - Datasource key
36
+ * @param {string} systemKey - System key
37
+ * @param {string} entityType - Fallback entity type
38
+ * @returns {string} Suffix segment
39
+ */
40
+ function getDatasourceKeySuffix(key, systemKey, entityType) {
41
+ if (key.startsWith(`${systemKey}-deploy-`)) {
42
+ return key.slice(`${systemKey}-deploy-`.length);
43
+ }
44
+ if (systemKey && key.startsWith(`${systemKey}-`)) {
45
+ return key.slice(systemKey.length + 1);
46
+ }
47
+ if (key) {
48
+ return key;
49
+ }
50
+ return entityType;
51
+ }
52
+
53
+ /**
54
+ * Normalizes a single datasource entry for template use
55
+ * @param {Object} datasource - Datasource object
56
+ * @param {number} index - Index in array
57
+ * @param {string} systemKey - System key for filename generation
58
+ * @param {string} ext - File extension (e.g. '.json', '.yaml')
59
+ * @returns {{entityType: string, displayName: string, fileName: string, datasourceKey: string}} Normalized entry
60
+ */
61
+ function normalizeOneDatasource(datasource, index, systemKey, ext) {
62
+ const entityType = datasource.entityType ||
63
+ datasource.entityKey ||
64
+ datasource.key?.split('-').pop() ||
65
+ `entity${index + 1}`;
66
+ const displayName = datasource.displayName ||
67
+ datasource.name ||
68
+ `Datasource ${index + 1}`;
69
+ const key = datasource.key || '';
70
+ const suffix = getDatasourceKeySuffix(key, systemKey, entityType);
71
+ const datasourceKey = key || (systemKey ? `${systemKey}-${suffix}` : suffix);
72
+ const fileName = datasource.fileName || datasource.file ||
73
+ (systemKey ? `${systemKey}-datasource-${suffix}${ext}` : `${suffix}${ext}`);
74
+ return { entityType, displayName, fileName, datasourceKey };
75
+ }
76
+
33
77
  /**
34
78
  * Normalizes datasource entries for template use
35
79
  * @param {Array} datasources - Datasource objects
36
80
  * @param {string} systemKey - System key for filename generation
37
- * @returns {Array<{entityType: string, displayName: string, fileName: string}>} Normalized entries
81
+ * @param {string} [fileExt='.json'] - File extension for generated filenames (e.g. '.json', '.yaml')
82
+ * @returns {Array<{entityType: string, displayName: string, fileName: string, datasourceKey: string}>} Normalized entries
38
83
  */
39
- function normalizeDatasources(datasources, systemKey) {
84
+ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
40
85
  if (!Array.isArray(datasources)) {
41
86
  return [];
42
87
  }
43
- return datasources.map((datasource, index) => {
44
- const entityType = datasource.entityType ||
45
- datasource.entityKey ||
46
- datasource.key?.split('-').pop() ||
47
- `entity${index + 1}`;
48
- const displayName = datasource.displayName ||
49
- datasource.name ||
50
- `Datasource ${index + 1}`;
51
- let fileName = datasource.fileName || datasource.file;
52
- if (!fileName) {
53
- const key = datasource.key || '';
54
- // Suffix matches split getExternalDatasourceFileName for consistent README and file names
55
- let suffix;
56
- if (key.startsWith(`${systemKey}-deploy-`)) {
57
- suffix = key.slice(`${systemKey}-deploy-`.length);
58
- } else if (systemKey && key.startsWith(`${systemKey}-`)) {
59
- suffix = key.slice(systemKey.length + 1);
60
- } else if (key) {
61
- suffix = key;
62
- } else {
63
- suffix = entityType;
64
- }
65
- fileName = systemKey ? `${systemKey}-datasource-${suffix}.yaml` : `${suffix}.yaml`;
66
- }
67
- return { entityType, displayName, fileName };
68
- });
88
+ const ext = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
89
+ return datasources.map((datasource, index) =>
90
+ normalizeOneDatasource(datasource, index, systemKey, ext)
91
+ );
69
92
  }
70
93
 
71
94
  /**
@@ -78,6 +101,7 @@ function normalizeDatasources(datasources, systemKey) {
78
101
  * @param {string} [params.displayName] - Display name
79
102
  * @param {string} [params.description] - Description
80
103
  * @param {Array} [params.datasources] - Datasource objects
104
+ * @param {string} [params.fileExt] - File extension for config files (e.g. '.json', '.yaml'); default '.json'
81
105
  * @returns {Object} Template context
82
106
  */
83
107
  function buildExternalReadmeContext(params = {}) {
@@ -86,7 +110,8 @@ function buildExternalReadmeContext(params = {}) {
86
110
  const displayName = params.displayName || formatDisplayName(systemKey);
87
111
  const description = params.description || `External system integration for ${systemKey}`;
88
112
  const systemType = params.systemType || 'openapi';
89
- const datasources = normalizeDatasources(params.datasources, systemKey);
113
+ const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
114
+ const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
90
115
 
91
116
  return {
92
117
  appName,
@@ -94,7 +119,9 @@ function buildExternalReadmeContext(params = {}) {
94
119
  displayName,
95
120
  description,
96
121
  systemType,
122
+ fileExt: fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`,
97
123
  datasourceCount: datasources.length,
124
+ hasDatasources: datasources.length > 0,
98
125
  datasources
99
126
  };
100
127
  }
@@ -241,8 +241,66 @@ function displayIntegrationTestResults(results, verbose = false) {
241
241
  }
242
242
  }
243
243
 
244
+ /**
245
+ * Displays E2E test results (steps: config, credential, sync, data, cip).
246
+ * Supports sync response (data.steps only), final poll (data.steps + data.success), and running poll
247
+ * (data.completedActions, no data.steps yet). When status is present (async flow), shows it.
248
+ *
249
+ * @param {Object} data - E2E response or poll data
250
+ * @param {string} [data.status] - Optional status: 'running' | 'completed' | 'failed' (async flow)
251
+ * @param {Object[]} [data.steps] - Per-step results (final state)
252
+ * @param {Object[]} [data.completedActions] - Steps completed so far (running state when steps absent)
253
+ * @param {boolean} [data.success] - Overall success (final state)
254
+ * @param {string} [data.error] - Error message when failed
255
+ * @param {boolean} [verbose] - Show detailed output
256
+ */
257
+ /* eslint-disable max-statements,complexity -- Step iteration and status display */
258
+ function displayE2EResults(data, verbose = false) {
259
+ logger.log(chalk.blue('\nšŸ“Š E2E Test Results\n'));
260
+ if (data.status) {
261
+ const statusLabel = data.status === 'running'
262
+ ? chalk.yellow('running')
263
+ : data.status === 'completed'
264
+ ? chalk.green('completed')
265
+ : data.status === 'failed'
266
+ ? chalk.red('failed')
267
+ : data.status;
268
+ logger.log(`Status: ${statusLabel}`);
269
+ }
270
+ const steps = data.steps || data.completedActions || [];
271
+ if (steps.length === 0) {
272
+ if (data.success === false) {
273
+ logger.log(chalk.red('āœ— E2E test failed'));
274
+ if (data.error) logger.log(chalk.red(` Error: ${data.error}`));
275
+ } else if (data.status === 'running') {
276
+ logger.log(chalk.gray(' No steps completed yet'));
277
+ } else {
278
+ logger.log(chalk.yellow('No step results returned'));
279
+ }
280
+ return;
281
+ }
282
+ const isRunning = data.status === 'running' && !data.steps;
283
+ if (isRunning && verbose) {
284
+ logger.log(chalk.gray(` (${steps.length} step(s) completed so far)`));
285
+ }
286
+ for (const step of steps) {
287
+ const name = step.name || step.step || 'unknown';
288
+ const ok = step.success !== false && !step.error;
289
+ logger.log(` ${ok ? chalk.green('āœ“') : chalk.red('āœ—')} ${name}`);
290
+ if (!ok && (step.error || step.message)) logger.log(chalk.red(` ${step.error || step.message}`));
291
+ if (verbose && step.message && ok) logger.log(chalk.gray(` ${step.message}`));
292
+ }
293
+ if (isRunning) {
294
+ return;
295
+ }
296
+ const allPassed = steps.every(s => s.success !== false && !s.error);
297
+ logger.log(allPassed ? chalk.green('\nāœ… E2E test passed!') : chalk.red('\nāŒ E2E test failed'));
298
+ }
299
+
244
300
  module.exports = {
245
301
  displayTestResults,
246
- displayIntegrationTestResults
302
+ displayIntegrationTestResults,
303
+ displayE2EResults,
304
+ displayDatasourceIntegrationResult
247
305
  };
248
306