@aifabrix/builder 2.44.5 → 2.45.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 (249) hide show
  1. package/.cursor/rules/cli-layout.mdc +8 -4
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/README.md +15 -23
  4. package/integration/hubspot-test/README.md +2 -0
  5. package/integration/hubspot-test/test.js +5 -3
  6. package/jest.projects.js +104 -2
  7. package/lib/api/controller-health.api.js +49 -0
  8. package/lib/api/dimension-values.api.js +82 -0
  9. package/lib/api/dimensions.api.js +114 -0
  10. package/lib/api/external-systems.api.js +1 -0
  11. package/lib/api/integration-clients.api.js +168 -0
  12. package/lib/api/types/dimension-values.types.js +28 -0
  13. package/lib/api/types/dimensions.types.js +31 -0
  14. package/lib/api/types/integration-clients.types.js +45 -0
  15. package/lib/api/validation-runner.js +46 -25
  16. package/lib/app/deploy-config.js +11 -1
  17. package/lib/app/deploy-status-display.js +3 -3
  18. package/lib/app/deploy.js +36 -14
  19. package/lib/app/display.js +15 -11
  20. package/lib/app/helpers.js +3 -3
  21. package/lib/app/index.js +3 -3
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +7 -6
  24. package/lib/app/restart-display.js +126 -0
  25. package/lib/app/rotate-secret.js +7 -6
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +58 -19
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +148 -74
  32. package/lib/app/show-display.js +7 -0
  33. package/lib/app/show.js +87 -5
  34. package/lib/build/index.js +83 -49
  35. package/lib/cli/doctor-check.js +117 -0
  36. package/lib/cli/index.js +8 -2
  37. package/lib/cli/infra-guided.js +460 -0
  38. package/lib/cli/installation-log-command.js +73 -0
  39. package/lib/cli/setup-app.js +31 -3
  40. package/lib/cli/setup-auth.js +98 -27
  41. package/lib/cli/setup-dev-path-commands.js +50 -3
  42. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  43. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  44. package/lib/cli/setup-infra.js +132 -118
  45. package/lib/cli/setup-integration-client.js +182 -0
  46. package/lib/cli/setup-parameters.js +21 -2
  47. package/lib/cli/setup-platform.js +102 -0
  48. package/lib/cli/setup-secrets.js +18 -6
  49. package/lib/cli/setup-utility-resolve.js +132 -0
  50. package/lib/cli/setup-utility.js +143 -84
  51. package/lib/commands/app-logs.js +81 -33
  52. package/lib/commands/auth-config.js +116 -18
  53. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  54. package/lib/commands/datasource-capability-output.js +29 -0
  55. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  56. package/lib/commands/datasource-capability.js +411 -0
  57. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  58. package/lib/commands/datasource.js +53 -13
  59. package/lib/commands/dev-down.js +3 -3
  60. package/lib/commands/dev-infra-gate.js +32 -0
  61. package/lib/commands/dev-init.js +13 -7
  62. package/lib/commands/dimension-value.js +179 -0
  63. package/lib/commands/dimension.js +330 -0
  64. package/lib/commands/integration-client.js +430 -0
  65. package/lib/commands/login-device.js +65 -30
  66. package/lib/commands/login.js +21 -10
  67. package/lib/commands/parameters-validate.js +78 -13
  68. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  69. package/lib/commands/repair-datasource-keys.js +10 -5
  70. package/lib/commands/repair-datasource.js +19 -7
  71. package/lib/commands/repair-env-template.js +4 -1
  72. package/lib/commands/repair-openapi-sync.js +172 -0
  73. package/lib/commands/repair-persist.js +102 -0
  74. package/lib/commands/repair-rbac-extract.js +27 -0
  75. package/lib/commands/repair-rbac-migrate.js +186 -0
  76. package/lib/commands/repair-rbac.js +214 -31
  77. package/lib/commands/repair-system-alignment.js +246 -0
  78. package/lib/commands/repair-system-permissions.js +168 -0
  79. package/lib/commands/repair.js +120 -338
  80. package/lib/commands/secure.js +1 -1
  81. package/lib/commands/setup-modes.js +468 -0
  82. package/lib/commands/setup-prompts.js +421 -0
  83. package/lib/commands/setup.js +254 -0
  84. package/lib/commands/teardown.js +277 -0
  85. package/lib/commands/up-common.js +113 -19
  86. package/lib/commands/up-dataplane.js +44 -19
  87. package/lib/commands/up-miso.js +18 -18
  88. package/lib/commands/upload.js +111 -23
  89. package/lib/commands/wizard-core-helpers.js +14 -11
  90. package/lib/commands/wizard-core.js +6 -5
  91. package/lib/commands/wizard-dataplane.js +2 -2
  92. package/lib/commands/wizard-entity-selection.js +4 -3
  93. package/lib/commands/wizard-headless.js +2 -1
  94. package/lib/commands/wizard.js +2 -1
  95. package/lib/constants/infra-compose-service-names.js +40 -0
  96. package/lib/core/audit-logger.js +1 -34
  97. package/lib/core/config-admin-email.js +56 -0
  98. package/lib/core/config-normalize.js +60 -0
  99. package/lib/core/config-registered-controller-urls.js +54 -0
  100. package/lib/core/config.js +33 -50
  101. package/lib/core/env-reader.js +16 -3
  102. package/lib/core/secrets-admin-env.js +101 -0
  103. package/lib/core/secrets-ensure-infra.js +34 -1
  104. package/lib/core/secrets-ensure.js +88 -66
  105. package/lib/core/secrets-env-content.js +428 -0
  106. package/lib/core/secrets-env-declarative-expand.js +170 -0
  107. package/lib/core/secrets-env-write.js +29 -1
  108. package/lib/core/secrets-load.js +252 -0
  109. package/lib/core/secrets-names.js +32 -0
  110. package/lib/core/secrets.js +17 -757
  111. package/lib/datasource/capability/basic-exposure.js +76 -0
  112. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  113. package/lib/datasource/capability/capability-key.js +34 -0
  114. package/lib/datasource/capability/capability-resolve.js +172 -0
  115. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  116. package/lib/datasource/capability/copy-operations.js +348 -0
  117. package/lib/datasource/capability/copy-test-payload.js +139 -0
  118. package/lib/datasource/capability/create-operations.js +235 -0
  119. package/lib/datasource/capability/dimension-operations.js +151 -0
  120. package/lib/datasource/capability/dimension-validate.js +219 -0
  121. package/lib/datasource/capability/json-pointer.js +31 -0
  122. package/lib/datasource/capability/reference-rewrite.js +51 -0
  123. package/lib/datasource/capability/relate-operations.js +325 -0
  124. package/lib/datasource/capability/relate-validate.js +219 -0
  125. package/lib/datasource/capability/remove-operations.js +275 -0
  126. package/lib/datasource/capability/run-capability-copy.js +152 -0
  127. package/lib/datasource/capability/run-capability-diff.js +135 -0
  128. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  129. package/lib/datasource/capability/run-capability-edit.js +377 -0
  130. package/lib/datasource/capability/run-capability-relate.js +193 -0
  131. package/lib/datasource/capability/run-capability-remove.js +105 -0
  132. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  133. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  134. package/lib/datasource/list.js +136 -23
  135. package/lib/datasource/log-viewer.js +2 -4
  136. package/lib/datasource/unified-validation-run.js +51 -16
  137. package/lib/datasource/validate.js +53 -1
  138. package/lib/deployment/deploy-poll-ui.js +60 -0
  139. package/lib/deployment/deployer-status.js +29 -3
  140. package/lib/deployment/deployer.js +48 -30
  141. package/lib/deployment/environment.js +7 -2
  142. package/lib/deployment/poll-interval.js +72 -0
  143. package/lib/deployment/push.js +11 -9
  144. package/lib/external-system/deploy.js +9 -2
  145. package/lib/external-system/download.js +61 -32
  146. package/lib/external-system/sync-deploy-manifest.js +33 -0
  147. package/lib/infrastructure/index.js +49 -19
  148. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  149. package/lib/internal/node-fs.js +2 -0
  150. package/lib/parameters/infra-kv-discovery.js +29 -4
  151. package/lib/parameters/infra-parameter-catalog.js +6 -3
  152. package/lib/parameters/infra-parameter-validate.js +67 -19
  153. package/lib/resolvers/datasource-resolver.js +53 -0
  154. package/lib/resolvers/dimension-file.js +52 -0
  155. package/lib/resolvers/manifest-resolver.js +133 -0
  156. package/lib/schema/application-schema.json +4 -0
  157. package/lib/schema/external-datasource.schema.json +183 -53
  158. package/lib/schema/external-system.schema.json +23 -10
  159. package/lib/schema/infra.parameter.yaml +26 -1
  160. package/lib/schema/wizard-config.schema.json +1 -1
  161. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  162. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  163. package/lib/utils/app-config-resolver.js +24 -1
  164. package/lib/utils/app-run-containers.js +2 -2
  165. package/lib/utils/applications-config-defaults.js +206 -0
  166. package/lib/utils/auth-config-validator.js +2 -12
  167. package/lib/utils/bash-secret-env.js +59 -0
  168. package/lib/utils/cli-secrets-error-format.js +78 -0
  169. package/lib/utils/cli-test-layout-chalk.js +31 -9
  170. package/lib/utils/cli-utils.js +4 -36
  171. package/lib/utils/compose-generate-docker-compose.js +111 -6
  172. package/lib/utils/compose-generator.js +17 -8
  173. package/lib/utils/controller-url.js +50 -7
  174. package/lib/utils/datasource-test-run-display.js +8 -0
  175. package/lib/utils/dev-hosts-helper.js +3 -2
  176. package/lib/utils/dev-init-ssh-merge.js +2 -1
  177. package/lib/utils/docker-build.js +17 -9
  178. package/lib/utils/docker-reload-mount.js +127 -0
  179. package/lib/utils/env-copy.js +99 -14
  180. package/lib/utils/env-template.js +5 -1
  181. package/lib/utils/external-readme.js +71 -2
  182. package/lib/utils/external-system-local-test-tty.js +3 -2
  183. package/lib/utils/external-system-readiness-core.js +45 -12
  184. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  185. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  186. package/lib/utils/external-system-readiness-display.js +10 -1
  187. package/lib/utils/file-upload.js +40 -3
  188. package/lib/utils/health-check-db-init.js +107 -0
  189. package/lib/utils/health-check-public-warn.js +69 -0
  190. package/lib/utils/health-check-url.js +28 -10
  191. package/lib/utils/health-check.js +139 -107
  192. package/lib/utils/help-builder.js +5 -1
  193. package/lib/utils/image-name.js +34 -7
  194. package/lib/utils/infra-optional-service-flags.js +69 -0
  195. package/lib/utils/installation-log-core.js +282 -0
  196. package/lib/utils/installation-log-record.js +237 -0
  197. package/lib/utils/installation-log.js +123 -0
  198. package/lib/utils/integration-file-backup.js +74 -0
  199. package/lib/utils/log-redaction.js +105 -0
  200. package/lib/utils/manifest-location.js +164 -0
  201. package/lib/utils/manifest-source-emit.js +162 -0
  202. package/lib/utils/mutagen-install.js +30 -3
  203. package/lib/utils/paths.js +308 -76
  204. package/lib/utils/postgres-wipe.js +212 -0
  205. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  206. package/lib/utils/remote-dev-auth.js +21 -5
  207. package/lib/utils/remote-docker-env.js +9 -1
  208. package/lib/utils/remote-secrets-loader.js +49 -4
  209. package/lib/utils/resolve-docker-image-ref.js +9 -3
  210. package/lib/utils/run-cli-flags.js +29 -0
  211. package/lib/utils/secrets-ancestor-paths.js +47 -0
  212. package/lib/utils/secrets-canonical.js +10 -3
  213. package/lib/utils/secrets-helpers.js +17 -10
  214. package/lib/utils/secrets-kv-refs.js +42 -0
  215. package/lib/utils/secrets-kv-scope.js +19 -2
  216. package/lib/utils/secrets-materialize-local.js +134 -0
  217. package/lib/utils/secrets-path.js +26 -13
  218. package/lib/utils/secrets-utils.js +20 -10
  219. package/lib/utils/system-builder-root.js +42 -0
  220. package/lib/utils/url-declarative-public-base.js +80 -12
  221. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  222. package/lib/utils/url-declarative-resolve-build.js +24 -388
  223. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  224. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  225. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  226. package/lib/utils/url-declarative-resolve.js +47 -7
  227. package/lib/utils/url-declarative-runtime-base-path.js +52 -0
  228. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  229. package/lib/utils/urls-local-registry-scan.js +103 -0
  230. package/lib/utils/urls-local-registry.js +158 -76
  231. package/lib/utils/validation-poll-ui.js +81 -0
  232. package/lib/utils/validation-run-poll.js +29 -5
  233. package/lib/utils/with-muted-logger.js +53 -0
  234. package/package.json +3 -1
  235. package/templates/applications/dataplane/application.yaml +5 -1
  236. package/templates/applications/dataplane/rbac.yaml +10 -10
  237. package/templates/applications/keycloak/env.template +8 -6
  238. package/templates/applications/miso-controller/application.yaml +9 -0
  239. package/templates/applications/miso-controller/env.template +27 -29
  240. package/templates/applications/miso-controller/rbac.yaml +9 -9
  241. package/templates/external-system/README.md.hbs +83 -123
  242. package/.npmrc.token +0 -1
  243. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  244. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  245. package/.nyc_output/processinfo/index.json +0 -1
  246. package/lib/api/service-users.api.js +0 -150
  247. package/lib/api/types/service-users.types.js +0 -65
  248. package/lib/cli/setup-service-user.js +0 -187
  249. package/lib/commands/service-user.js +0 -429
@@ -11,6 +11,7 @@ const { formatSuccessLine } = require('./cli-test-layout-chalk');
11
11
 
12
12
  const fs = require('fs');
13
13
  const fsp = require('fs').promises;
14
+ const fsRealSync = require('../internal/fs-real-sync');
14
15
  const path = require('path');
15
16
  const yaml = require('js-yaml');
16
17
  const logger = require('./logger');
@@ -98,22 +99,37 @@ function resolveEnvOutputPath(rawOutputPath, variablesPath) {
98
99
  }
99
100
 
100
101
  /**
101
- * Writes .env to envOutputPath for reload path: merge run .env into existing file.
102
+ * Writes .env to envOutputPath for `--reload`: merge container `.env.run` into the host file.
103
+ * Preserves comments when a file already exists (e.g. after `aifabrix resolve`). Keys present only
104
+ * in `.env.run` (compose-only such as DB_0_NAME) are **not** appended unless they already exist as
105
+ * `KEY=` lines in the base file. If the output file is missing, seeds from `generateEnvContent`
106
+ * (`local`) so the host file keeps template comments and localhost-oriented URLs.
107
+ *
102
108
  * @async
103
109
  * @param {string} outputPath - Resolved output path
104
110
  * @param {string} runEnvPath - Path to .env.run
111
+ * @param {string} appName - Application name (for template seed when output is absent)
105
112
  */
106
- async function writeEnvOutputForReload(outputPath, runEnvPath) {
107
- const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
113
+ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
114
+ const { parseEnvContentToMap, mergeEnvMapIntoContent, generateEnvContent } = require('../core/secrets');
108
115
  const runContent = await fsp.readFile(runEnvPath, 'utf8');
109
116
  const runMap = parseEnvContentToMap(runContent);
110
- let toWrite = runContent;
111
- if (fs.existsSync(outputPath)) {
112
- const existingContent = await fsp.readFile(outputPath, 'utf8');
113
- toWrite = mergeEnvMapIntoContent(existingContent, runMap);
117
+ let baseContent;
118
+ if (fsRealSync.existsSync(outputPath)) {
119
+ baseContent = await fsp.readFile(outputPath, 'utf8');
120
+ } else if (appName) {
121
+ baseContent = await generateEnvContent(appName, null, 'local', false);
122
+ baseContent = substituteMntDataForLocal(baseContent, outputPath);
123
+ } else {
124
+ baseContent = runContent;
114
125
  }
126
+ const toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
115
127
  await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
116
- logger.log(formatSuccessLine(`Wrote .env to envOutputPath (same as container, for --reload): ${outputPath}`));
128
+ logger.log(
129
+ formatSuccessLine(
130
+ `Wrote .env to envOutputPath (--reload: merged container env; no extra keys): ${outputPath}`
131
+ )
132
+ );
117
133
  }
118
134
 
119
135
  /**
@@ -245,6 +261,30 @@ async function patchEnvContentForLocal(envContent, variables) {
245
261
  return envContent;
246
262
  }
247
263
 
264
+ /**
265
+ * Copy the just-written builder `<appPath>/.env` to `build.envOutputPath` so both files
266
+ * share the same resolution pass (no second `generateEnvContent` with a different environment).
267
+ * Applies {@link substituteMntDataForLocal} for host paths; merges into an existing output file
268
+ * when present (preserves comments).
269
+ *
270
+ * @async
271
+ * @param {string} envPath - Path to `<appPath>/.env` (already written)
272
+ * @param {string} outputPath - Resolved `build.envOutputPath`
273
+ * @param {string} envOutputPathLabel - Label for log (e.g. raw `build.envOutputPath` value)
274
+ */
275
+ async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel) {
276
+ const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
277
+ const raw = fs.readFileSync(envPath, 'utf8');
278
+ const synced = substituteMntDataForLocal(raw, outputPath);
279
+ let toWrite = synced;
280
+ if (fs.existsSync(outputPath)) {
281
+ const existingContent = fs.readFileSync(outputPath, 'utf8');
282
+ toWrite = mergeEnvMapIntoContent(existingContent, parseEnvContentToMap(synced));
283
+ }
284
+ fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
285
+ logger.log(formatSuccessLine(`Copied resolved .env to envOutputPath (${envOutputPathLabel})`));
286
+ }
287
+
248
288
  /**
249
289
  * Write regenerated local .env to output path (merge with existing if present).
250
290
  * @async
@@ -252,10 +292,12 @@ async function patchEnvContentForLocal(envContent, variables) {
252
292
  * @param {string} appName - Application name
253
293
  * @param {string} [secretsPath] - Path to secrets file (optional)
254
294
  * @param {string} envOutputPathLabel - Label for log message (e.g. variables.build.envOutputPath)
295
+ * @param {Object} [extraOpts] - Optional: `appPath` for {@link module:lib/core/secrets-env-content.generateEnvContent}
255
296
  */
256
- async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel) {
297
+ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel, extraOpts = {}) {
257
298
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
258
- let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
299
+ const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
300
+ let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
259
301
  localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
260
302
  let toWrite = localEnvContent;
261
303
  if (fs.existsSync(outputPath)) {
@@ -284,16 +326,45 @@ async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOu
284
326
  }
285
327
 
286
328
  /**
287
- * Process and optionally copy env file to envOutputPath if configured
288
- * Regenerates .env file with env=local for local development (apps/.env)
329
+ * Process and optionally copy env file to envOutputPath if configured.
330
+ * When `appName` is set and `preferLocalEnvOutputPath` is true, regenerates **local**-flavored env at
331
+ * `build.envOutputPath` (IDE/host). **False** only when `remote-server` and `applications.<app>.reload` are both set
332
+ * (docker flavor at env output). Otherwise, when `appName`
333
+ * is set and `envPath` exists, copies the same resolved `<appPath>/.env` content to the output path.
334
+ * Falls back to local regeneration when `envPath` is missing, or patched copy when `appName` is omitted.
335
+ *
289
336
  * @async
290
337
  * @function processEnvVariables
291
338
  * @param {string} envPath - Path to generated .env file
292
339
  * @param {string} variablesPath - Path to application config
293
340
  * @param {string} appName - Application name (for regenerating with local env)
294
341
  * @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
342
+ * @param {Object} [copyOptions] - Optional: `preferLocalEnvOutputPath` (IDE/host file gets **local** flavor
343
+ * while `<appPath>/.env` may be docker); `appPath` overrides app root for local regeneration
295
344
  */
296
- async function processEnvVariables(envPath, variablesPath, appName, secretsPath) {
345
+ async function copyOrRegenerateEnvForNamedApp(opts) {
346
+ const {
347
+ envPath,
348
+ outputPath,
349
+ appName,
350
+ secretsPath,
351
+ label,
352
+ preferLocal,
353
+ appPathForLocal
354
+ } = opts;
355
+ const localOpts = { appPath: appPathForLocal || undefined };
356
+ if (preferLocal) {
357
+ await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
358
+ return;
359
+ }
360
+ if (fs.existsSync(envPath)) {
361
+ await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label);
362
+ return;
363
+ }
364
+ await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
365
+ }
366
+
367
+ async function processEnvVariables(envPath, variablesPath, appName, secretsPath, copyOptions = {}) {
297
368
  if (!variablesPath || !fs.existsSync(variablesPath)) {
298
369
  return;
299
370
  }
@@ -310,8 +381,21 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
310
381
  }
311
382
 
312
383
  const label = variables.build.envOutputPath;
384
+ const appPathForLocal =
385
+ (copyOptions && copyOptions.appPath) ||
386
+ (variablesPath ? path.dirname(variablesPath) : null);
387
+ const preferLocal =
388
+ copyOptions && typeof copyOptions === 'object' && copyOptions.preferLocalEnvOutputPath === true;
313
389
  if (appName) {
314
- await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label);
390
+ await copyOrRegenerateEnvForNamedApp({
391
+ envPath,
392
+ outputPath,
393
+ appName,
394
+ secretsPath,
395
+ label,
396
+ preferLocal,
397
+ appPathForLocal
398
+ });
315
399
  } else {
316
400
  await writePatchedEnvToOutputPath(envPath, outputPath, variables, label);
317
401
  }
@@ -321,6 +405,7 @@ module.exports = {
321
405
  processEnvVariables,
322
406
  resolveEnvOutputPath,
323
407
  substituteMntDataForLocal,
408
+ syncWrittenBuilderEnvToOutputPath,
324
409
  writeEnvOutputForReload,
325
410
  writeEnvOutputForLocal
326
411
  };
@@ -13,6 +13,7 @@ const fsSync = require('fs');
13
13
  const path = require('path');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
+ const pathsUtil = require('./paths');
16
17
 
17
18
  /**
18
19
  * Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and optionally MISO_CONTROLLER_URL.
@@ -105,7 +106,10 @@ ${missingEntries.join('\n')}
105
106
  }
106
107
 
107
108
  async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, _controllerUrl) {
108
- const envTemplatePath = path.join(process.cwd(), 'builder', appKey, 'env.template');
109
+ // Use paths.getBuilderPath so the system builder root (e.g. ~/.aifabrix/builder/<app>) and
110
+ // AIFABRIX_BUILDER_DIR are respected. Falling back to process.cwd() previously made the binary
111
+ // CLI skip env.template updates whenever cwd was a non-builder repo (e.g. aifabrix-training).
112
+ const envTemplatePath = path.join(pathsUtil.getBuilderPath(appKey), 'env.template');
109
113
 
110
114
  if (!fsSync.existsSync(envTemplatePath)) {
111
115
  logger.warn(chalk.yellow(`⚠ env.template not found for ${appKey}, skipping update`));
@@ -15,6 +15,72 @@ const path = require('path');
15
15
  const handlebars = require('handlebars');
16
16
  const { getProjectRoot } = require('./paths');
17
17
 
18
+ /**
19
+ * Extracts secret keys from integration/<appName>/env.template when present.
20
+ * Looks for values like `kv://systemKey/apiKey` and returns that key for
21
+ * `aifabrix secret set <key> <value>`.
22
+ *
23
+ * @param {object} args
24
+ * @param {string} args.projectRoot
25
+ * @param {string} args.appName
26
+ * @returns {Array<{path: string, description: string}>}
27
+ */
28
+ function extractSecretPathsFromEnvTemplate({ projectRoot, appName }) {
29
+ if (!projectRoot || !appName) return [];
30
+
31
+ const envTemplatePath = path.join(projectRoot, 'integration', appName, 'env.template');
32
+ if (!fs.existsSync(envTemplatePath)) return [];
33
+
34
+ const raw = _tryReadTextOrNull(envTemplatePath);
35
+ if (raw === null) return [];
36
+ if (typeof raw !== 'string') return [];
37
+
38
+ function parseSecretKey(value) {
39
+ if (!value || typeof value !== 'string') return null;
40
+ const kvIdx = value.indexOf('kv://');
41
+ if (kvIdx === -1) return null;
42
+ const after = value.slice(kvIdx + 'kv://'.length);
43
+ const key = after.split(/[ \t#]/)[0]?.trim();
44
+ return key || null;
45
+ }
46
+
47
+ function parseLine(line) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#')) return null;
50
+ const eqIdx = trimmed.indexOf('=');
51
+ if (eqIdx <= 0) return null;
52
+ const varName = trimmed.slice(0, eqIdx).trim();
53
+ const value = trimmed.slice(eqIdx + 1).trim();
54
+ const key = parseSecretKey(value);
55
+ if (!key) return null;
56
+ return { varName: varName || 'Secret', key };
57
+ }
58
+
59
+ const out = [];
60
+ const seen = new Set();
61
+
62
+ for (const line of raw.split('\n')) {
63
+ const parsed = parseLine(line);
64
+ if (!parsed) continue;
65
+ const dedupeKey = `${parsed.varName}::${parsed.key}`;
66
+ if (seen.has(dedupeKey)) continue;
67
+ seen.add(dedupeKey);
68
+ out.push({ path: parsed.key, description: parsed.varName });
69
+ }
70
+
71
+ return out;
72
+ }
73
+
74
+ function _tryReadTextOrNull(p) {
75
+ try {
76
+ return fs.readFileSync(p, 'utf8');
77
+ } catch (e) {
78
+ // Some Jest suites partially mock fs.existsSync; treat missing files as absent templates.
79
+ if (e && e.code === 'ENOENT') return null;
80
+ throw e;
81
+ }
82
+ }
83
+
18
84
  /**
19
85
  * Formats a display name from a key
20
86
  * @param {string} key - System or app key
@@ -179,7 +245,9 @@ function buildExternalReadmeContext(params = {}) {
179
245
  const normalizedExt = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
180
246
  const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
181
247
  const authType = params.authType || params.authentication?.type || params.authentication?.method || params.authentication?.authType;
182
- const secretPaths = buildSecretPaths(systemKey, authType);
248
+ const projectRoot = getProjectRoot();
249
+ const secretPathsFromEnv = extractSecretPathsFromEnvTemplate({ projectRoot, appName });
250
+ const secretPaths = secretPathsFromEnv.length > 0 ? secretPathsFromEnv : buildSecretPaths(systemKey, authType);
183
251
  const rbacOptionalFile = rbacOptionalFilename(normalizedExt);
184
252
 
185
253
  return {
@@ -238,5 +306,6 @@ module.exports = {
238
306
  buildExternalReadmeContext,
239
307
  generateExternalReadmeContent,
240
308
  wrapPlainTextForMarkdown,
241
- collapseConsecutiveBlankLines
309
+ collapseConsecutiveBlankLines,
310
+ extractSecretPathsFromEnvTemplate
242
311
  };
@@ -19,6 +19,7 @@ const {
19
19
  headerKeyValue,
20
20
  formatStatusKeyValue,
21
21
  colorRollupPrefixedLine,
22
+ formatSuccessLine,
22
23
  metadata: metaGray
23
24
  } = require('./cli-test-layout-chalk');
24
25
 
@@ -338,7 +339,7 @@ function displayLocalExternalTestPlanLayout(results, verbose, appName) {
338
339
  function logVerboseSystemRows(systemResults) {
339
340
  for (const s of systemResults || []) {
340
341
  const ok = s.valid;
341
- logger.log(ok ? chalk.green(` ${s.file}`) : chalk.red(` ✖ ${s.file}`));
342
+ logger.log(ok ? (chalk.green(' ') + formatSuccessLine(s.file)) : chalk.red(` ✖ ${s.file}`));
342
343
  (s.errors || []).forEach(e => logger.log(chalk.red(` - ${e}`)));
343
344
  }
344
345
  }
@@ -346,7 +347,7 @@ function logVerboseSystemRows(systemResults) {
346
347
  function logVerboseDatasourceRows(datasourceResults) {
347
348
  for (const d of datasourceResults || []) {
348
349
  const ok = d.valid;
349
- logger.log(ok ? chalk.green(` ${d.key} (${d.file})`) : chalk.red(` ✖ ${d.key} (${d.file})`));
350
+ logger.log(ok ? (chalk.green(' ') + formatSuccessLine(`${d.key} (${d.file})`)) : chalk.red(` ✖ ${d.key} (${d.file})`));
350
351
  (d.errors || []).forEach(e => logger.log(chalk.red(` - ${e}`)));
351
352
  (d.warnings || []).forEach(w => logger.log(chalk.yellow(` ⚠ ${w}`)));
352
353
  if (d.fieldMappingResults && d.fieldMappingResults.mappedFields) {
@@ -43,30 +43,57 @@ function unwrapPublicationResult(res) {
43
43
  * Rules: inactive/archived → Failed; MCP expected but missing → Partial; draft → Partial; published/deployed + active → Ready.
44
44
  * @param {Object} ds - ExternalDataSourceResponse-like
45
45
  * @param {boolean} generateMcpContract - From application config
46
- * @returns {'ready'|'partial'|'failed'}
46
+ * @returns {{ tier: 'ready'|'partial'|'failed', partialReason: string|null }}
47
47
  */
48
- function classifyDatasourceTierA(ds, generateMcpContract) {
48
+ function classifyDatasourceTierADetail(ds, generateMcpContract) {
49
49
  const active = ds.isActive !== false;
50
50
  const status = String(ds.status || '').toLowerCase();
51
51
  if (!active || status === 'archived') {
52
- return 'failed';
52
+ return { tier: 'failed', partialReason: null };
53
53
  }
54
54
  if (generateMcpContract === true && !ds.mcpContract) {
55
- return 'partial';
55
+ return { tier: 'partial', partialReason: 'mcp_missing' };
56
56
  }
57
57
  if (status === 'draft') {
58
- return 'partial';
58
+ return { tier: 'partial', partialReason: 'draft' };
59
59
  }
60
60
  if (status === 'published' || status === 'deployed') {
61
- return 'ready';
61
+ return { tier: 'ready', partialReason: null };
62
+ }
63
+ return { tier: 'partial', partialReason: 'unknown_status' };
64
+ }
65
+
66
+ /**
67
+ * @param {Object} ds - ExternalDataSourceResponse-like
68
+ * @param {boolean} generateMcpContract - From application config
69
+ * @returns {'ready'|'partial'|'failed'}
70
+ */
71
+ function classifyDatasourceTierA(ds, generateMcpContract) {
72
+ return classifyDatasourceTierADetail(ds, generateMcpContract).tier;
73
+ }
74
+
75
+ /**
76
+ * Short hint for CLI when Tier A is partial (machine codes from classifyDatasourceTierADetail).
77
+ * @param {string|null} partialReason
78
+ * @returns {string}
79
+ */
80
+ function formatTierAPartialHint(partialReason) {
81
+ if (partialReason === 'mcp_missing') {
82
+ return 'no MCP contract stored (OpenAPI link or generation)';
83
+ }
84
+ if (partialReason === 'draft') {
85
+ return 'status draft (trigger paths / certification gate)';
62
86
  }
63
- return 'partial';
87
+ if (partialReason === 'unknown_status') {
88
+ return 'unexpected lifecycle status';
89
+ }
90
+ return '';
64
91
  }
65
92
 
66
93
  /**
67
94
  * @param {Array<Object>} datasources - Datasource list
68
95
  * @param {boolean} generateMcpContract
69
- * @returns {{ rows: Array<{ key: string, tier: string }>, ready: number, partial: number, failed: number }}
96
+ * @returns {{ rows: Array<{ key: string, tier: string, partialReason?: string|null }>, ready: number, partial: number, failed: number }}
70
97
  */
71
98
  function summarizeDatasourceTiersA(datasources, generateMcpContract) {
72
99
  const rows = [];
@@ -75,10 +102,14 @@ function summarizeDatasourceTiersA(datasources, generateMcpContract) {
75
102
  let failed = 0;
76
103
  for (const ds of datasources || []) {
77
104
  const key = ds.key || ds.sourceKey || 'unknown';
78
- const tier = classifyDatasourceTierA(ds, generateMcpContract);
79
- rows.push({ key, tier });
80
- if (tier === 'ready') ready += 1;
81
- else if (tier === 'partial') partial += 1;
105
+ const detail = classifyDatasourceTierADetail(ds, generateMcpContract);
106
+ const row = { key, tier: detail.tier };
107
+ if (detail.tier === 'partial' && detail.partialReason) {
108
+ row.partialReason = detail.partialReason;
109
+ }
110
+ rows.push(row);
111
+ if (detail.tier === 'ready') ready += 1;
112
+ else if (detail.tier === 'partial') partial += 1;
82
113
  else failed += 1;
83
114
  }
84
115
  return { rows, ready, partial, failed };
@@ -401,7 +432,9 @@ module.exports = {
401
432
  unwrapApiData,
402
433
  unwrapPublicationResult,
403
434
  isPublicationResultShape,
435
+ classifyDatasourceTierADetail,
404
436
  classifyDatasourceTierA,
437
+ formatTierAPartialHint,
405
438
  summarizeDatasourceTiersA,
406
439
  aggregateVerdictFromCounts,
407
440
  classifyDatasourceTierB,
@@ -83,7 +83,7 @@ function logDeployProbeDatasourceSection(probeData) {
83
83
  * @param {Object|null} systemFromDataplane
84
84
  * @param {boolean} genMcp
85
85
  */
86
- function logDeployContractsSection(systemFromDataplane, genMcp) {
86
+ function logDeployContractsSection(systemFromDataplane, genMcp, dataplaneUrl, systemKey) {
87
87
  if (!systemFromDataplane) return;
88
88
  logSeparator();
89
89
  logSectionTitle('Contracts:');
@@ -94,7 +94,7 @@ function logDeployContractsSection(systemFromDataplane, genMcp) {
94
94
  } else {
95
95
  logger.log(chalk.gray('○ OpenAPI docs URL not available'));
96
96
  }
97
- logDocsBlock(systemFromDataplane);
97
+ logDocsBlock(systemFromDataplane, { dataplaneUrl, systemKey, genMcp: mcpOk });
98
98
  }
99
99
 
100
100
  /**
@@ -260,7 +260,7 @@ function logDeployReadinessSummary(ctx) {
260
260
 
261
261
  logDeployIdentityAndCredentialBlocks(systemCfg, !!probeData);
262
262
 
263
- logDeployContractsSection(systemFromDataplane, genMcp);
263
+ logDeployContractsSection(systemFromDataplane, genMcp, dataplaneUrl, systemKey);
264
264
  logDeployNextActionsSection(systemKey, probeData, summary, genMcp);
265
265
  }
266
266
 
@@ -9,7 +9,11 @@
9
9
  const chalk = require('chalk');
10
10
  const { failureGlyph, successGlyph } = require('./cli-test-layout-chalk');
11
11
  const logger = require('./logger');
12
- const { extractIdentitySummary, resolveCredentialTestEndpointDisplay } = require('./external-system-readiness-core');
12
+ const {
13
+ extractIdentitySummary,
14
+ resolveCredentialTestEndpointDisplay,
15
+ formatTierAPartialHint
16
+ } = require('./external-system-readiness-core');
13
17
 
14
18
  const SEP = chalk.gray('────────────────────────────────');
15
19
 
@@ -56,8 +60,12 @@ function logDatasourceTable(rows, counts, title) {
56
60
  logSectionTitle(title && String(title).trim() ? String(title).trim() : 'Datasources:');
57
61
  for (const r of rows) {
58
62
  const statusLabel = r.tier === 'ready' ? 'Ready' : r.tier === 'failed' ? 'Failed' : 'Partial';
63
+ const hint =
64
+ r.tier === 'partial' && r.partialReason
65
+ ? chalk.gray(` — ${formatTierAPartialHint(r.partialReason)}`)
66
+ : '';
59
67
  logger.log(
60
- `${tierGlyph(r.tier)} ${r.key.padEnd(14, ' ')} ${chalk.gray('(' + statusLabel + ')')}`
68
+ `${tierGlyph(r.tier)} ${r.key.padEnd(14, ' ')} ${chalk.gray('(' + statusLabel + ')')}${hint}`
61
69
  );
62
70
  }
63
71
  logger.log('');
@@ -120,14 +128,35 @@ function logNextActions(actions, extraLine) {
120
128
  }
121
129
  }
122
130
 
131
+ /**
132
+ * Dataplane serves MCP OpenAPI docs at /api/v1/mcp/{systemKey}/docs.
133
+ *
134
+ * @param {Object} sys - ExternalSystemResponse
135
+ * @param {{ dataplaneUrl?: string, systemKey?: string, genMcp?: boolean }} [opts]
136
+ * @returns {string|null}
137
+ */
138
+ function deriveMcpDocsPageUrl(sys, opts) {
139
+ if (!sys || !opts) return null;
140
+ if (sys.mcpDocsPageUrl) return String(sys.mcpDocsPageUrl);
141
+ const { dataplaneUrl, systemKey, genMcp } = opts;
142
+ if (!genMcp || !dataplaneUrl || !systemKey) return null;
143
+ if (!sys.mcpServerUrl) return null;
144
+ if (!sys.openApiDocsPageUrl && !sys.apiDocumentUrl) return null;
145
+ const base = String(dataplaneUrl).replace(/\/+$/, '');
146
+ return `${base}/api/v1/mcp/${encodeURIComponent(systemKey)}/docs`;
147
+ }
148
+
123
149
  /**
124
150
  * @param {Object} sys - ExternalSystemResponse
151
+ * @param {{ dataplaneUrl?: string, systemKey?: string, genMcp?: boolean }} [opts]
125
152
  */
126
- function logDocsBlock(sys) {
153
+ function logDocsBlock(sys, opts) {
127
154
  if (!sys) return;
128
155
  const urls = [];
129
156
  if (sys.openApiDocsPageUrl) urls.push({ label: 'OpenAPI Docs Page', url: sys.openApiDocsPageUrl });
130
157
  if (sys.apiDocumentUrl) urls.push({ label: 'API Docs', url: sys.apiDocumentUrl });
158
+ const mcpDocs = deriveMcpDocsPageUrl(sys, opts);
159
+ if (mcpDocs) urls.push({ label: 'MCP Docs Page', url: mcpDocs });
131
160
  if (sys.mcpServerUrl) urls.push({ label: 'MCP Server', url: sys.mcpServerUrl });
132
161
  if (urls.length === 0) return;
133
162
  logSeparator();
@@ -147,5 +176,6 @@ module.exports = {
147
176
  logIdentityBlock,
148
177
  logCredentialIntentBlock,
149
178
  logNextActions,
179
+ deriveMcpDocsPageUrl,
150
180
  logDocsBlock
151
181
  };
@@ -44,7 +44,16 @@ function logPublishResultBlock(publication) {
44
44
  }
45
45
  logSeparator();
46
46
  logSectionTitle('MCP Contract:');
47
- logger.log(genMcp ? formatSuccessLine('Generated') : chalk.gray('○ Not requested (generateMcpContract false)'));
47
+ if (genMcp) {
48
+ logger.log(formatSuccessLine('Generation requested (manifest generateMcpContract)'));
49
+ logger.log(
50
+ chalk.gray(
51
+ ' Per-datasource MCP appears when dataplane stores mcpContract (OpenAPI resolves + generation succeeds).'
52
+ )
53
+ );
54
+ } else {
55
+ logger.log(chalk.gray('○ Not requested (generateMcpContract false)'));
56
+ }
48
57
  }
49
58
 
50
59
  /**
@@ -33,10 +33,10 @@ async function validateFileExists(filePath) {
33
33
  * @param {Object} additionalFields - Additional fields
34
34
  * @returns {Promise<FormData>} FormData object
35
35
  */
36
- async function buildFormData(filePath, fieldName, additionalFields) {
36
+ async function buildFormData(filePath, fieldName, additionalFields, opts = {}) {
37
37
  const formData = new FormData();
38
38
  const fileContent = await fs.readFile(filePath);
39
- const fileName = path.basename(filePath);
39
+ const fileName = opts.filenameOverride ? String(opts.filenameOverride) : path.basename(filePath);
40
40
  const fileBlob = new Blob([fileContent], { type: 'application/octet-stream' });
41
41
  formData.append(fieldName, fileBlob, fileName);
42
42
 
@@ -74,6 +74,43 @@ async function uploadFile(url, filePath, fieldName = 'file', authConfig = {}, ad
74
74
  return await client.postFormData(endpointPath, formData);
75
75
  }
76
76
 
77
+ /**
78
+ * Upload a file using multipart/form-data but override the filename sent to the server.
79
+ * Useful when the server derives a key from the upload filename.
80
+ *
81
+ * @async
82
+ * @function uploadFileAs
83
+ * @param {string} url - Full API endpoint URL
84
+ * @param {string} filePath - Path to file to upload
85
+ * @param {string} filenameOverride - Filename to present to server (e.g. 'my-key.json')
86
+ * @param {string} fieldName - Form field name for the file (default: 'file')
87
+ * @param {Object} [authConfig] - Authentication configuration
88
+ * @param {Object} [additionalFields] - Additional form fields to include
89
+ * @returns {Promise<Object>} API response
90
+ */
91
+ async function uploadFileAs(
92
+ url,
93
+ filePath,
94
+ filenameOverride,
95
+ fieldName = 'file',
96
+ authConfig = {},
97
+ additionalFields = {}
98
+ ) {
99
+ await validateFileExists(filePath);
100
+ if (!filenameOverride || typeof filenameOverride !== 'string') {
101
+ throw new Error('filenameOverride is required and must be a string');
102
+ }
103
+
104
+ const parsed = new URL(url);
105
+ const baseUrl = parsed.origin;
106
+ const endpointPath = parsed.pathname + parsed.search;
107
+
108
+ const formData = await buildFormData(filePath, fieldName, additionalFields, { filenameOverride });
109
+ const client = new ApiClient(baseUrl, authConfig);
110
+ return await client.postFormData(endpointPath, formData);
111
+ }
112
+
77
113
  module.exports = {
78
- uploadFile
114
+ uploadFile,
115
+ uploadFileAs
79
116
  };