@aifabrix/builder 2.42.1 → 2.44.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 (392) hide show
  1. package/.cursor/rules/anchor-docs.mdc +15 -0
  2. package/README.md +2 -2
  3. package/anchor-docs/README.md +10 -0
  4. package/anchor-docs/_TEMPLATE +24 -0
  5. package/bin/aifabrix.js +13 -4
  6. package/integration/hubspot-test/README.md +157 -0
  7. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  8. package/integration/{hubspot → hubspot-test}/create-hubspot.js +10 -10
  9. package/integration/hubspot-test/env.template +4 -0
  10. package/integration/hubspot-test/hubspot-test-datasource-company.json +138 -0
  11. package/integration/hubspot-test/hubspot-test-datasource-contact.json +146 -0
  12. package/integration/hubspot-test/hubspot-test-datasource-deal.json +146 -0
  13. package/integration/hubspot-test/hubspot-test-datasource-users.json +76 -0
  14. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +201 -24
  15. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  16. package/integration/hubspot-test/rbac.json +166 -0
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  20. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  23. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  26. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  27. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  29. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  30. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  31. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  32. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  33. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  34. package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +1 -7
  35. package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +3 -3
  36. package/integration/{hubspot → hubspot-test}/test.js +137 -102
  37. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  38. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  39. package/integration/hubspot-test/wizard-hubspot-test-headless.yaml +23 -0
  40. package/integration/roundtrip-test-local/README.md +144 -0
  41. package/integration/roundtrip-test-local/application.yaml +13 -0
  42. package/integration/roundtrip-test-local/env.template +15 -0
  43. package/integration/roundtrip-test-local/roundtrip-test-local-datasource-roundtrip-test-company.yaml +14 -0
  44. package/integration/roundtrip-test-local/roundtrip-test-local-deploy.json +61 -0
  45. package/integration/roundtrip-test-local/roundtrip-test-local-system.yaml +25 -0
  46. package/integration/roundtrip-test-local2/README.md +144 -0
  47. package/integration/roundtrip-test-local2/application.yaml +13 -0
  48. package/integration/roundtrip-test-local2/env.template +15 -0
  49. package/integration/roundtrip-test-local2/roundtrip-test-local2-datasource-company.yaml +31 -0
  50. package/integration/roundtrip-test-local2/roundtrip-test-local2-deploy.json +86 -0
  51. package/integration/roundtrip-test-local2/roundtrip-test-local2-system.yaml +25 -0
  52. package/integration/test/wizard.yaml +8 -0
  53. package/jest.config.default.js +10 -0
  54. package/jest.config.integration.fixtures.js +22 -0
  55. package/jest.config.integration.js +21 -18
  56. package/jest.config.isolated.js +10 -0
  57. package/jest.projects.js +288 -0
  58. package/lib/api/datasources-core.api.js +3 -3
  59. package/lib/api/dev-mtls-request.js +110 -0
  60. package/lib/api/dev-server-https.js +145 -0
  61. package/lib/api/dev.api.js +133 -144
  62. package/lib/api/index.js +0 -1
  63. package/lib/api/pipeline.api.js +67 -20
  64. package/lib/api/service-users.api.js +111 -2
  65. package/lib/api/types/dev.types.js +4 -3
  66. package/lib/api/types/pipeline.types.js +8 -5
  67. package/lib/api/types/service-users.types.js +41 -0
  68. package/lib/api/types/validation-run.types.js +56 -0
  69. package/lib/api/validation-run.api.js +99 -0
  70. package/lib/api/validation-runner.js +99 -0
  71. package/lib/app/config.js +1 -1
  72. package/lib/app/deploy-status-display.js +2 -2
  73. package/lib/app/deploy.js +7 -6
  74. package/lib/app/display.js +2 -1
  75. package/lib/app/dockerfile.js +3 -2
  76. package/lib/app/down.js +2 -1
  77. package/lib/app/helpers.js +6 -5
  78. package/lib/app/index.js +27 -8
  79. package/lib/app/list.js +7 -6
  80. package/lib/app/push.js +4 -3
  81. package/lib/app/register.js +19 -8
  82. package/lib/app/rotate-secret.js +17 -13
  83. package/lib/app/run-container-start.js +184 -0
  84. package/lib/app/run-docker-fallback.js +108 -0
  85. package/lib/app/run-env-compose.js +30 -42
  86. package/lib/app/run-helpers.js +49 -126
  87. package/lib/app/run-infra-requirements.js +30 -0
  88. package/lib/app/run-resolve-image.js +21 -0
  89. package/lib/app/run.js +74 -21
  90. package/lib/app/show-display.js +1 -1
  91. package/lib/app/show.js +1 -1
  92. package/lib/build/index.js +13 -10
  93. package/lib/cli/index.js +2 -0
  94. package/lib/cli/setup-app.help.js +67 -0
  95. package/lib/cli/setup-app.js +59 -123
  96. package/lib/cli/setup-app.test-commands.js +179 -0
  97. package/lib/cli/setup-auth.js +36 -14
  98. package/lib/cli/setup-credential-deployment.js +22 -8
  99. package/lib/cli/setup-dev-path-commands.js +124 -0
  100. package/lib/cli/setup-dev.js +190 -103
  101. package/lib/cli/setup-environment.js +11 -20
  102. package/lib/cli/setup-external-system.js +62 -22
  103. package/lib/cli/setup-infra.js +139 -47
  104. package/lib/cli/setup-parameters.js +32 -0
  105. package/lib/cli/setup-secrets.js +147 -10
  106. package/lib/cli/setup-service-user.js +146 -20
  107. package/lib/cli/setup-utility.js +47 -19
  108. package/lib/commands/app-down.js +5 -7
  109. package/lib/commands/app-install.js +14 -7
  110. package/lib/commands/app-logs.js +13 -10
  111. package/lib/commands/app-shell.js +4 -1
  112. package/lib/commands/app-test.js +25 -19
  113. package/lib/commands/app.js +22 -10
  114. package/lib/commands/auth-config.js +10 -14
  115. package/lib/commands/auth-status.js +4 -3
  116. package/lib/commands/credential-env.js +4 -3
  117. package/lib/commands/credential-list.js +5 -4
  118. package/lib/commands/credential-push.js +4 -3
  119. package/lib/commands/datasource-unified-test-cli.js +495 -0
  120. package/lib/commands/datasource-unified-test-cli.options.js +149 -0
  121. package/lib/commands/datasource-validation-cli.js +129 -0
  122. package/lib/commands/datasource.js +123 -71
  123. package/lib/commands/deployment-list.js +6 -5
  124. package/lib/commands/dev-cli-handlers.js +122 -18
  125. package/lib/commands/dev-down.js +4 -3
  126. package/lib/commands/dev-init.js +231 -116
  127. package/lib/commands/dev-show-display.js +473 -0
  128. package/lib/commands/login-credentials.js +3 -2
  129. package/lib/commands/login-device.js +4 -3
  130. package/lib/commands/login.js +5 -4
  131. package/lib/commands/logout.js +8 -7
  132. package/lib/commands/parameters-validate.js +54 -0
  133. package/lib/commands/repair-datasource.js +314 -68
  134. package/lib/commands/repair-env-template.js +16 -10
  135. package/lib/commands/repair-rbac.js +25 -19
  136. package/lib/commands/repair.js +116 -32
  137. package/lib/commands/secrets-list.js +23 -12
  138. package/lib/commands/secrets-remove-all.js +220 -0
  139. package/lib/commands/secrets-remove.js +22 -13
  140. package/lib/commands/secrets-set.js +21 -12
  141. package/lib/commands/secrets-validate.js +20 -7
  142. package/lib/commands/secure.js +10 -9
  143. package/lib/commands/service-user.js +243 -13
  144. package/lib/commands/test-e2e-external.js +27 -1
  145. package/lib/commands/up-common.js +28 -2
  146. package/lib/commands/up-dataplane.js +31 -18
  147. package/lib/commands/up-miso.js +19 -29
  148. package/lib/commands/upload.js +138 -39
  149. package/lib/commands/wizard-core-helpers.js +1 -1
  150. package/lib/commands/wizard-dataplane.js +4 -3
  151. package/lib/commands/wizard-helpers.js +3 -3
  152. package/lib/commands/wizard.js +2 -2
  153. package/lib/core/admin-secrets.js +16 -5
  154. package/lib/core/audit-logger.js +12 -4
  155. package/lib/core/config-attach-extensions.js +46 -0
  156. package/lib/core/config-runtime-paths.js +29 -0
  157. package/lib/core/config.js +59 -58
  158. package/lib/core/diff.js +3 -2
  159. package/lib/core/ensure-encryption-key.js +2 -4
  160. package/lib/core/secrets-ensure-infra.js +77 -0
  161. package/lib/core/secrets-ensure.js +120 -64
  162. package/lib/core/secrets-env-write.js +35 -7
  163. package/lib/core/secrets-infra-placeholder-sync.js +61 -0
  164. package/lib/core/secrets.js +228 -42
  165. package/lib/core/templates-env.js +4 -3
  166. package/lib/core/templates.js +1 -1
  167. package/lib/datasource/abac-validator.js +148 -0
  168. package/lib/datasource/deploy.js +75 -53
  169. package/lib/datasource/field-reference-validator.js +77 -36
  170. package/lib/datasource/integration-context.js +63 -0
  171. package/lib/datasource/list.js +8 -7
  172. package/lib/datasource/log-viewer.js +252 -0
  173. package/lib/datasource/resolve-app.js +109 -0
  174. package/lib/datasource/test-e2e.js +95 -155
  175. package/lib/datasource/test-integration.js +121 -109
  176. package/lib/datasource/unified-validation-run-body.js +65 -0
  177. package/lib/datasource/unified-validation-run-post.js +23 -0
  178. package/lib/datasource/unified-validation-run-resolve.js +43 -0
  179. package/lib/datasource/unified-validation-run.js +92 -0
  180. package/lib/datasource/validate.js +162 -15
  181. package/lib/deployment/deployer.js +4 -3
  182. package/lib/deployment/environment.js +7 -6
  183. package/lib/deployment/push.js +17 -8
  184. package/lib/external-system/delete.js +4 -3
  185. package/lib/external-system/deploy.js +131 -53
  186. package/lib/external-system/download-helpers.js +1 -1
  187. package/lib/external-system/download.js +7 -6
  188. package/lib/external-system/generator.js +104 -14
  189. package/lib/external-system/integration-test-dispatch.js +26 -0
  190. package/lib/external-system/test-execution.js +5 -1
  191. package/lib/external-system/test-helpers.js +0 -4
  192. package/lib/external-system/test-system-level-helpers.js +110 -0
  193. package/lib/external-system/test-system-level.js +83 -44
  194. package/lib/external-system/test.js +59 -8
  195. package/lib/generator/builders.js +23 -11
  196. package/lib/generator/deploy-manifest-azure-kv.js +81 -0
  197. package/lib/generator/external-controller-manifest.js +3 -3
  198. package/lib/generator/external.js +23 -11
  199. package/lib/generator/helpers.js +71 -12
  200. package/lib/generator/index.js +8 -4
  201. package/lib/generator/split-readme.js +12 -7
  202. package/lib/generator/split-variables.js +2 -1
  203. package/lib/generator/split.js +46 -11
  204. package/lib/generator/wizard-readme.js +3 -3
  205. package/lib/generator/wizard.js +16 -13
  206. package/lib/infrastructure/compose.js +60 -6
  207. package/lib/infrastructure/helpers.js +238 -51
  208. package/lib/infrastructure/index.js +64 -37
  209. package/lib/infrastructure/services.js +21 -15
  210. package/lib/internal/fs-real-sync.js +104 -0
  211. package/lib/internal/node-fs.js +98 -0
  212. package/lib/parameters/database-secret-values.js +173 -0
  213. package/lib/parameters/infra-kv-discovery.js +121 -0
  214. package/lib/parameters/infra-parameter-catalog.js +458 -0
  215. package/lib/parameters/infra-parameter-validate.js +64 -0
  216. package/lib/schema/application-schema.json +37 -17
  217. package/lib/schema/datasource-test-run.schema.json +493 -0
  218. package/lib/schema/deployment-rules.yaml +102 -63
  219. package/lib/schema/external-datasource.schema.json +1201 -433
  220. package/lib/schema/external-system.schema.json +181 -5
  221. package/lib/schema/flag-map-validation-run.json +31 -0
  222. package/lib/schema/infra-parameter.schema.json +106 -0
  223. package/lib/schema/infra.parameter.yaml +421 -0
  224. package/lib/schema/type/credential-auth-templates.json +40 -0
  225. package/lib/schema/type/document-storage.json +213 -0
  226. package/lib/schema/type/message-service.json +123 -0
  227. package/lib/schema/type/vector-store.json +88 -0
  228. package/lib/utils/aifabrix-runtime-config-dir.js +132 -0
  229. package/lib/utils/api-error-handler.js +2 -2
  230. package/lib/utils/api.js +49 -14
  231. package/lib/utils/app-config-resolver.js +23 -1
  232. package/lib/utils/app-register-api.js +3 -2
  233. package/lib/utils/app-register-auth.js +1 -1
  234. package/lib/utils/app-register-config.js +4 -4
  235. package/lib/utils/app-register-display.js +3 -2
  236. package/lib/utils/app-register-validator.js +3 -2
  237. package/lib/utils/app-run-containers.js +26 -22
  238. package/lib/utils/app-scoped-config.js +31 -0
  239. package/lib/utils/app-service-env-from-builder.js +164 -0
  240. package/lib/utils/build-copy.js +1 -1
  241. package/lib/utils/build-helpers.js +20 -20
  242. package/lib/utils/build-resolve-image.js +165 -0
  243. package/lib/utils/cli-layout-chalk.js +8 -0
  244. package/lib/utils/cli-test-layout-chalk.js +267 -0
  245. package/lib/utils/cli-utils.js +88 -11
  246. package/lib/utils/compose-db-passwords.js +138 -0
  247. package/lib/utils/compose-generate-docker-compose.js +216 -0
  248. package/lib/utils/compose-generator.js +197 -291
  249. package/lib/utils/compose-miso-env.js +18 -0
  250. package/lib/utils/compose-traefik-ingress-base.js +158 -0
  251. package/lib/utils/config-paths.js +209 -6
  252. package/lib/utils/config-scoped-resources-preference.js +41 -0
  253. package/lib/utils/controller-deployment-outcome.js +68 -0
  254. package/lib/utils/credential-display.js +2 -2
  255. package/lib/utils/credential-secrets-env.js +16 -1
  256. package/lib/utils/dataplane-pipeline-warning.js +4 -3
  257. package/lib/utils/datasource-test-run-capability-scope.js +43 -0
  258. package/lib/utils/datasource-test-run-debug-display.js +137 -0
  259. package/lib/utils/datasource-test-run-debug-slice.js +93 -0
  260. package/lib/utils/datasource-test-run-display.js +442 -0
  261. package/lib/utils/datasource-test-run-exit.js +58 -0
  262. package/lib/utils/datasource-test-run-legacy-adapter.js +93 -0
  263. package/lib/utils/datasource-test-run-report-version.js +51 -0
  264. package/lib/utils/datasource-test-run-schema-sync.js +59 -0
  265. package/lib/utils/datasource-test-run-tty-log.js +81 -0
  266. package/lib/utils/datasource-validation-watch.js +266 -0
  267. package/lib/utils/declarative-url-ports.js +47 -0
  268. package/lib/utils/derive-env-key-from-client-id.js +41 -0
  269. package/lib/utils/dev-ca-install.js +185 -23
  270. package/lib/utils/dev-cert-helper.js +266 -17
  271. package/lib/utils/dev-hosts-helper.js +307 -0
  272. package/lib/utils/dev-init-cert-hints.js +37 -0
  273. package/lib/utils/dev-init-health-messages.js +52 -0
  274. package/lib/utils/dev-init-resolve.js +86 -0
  275. package/lib/utils/dev-init-ssh-merge.js +65 -0
  276. package/lib/utils/dev-ssh-config-helper.js +196 -0
  277. package/lib/utils/dev-user-groups.js +93 -0
  278. package/lib/utils/docker-build.js +42 -17
  279. package/lib/utils/docker-exec.js +28 -0
  280. package/lib/utils/docker-manifest-public-port.js +116 -0
  281. package/lib/utils/docker-not-running-hint.js +52 -0
  282. package/lib/utils/docker.js +98 -11
  283. package/lib/utils/ensure-dev-certs-for-remote-docker.js +192 -0
  284. package/lib/utils/env-config-loader.js +10 -91
  285. package/lib/utils/env-copy.js +19 -10
  286. package/lib/utils/env-map.js +42 -11
  287. package/lib/utils/env-template.js +2 -2
  288. package/lib/utils/environment-scoped-resources.js +144 -0
  289. package/lib/utils/error-formatter.js +125 -9
  290. package/lib/utils/error-formatters/http-status-errors.js +6 -5
  291. package/lib/utils/error-formatters/network-errors.js +2 -1
  292. package/lib/utils/error-formatters/permission-errors.js +2 -1
  293. package/lib/utils/error-formatters/validation-errors.js +2 -1
  294. package/lib/utils/external-env-template.js +180 -0
  295. package/lib/utils/external-readme.js +8 -1
  296. package/lib/utils/external-system-display.js +277 -136
  297. package/lib/utils/external-system-local-test-tty.js +389 -0
  298. package/lib/utils/external-system-readiness-core.js +377 -0
  299. package/lib/utils/external-system-readiness-deploy-display.js +270 -0
  300. package/lib/utils/external-system-readiness-display-internals.js +150 -0
  301. package/lib/utils/external-system-readiness-display.js +186 -0
  302. package/lib/utils/external-system-test-helpers.js +24 -6
  303. package/lib/utils/external-system-validators.js +32 -14
  304. package/lib/utils/health-check-url.js +119 -0
  305. package/lib/utils/health-check.js +59 -25
  306. package/lib/utils/help-builder.js +14 -13
  307. package/lib/utils/image-version.js +4 -8
  308. package/lib/utils/infra-containers.js +4 -7
  309. package/lib/utils/infra-env-defaults.js +162 -0
  310. package/lib/utils/infra-status-display.js +167 -0
  311. package/lib/utils/infra-status.js +16 -8
  312. package/lib/utils/local-secrets.js +29 -7
  313. package/lib/utils/paths.js +136 -48
  314. package/lib/utils/port-resolver.js +10 -23
  315. package/lib/utils/redis-env-scope.js +62 -0
  316. package/lib/utils/register-aifabrix-shell-env.js +204 -0
  317. package/lib/utils/remote-builder-validation.js +99 -0
  318. package/lib/utils/remote-dev-auth.js +117 -21
  319. package/lib/utils/remote-docker-env.js +67 -15
  320. package/lib/utils/remote-secrets-loader.js +13 -4
  321. package/lib/utils/resolve-docker-image-ref.js +124 -0
  322. package/lib/utils/schema-loader.js +22 -9
  323. package/lib/utils/secrets-bash-kv.js +25 -0
  324. package/lib/utils/secrets-generator.js +171 -51
  325. package/lib/utils/secrets-helpers.js +70 -59
  326. package/lib/utils/secrets-kv-scope.js +60 -0
  327. package/lib/utils/secrets-utils.js +35 -37
  328. package/lib/utils/secrets-validation.js +3 -1
  329. package/lib/utils/secrets-yaml-preserve.js +109 -0
  330. package/lib/utils/secure-file-permissions.js +91 -0
  331. package/lib/utils/ssh-key-helper.js +4 -2
  332. package/lib/utils/template-helpers.js +2 -2
  333. package/lib/utils/test-log-writer.js +3 -3
  334. package/lib/utils/token-manager.js +37 -5
  335. package/lib/utils/url-declarative-public-base.js +188 -0
  336. package/lib/utils/url-declarative-resolve-build.js +493 -0
  337. package/lib/utils/url-declarative-resolve-load-doc.js +51 -0
  338. package/lib/utils/url-declarative-resolve.js +220 -0
  339. package/lib/utils/url-declarative-token-parse.js +74 -0
  340. package/lib/utils/url-declarative-url-flags.js +50 -0
  341. package/lib/utils/url-declarative-vdir-inactive-env.js +99 -0
  342. package/lib/utils/url-public-path-prefix.js +34 -0
  343. package/lib/utils/urls-local-registry.js +220 -0
  344. package/lib/utils/validation-report-tty-kit.js +77 -0
  345. package/lib/utils/validation-run-poll.js +89 -0
  346. package/lib/utils/validation-run-post-retry.js +73 -0
  347. package/lib/utils/validation-run-request.js +98 -0
  348. package/lib/utils/variable-transformer.js +21 -4
  349. package/lib/utils/yaml-preserve.js +78 -1
  350. package/lib/validation/datasource-warnings.js +56 -0
  351. package/lib/validation/env-template-auth.js +50 -2
  352. package/lib/validation/external-manifest-validator.js +35 -7
  353. package/lib/validation/validate-display.js +37 -31
  354. package/lib/validation/validate.js +9 -10
  355. package/lib/validation/validator-unresolved-placeholders.js +98 -0
  356. package/lib/validation/validator.js +32 -78
  357. package/lib/validation/wizard-config-validator.js +2 -1
  358. package/package.json +11 -3
  359. package/scripts/check-datasource-test-run-schema-sync.js +34 -0
  360. package/scripts/diagnose-cli.js +150 -0
  361. package/scripts/install-local.js +304 -55
  362. package/templates/README.md +15 -2
  363. package/templates/applications/dataplane/application.yaml +52 -2
  364. package/templates/applications/dataplane/env.template +80 -18
  365. package/templates/applications/dataplane/rbac.yaml +8 -0
  366. package/templates/applications/keycloak/application.yaml +9 -1
  367. package/templates/applications/keycloak/env.template +15 -6
  368. package/templates/applications/miso-controller/application.yaml +10 -2
  369. package/templates/applications/miso-controller/env.template +55 -14
  370. package/templates/applications/miso-controller/rbac.yaml +5 -0
  371. package/templates/external-system/README.md.hbs +20 -7
  372. package/templates/external-system/deploy.js.hbs +5 -5
  373. package/templates/external-system/env.template.hbs +22 -0
  374. package/templates/external-system/external-datasource.yaml.hbs +197 -118
  375. package/templates/infra/compose.yaml.hbs +20 -4
  376. package/templates/python/docker-compose.hbs +16 -0
  377. package/templates/typescript/docker-compose.hbs +16 -0
  378. package/integration/hubspot/README.md +0 -102
  379. package/integration/hubspot/env.template +0 -4
  380. package/integration/hubspot/hubspot-datasource-company.json +0 -541
  381. package/integration/hubspot/hubspot-datasource-contact.json +0 -639
  382. package/integration/hubspot/hubspot-datasource-deal.json +0 -588
  383. package/integration/hubspot/hubspot-datasource-users.json +0 -116
  384. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  385. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  386. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  387. package/lib/api/external-test.api.js +0 -111
  388. package/lib/schema/env-config.yaml +0 -43
  389. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  390. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  391. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  392. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
@@ -9,13 +9,13 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
12
13
  const path = require('path');
13
- const chalk = require('chalk');
14
14
  const logger = require('../utils/logger');
15
15
  const { getAifabrixSecretsPath } = require('../core/config');
16
16
  const { saveLocalSecret, saveSecret } = require('../utils/local-secrets');
17
17
  const pathsUtil = require('../utils/paths');
18
- const { isRemoteSecretsUrl, getRemoteDevAuth } = require('../utils/remote-dev-auth');
18
+ const remoteDevAuth = require('../utils/remote-dev-auth');
19
19
  const devApi = require('../api/dev.api');
20
20
 
21
21
  /**
@@ -39,20 +39,29 @@ const devApi = require('../api/dev.api');
39
39
  * @returns {Promise<void>}
40
40
  */
41
41
  async function setSharedSecret(key, value, generalSecretsPath) {
42
- if (isRemoteSecretsUrl(generalSecretsPath)) {
43
- const auth = await getRemoteDevAuth();
42
+ const target = await remoteDevAuth.resolveSharedSecretsEndpoint(generalSecretsPath);
43
+ if (remoteDevAuth.isRemoteSecretsUrl(target)) {
44
+ const auth = await remoteDevAuth.getRemoteDevAuth();
44
45
  if (!auth) {
45
46
  throw new Error('Remote server not configured or certificate missing. Run "aifabrix dev init" first.');
46
47
  }
47
- await devApi.addSecret(auth.serverUrl, auth.clientCertPem, { key, value });
48
- logger.log(chalk.green(`✓ Secret '${key}' saved to remote secrets (shared).`));
48
+ await devApi.addSecret(
49
+ auth.serverUrl,
50
+ auth.clientCertPem,
51
+ { key, value },
52
+ auth.serverCaPem || undefined,
53
+ target
54
+ );
55
+ const host = remoteDevAuth.getSharedSecretsRemoteHostname(target);
56
+ const where = host ? `shared secrets (remote - ${host})` : 'shared secrets (remote)';
57
+ logger.log(formatSuccessLine(`Secret '${key}' saved to ${where}.`));
49
58
  return;
50
59
  }
51
- const resolvedPath = path.isAbsolute(generalSecretsPath)
52
- ? generalSecretsPath
53
- : path.resolve(process.cwd(), generalSecretsPath);
60
+ const resolvedPath = path.isAbsolute(target)
61
+ ? target
62
+ : path.resolve(process.cwd(), target);
54
63
  await saveSecret(key, value, resolvedPath);
55
- logger.log(chalk.green(`✓ Secret '${key}' saved to general secrets file: ${resolvedPath}`));
64
+ logger.log(formatSuccessLine(`Secret '${key}' saved to general secrets file: ${resolvedPath}`));
56
65
  }
57
66
 
58
67
  async function handleSecretsSet(key, value, options) {
@@ -80,8 +89,8 @@ async function handleSecretsSet(key, value, options) {
80
89
  await setSharedSecret(key, value, generalSecretsPath);
81
90
  } else {
82
91
  await saveLocalSecret(key, value);
83
- const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
84
- logger.log(chalk.green(`✓ Secret '${key}' saved to user secrets file: ${userSecretsPath}`));
92
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
93
+ logger.log(formatSuccessLine(`Secret '${key}' saved to user secrets file: ${userSecretsPath}`));
85
94
  }
86
95
  }
87
96
 
@@ -1,3 +1,4 @@
1
+ const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
1
2
  /**
2
3
  * AI Fabrix Builder – Secrets validate command
3
4
  *
@@ -9,9 +10,9 @@
9
10
  */
10
11
 
11
12
  const chalk = require('chalk');
12
- const path = require('path');
13
13
  const logger = require('../utils/logger');
14
14
  const { validateSecretsFile } = require('../utils/secrets-validation');
15
+ const { validateDataplaneSecrets } = require('../utils/token-manager');
15
16
  const pathsUtil = require('../utils/paths');
16
17
  const secretsEnsure = require('../core/secrets-ensure');
17
18
 
@@ -33,18 +34,30 @@ async function handleSecretsValidate(pathArg, options = {}) {
33
34
  if (target.type === 'file' && target.filePath) {
34
35
  filePath = target.filePath;
35
36
  } else {
36
- filePath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
37
+ filePath = pathsUtil.getPrimaryUserSecretsLocalPath();
37
38
  }
38
39
  }
39
40
 
40
41
  const result = validateSecretsFile(filePath, { checkNaming: Boolean(options.naming) });
42
+ const dataplaneResult = validateDataplaneSecrets(filePath);
43
+ const allValid = result.valid && dataplaneResult.valid;
41
44
  if (result.valid) {
42
- logger.log(chalk.green(`✓ Secrets file is valid: ${result.path}`));
43
- return { valid: true, errors: [] };
45
+ logger.log(formatSuccessLine(`Secrets file is valid: ${result.path}`));
46
+ } else {
47
+ logger.log(formatBlockingError(`Validation failed: ${result.path}`));
48
+ result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
44
49
  }
45
- logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
46
- result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
47
- return { valid: false, errors: result.errors };
50
+ if (!dataplaneResult.valid) {
51
+ logger.log(chalk.yellow(`⚠ ${dataplaneResult.hint}`));
52
+ if (result.valid) {
53
+ logger.log(chalk.yellow(' Wizard/dataplane calls may fail until dataplane credentials are present.'));
54
+ }
55
+ }
56
+ return {
57
+ valid: allValid,
58
+ errors: allValid ? [] : [...result.errors, ...(dataplaneResult.valid ? [] : [dataplaneResult.hint])],
59
+ dataplaneValid: dataplaneResult.valid
60
+ };
48
61
  }
49
62
 
50
63
  module.exports = { handleSecretsValidate };
@@ -1,3 +1,4 @@
1
+ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
1
2
  /**
2
3
  * AI Fabrix Builder - Secure Command
3
4
  *
@@ -32,7 +33,7 @@ async function findSecretsFiles() {
32
33
  const files = [];
33
34
 
34
35
  // User's secrets file
35
- const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
36
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
36
37
  if (fs.existsSync(userSecretsPath)) {
37
38
  files.push({ path: userSecretsPath, type: 'user' });
38
39
  }
@@ -131,7 +132,7 @@ async function getEncryptionKey(options) {
131
132
  // Check if key already exists in config
132
133
  const existingKey = await getSecretsEncryptionKey();
133
134
  if (existingKey) {
134
- logger.log(chalk.yellow('⚠️ Encryption key already configured in config.yaml'));
135
+ logger.log(chalk.yellow(' Encryption key already configured in config.yaml'));
135
136
  const useExisting = await inquirer.prompt([{
136
137
  type: 'confirm',
137
138
  name: 'use',
@@ -143,18 +144,18 @@ async function getEncryptionKey(options) {
143
144
  } else {
144
145
  encryptionKey = await promptForEncryptionKey();
145
146
  await setSecretsEncryptionKey(encryptionKey);
146
- logger.log(chalk.green('Encryption key saved to config.yaml'));
147
+ logger.log(formatSuccessLine('Encryption key saved to config.yaml'));
147
148
  }
148
149
  } else {
149
150
  encryptionKey = await promptForEncryptionKey();
150
151
  await setSecretsEncryptionKey(encryptionKey);
151
- logger.log(chalk.green('Encryption key saved to config.yaml'));
152
+ logger.log(formatSuccessLine('Encryption key saved to config.yaml'));
152
153
  }
153
154
  } else {
154
155
  // Validate and save the provided key
155
156
  validateEncryptionKey(encryptionKey);
156
157
  await setSecretsEncryptionKey(encryptionKey);
157
- logger.log(chalk.green('Encryption key saved to config.yaml'));
158
+ logger.log(formatSuccessLine('Encryption key saved to config.yaml'));
158
159
  }
159
160
  return encryptionKey;
160
161
  }
@@ -178,12 +179,12 @@ async function processSecretsFiles(secretsFiles, encryptionKey) {
178
179
  totalValues += result.total;
179
180
 
180
181
  if (result.encrypted > 0) {
181
- logger.log(chalk.green(` Encrypted ${result.encrypted} of ${result.total} values`));
182
+ logger.log(chalk.green(` Encrypted ${result.encrypted} of ${result.total} values`));
182
183
  } else {
183
184
  logger.log(chalk.gray(` - All values already encrypted (${result.total} total)`));
184
185
  }
185
186
  } catch (error) {
186
- logger.log(chalk.red(` Error: ${error.message}`));
187
+ logger.log(chalk.red(` Error: ${error.message}`));
187
188
  }
188
189
  }
189
190
 
@@ -197,7 +198,7 @@ async function processSecretsFiles(secretsFiles, encryptionKey) {
197
198
  * @param {number} totalValues - Total number of values
198
199
  */
199
200
  function displayEncryptionSummary(filesCount, totalEncrypted, totalValues) {
200
- logger.log(chalk.green('\n✅ Encryption complete!'));
201
+ logger.log(formatSuccessParagraph('Encryption complete!'));
201
202
  logger.log(chalk.gray(` Files processed: ${filesCount}`));
202
203
  logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
203
204
  logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
@@ -224,7 +225,7 @@ async function handleSecure(options) {
224
225
  const secretsFiles = await findSecretsFiles();
225
226
 
226
227
  if (secretsFiles.length === 0) {
227
- logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
228
+ logger.log(chalk.yellow(' No secrets files found to encrypt'));
228
229
  logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure aifabrix-secrets in config.yaml'));
229
230
  return;
230
231
  }
@@ -1,3 +1,4 @@
1
+ const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
1
2
  /**
2
3
  * Service user create command – create service user and get one-time secret
3
4
  * POST /api/v1/service-users. Used by `aifabrix service-user create`.
@@ -12,7 +13,14 @@ const logger = require('../utils/logger');
12
13
  const { resolveControllerUrl } = require('../utils/controller-url');
13
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
15
  const { normalizeControllerUrl } = require('../core/config');
15
- const { createServiceUser } = require('../api/service-users.api');
16
+ const {
17
+ createServiceUser,
18
+ listServiceUsers,
19
+ regenerateSecretServiceUser,
20
+ deleteServiceUser,
21
+ updateGroupsServiceUser,
22
+ updateRedirectUrisServiceUser
23
+ } = require('../api/service-users.api');
16
24
 
17
25
  const ONE_TIME_WARNING =
18
26
  'Save this secret now; it will not be shown again.';
@@ -54,6 +62,12 @@ function extractCreateResponse(response) {
54
62
  return { clientId, clientSecret };
55
63
  }
56
64
 
65
+ const ID_WIDTH = 38;
66
+ const USERNAME_WIDTH = 22;
67
+ const EMAIL_WIDTH = 28;
68
+ const CLIENT_ID_WIDTH = 24;
69
+ const TABLE_SEPARATOR_LENGTH = 130;
70
+
57
71
  /**
58
72
  * Log error for failed create response and exit
59
73
  * @param {Object} response - API response with success: false
@@ -62,25 +76,105 @@ function handleCreateError(response) {
62
76
  const status = response.status;
63
77
  const msg = response.formattedError || response.error || 'Request failed';
64
78
  if (status === 400) {
65
- logger.error(chalk.red(`❌ Validation error: ${msg}`));
79
+ logger.error(formatBlockingError(`Validation error: ${msg}`));
66
80
  } else if (status === 401) {
67
- logger.error(chalk.red('Unauthorized. Run "aifabrix login" and try again.'));
81
+ logger.error(formatBlockingError('Unauthorized. Run "aifabrix login" and try again.'));
68
82
  } else if (status === 403) {
69
- logger.error(chalk.red('Missing permission: service-user:create'));
83
+ logger.error(formatBlockingError('Missing permission: service-user:create'));
70
84
  logger.error(chalk.gray('Your account needs the service-user:create permission on the controller.'));
71
85
  } else {
72
- logger.error(chalk.red(`❌ Failed to create service user: ${msg}`));
86
+ logger.error(formatBlockingError(`Failed to create service user: ${msg}`));
73
87
  }
74
88
  process.exit(1);
75
89
  }
76
90
 
91
+ /**
92
+ * Log error for service-user API response and exit
93
+ * @param {Object} response - API response with success: false
94
+ * @param {string} permissionScope - Permission hint: 'read' | 'update' | 'delete'
95
+ */
96
+ function handleServiceUserApiError(response, permissionScope) {
97
+ const status = response.status;
98
+ const msg = response.formattedError || response.error || 'Request failed';
99
+ if (status === 400) {
100
+ logger.error(formatBlockingError(`Validation error: ${msg}`));
101
+ } else if (status === 401) {
102
+ logger.error(formatBlockingError('Unauthorized. Run "aifabrix login" and try again.'));
103
+ } else if (status === 403) {
104
+ logger.error(formatBlockingError(`Missing permission: service-user:${permissionScope}`));
105
+ logger.error(chalk.gray(`Your account needs the service-user:${permissionScope} permission on the controller.`));
106
+ } else if (status === 404) {
107
+ logger.error(formatBlockingError('Service user not found.'));
108
+ const detail = response.error || '';
109
+ if (detail) {
110
+ logger.error(chalk.gray(detail));
111
+ }
112
+ } else {
113
+ logger.error(formatBlockingError(`Request failed: ${msg}`));
114
+ }
115
+ process.exit(1);
116
+ }
117
+
118
+ /**
119
+ * Resolve controller URL and auth for list/rotate/delete/update (no create-specific validation)
120
+ * @async
121
+ * @param {Object} options - CLI options (controller optional)
122
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
123
+ */
124
+ async function resolveControllerAndAuth(options) {
125
+ const controllerUrl = options.controller || (await resolveControllerUrl());
126
+ if (!controllerUrl) {
127
+ logger.error(formatBlockingError('Controller URL is required. Run "aifabrix login" first.'));
128
+ process.exit(1);
129
+ }
130
+ const authResult = await getServiceUserAuth(controllerUrl);
131
+ if (!authResult || !authResult.token) {
132
+ logger.error(formatBlockingError(`No authentication token for controller: ${controllerUrl}`));
133
+ logger.error(chalk.gray('Run: aifabrix login'));
134
+ process.exit(1);
135
+ }
136
+ return {
137
+ controllerUrl: authResult.controllerUrl,
138
+ authConfig: { type: 'bearer', token: authResult.token }
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Display service user list as a table (id, username, email, clientId, active).
144
+ * API shape: items have id, username, email, status, federatedIdentity.keycloakClientId.
145
+ * @param {Array<{ id?: string, username?: string, email?: string, status?: string, active?: boolean, clientId?: string, federatedIdentity?: { keycloakClientId?: string } }>} items - Service users
146
+ */
147
+ function displayServiceUserList(items) {
148
+ logger.log(chalk.bold('\n📋 Service users:\n'));
149
+ if (!items || items.length === 0) {
150
+ logger.log(chalk.gray(' No service users found.\n'));
151
+ return;
152
+ }
153
+ const idCol = 'Id'.padEnd(ID_WIDTH);
154
+ const usernameCol = 'Username'.padEnd(USERNAME_WIDTH);
155
+ const emailCol = 'Email'.padEnd(EMAIL_WIDTH);
156
+ const clientIdCol = 'ClientId'.padEnd(CLIENT_ID_WIDTH);
157
+ const activeCol = 'Active';
158
+ logger.log(chalk.gray(`${idCol}${usernameCol}${emailCol}${clientIdCol}${activeCol}`));
159
+ logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
160
+ items.forEach((row) => {
161
+ const id = (row.id ?? '').toString().padEnd(ID_WIDTH);
162
+ const username = (row.username ?? '—').padEnd(USERNAME_WIDTH);
163
+ const email = (row.email ?? '—').padEnd(EMAIL_WIDTH);
164
+ const clientId = (row.federatedIdentity?.keycloakClientId ?? row.clientId ?? '—').padEnd(CLIENT_ID_WIDTH);
165
+ const active = row.status === 'active' ? 'yes' : (row.status ?? (row.active === true ? 'yes' : row.active === false ? 'no' : '—'));
166
+ logger.log(`${id}${username}${email}${clientId}${active}`);
167
+ });
168
+ logger.log('');
169
+ }
170
+
77
171
  /**
78
172
  * Display success output with clientId, clientSecret and one-time warning
79
173
  * @param {string} clientId - Service user client ID
80
174
  * @param {string} clientSecret - One-time client secret
81
175
  */
82
176
  function displayCreateSuccess(clientId, clientSecret) {
83
- logger.log(chalk.bold('\n Service user created\n'));
177
+ logger.log(chalk.bold('\n Service user created\n'));
84
178
  logger.log(chalk.cyan(' clientId: ') + clientId);
85
179
  logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
86
180
  logger.log('');
@@ -114,19 +208,19 @@ function validateServiceUserOptions(options) {
114
208
  const redirectUris = parseList(options.redirectUris);
115
209
  const groupNames = parseList(options.groupNames);
116
210
  if (!username) {
117
- logger.error(chalk.red('Username is required. Use --username <username>.'));
211
+ logger.error(formatBlockingError('Username is required. Use --username <username>.'));
118
212
  process.exit(1);
119
213
  }
120
214
  if (!email) {
121
- logger.error(chalk.red('Email is required. Use --email <email>.'));
215
+ logger.error(formatBlockingError('Email is required. Use --email <email>.'));
122
216
  process.exit(1);
123
217
  }
124
218
  if (redirectUris.length === 0) {
125
- logger.error(chalk.red('At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
219
+ logger.error(formatBlockingError('At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
126
220
  process.exit(1);
127
221
  }
128
222
  if (groupNames.length === 0) {
129
- logger.error(chalk.red('At least one group name is required. Use --group-names <name1,name2,...>.'));
223
+ logger.error(formatBlockingError('At least one group name is required. Use --group-names <name1,name2,...>.'));
130
224
  process.exit(1);
131
225
  }
132
226
  return {
@@ -148,12 +242,12 @@ async function resolveOptionsAndAuth(options) {
148
242
  const validated = validateServiceUserOptions(options);
149
243
  const controllerUrl = options.controller || (await resolveControllerUrl());
150
244
  if (!controllerUrl) {
151
- logger.error(chalk.red('Controller URL is required. Run "aifabrix login" first.'));
245
+ logger.error(formatBlockingError('Controller URL is required. Run "aifabrix login" first.'));
152
246
  process.exit(1);
153
247
  }
154
248
  const authResult = await getServiceUserAuth(controllerUrl);
155
249
  if (!authResult || !authResult.token) {
156
- logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
250
+ logger.error(formatBlockingError(`No authentication token for controller: ${controllerUrl}`));
157
251
  logger.error(chalk.gray('Run: aifabrix login'));
158
252
  process.exit(1);
159
253
  }
@@ -194,6 +288,142 @@ async function runServiceUserCreate(options = {}) {
194
288
  displayCreateSuccess(clientId, clientSecret);
195
289
  }
196
290
 
291
+ /**
292
+ * Require service user id option; exit with message if missing
293
+ * @param {string} [id] - Service user ID from options
294
+ * @returns {string} Trimmed id
295
+ */
296
+ function requireServiceUserId(id) {
297
+ const trimmed = (id && typeof id === 'string' ? id.trim() : '') || '';
298
+ if (!trimmed) {
299
+ logger.error(formatBlockingError('Service user ID is required. Use --id <uuid>.'));
300
+ process.exit(1);
301
+ }
302
+ return trimmed;
303
+ }
304
+
305
+ /**
306
+ * Run service-user list: call GET /api/v1/service-users and display table
307
+ * @async
308
+ * @param {Object} options - CLI options (controller, page, pageSize, sort, filter, search)
309
+ * @returns {Promise<void>}
310
+ */
311
+ async function runServiceUserList(options = {}) {
312
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
313
+ const listOptions = {
314
+ page: options.page,
315
+ pageSize: options.pageSize,
316
+ sort: options.sort,
317
+ filter: options.filter,
318
+ search: options.search
319
+ };
320
+ const response = await listServiceUsers(controllerUrl, authConfig, listOptions);
321
+ if (response && response.success === false) {
322
+ handleServiceUserApiError(response, 'read');
323
+ return;
324
+ }
325
+ const body = response?.data?.data ?? response?.data ?? response ?? {};
326
+ const items = Array.isArray(body) ? body : (body.data ?? []);
327
+ displayServiceUserList(items);
328
+ }
329
+
330
+ /**
331
+ * Run service-user rotate-secret: call POST .../regenerate-secret and print new secret once with warning
332
+ * @async
333
+ * @param {Object} options - CLI options (controller, id required)
334
+ * @returns {Promise<void>}
335
+ */
336
+ async function runServiceUserRotateSecret(options = {}) {
337
+ const id = requireServiceUserId(options.id);
338
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
339
+ const response = await regenerateSecretServiceUser(controllerUrl, authConfig, id);
340
+ if (response && response.success === false) {
341
+ handleServiceUserApiError(response, 'update');
342
+ return;
343
+ }
344
+ const payload = response?.data?.data ?? response?.data ?? response ?? {};
345
+ const clientSecret = payload?.clientSecret ?? '';
346
+ if (response && response.success === true) {
347
+ logger.log(chalk.bold('\n✔ Secret rotated\n'));
348
+ logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
349
+ logger.log('');
350
+ logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
351
+ logger.log('');
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Run service-user delete: call DELETE .../service-users/{id} (deactivates the user)
357
+ * @async
358
+ * @param {Object} options - CLI options (controller, id required)
359
+ * @returns {Promise<void>}
360
+ */
361
+ async function runServiceUserDelete(options = {}) {
362
+ const id = requireServiceUserId(options.id);
363
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
364
+ const response = await deleteServiceUser(controllerUrl, authConfig, id);
365
+ if (response && response.success === false) {
366
+ handleServiceUserApiError(response, 'delete');
367
+ return;
368
+ }
369
+ if (response && response.success === true) {
370
+ logger.log(formatSuccessLine('Service user deactivated.\n'));
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Run service-user update-groups: call PUT .../groups with groupNames
376
+ * @async
377
+ * @param {Object} options - CLI options (controller, id, groupNames required)
378
+ * @returns {Promise<void>}
379
+ */
380
+ async function runServiceUserUpdateGroups(options = {}) {
381
+ const id = requireServiceUserId(options.id);
382
+ const groupNames = parseList(options.groupNames);
383
+ if (groupNames.length === 0) {
384
+ logger.error(formatBlockingError('At least one group name is required. Use --group-names <name1,name2,...>.'));
385
+ process.exit(1);
386
+ }
387
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
388
+ const response = await updateGroupsServiceUser(controllerUrl, authConfig, id, { groupNames });
389
+ if (response && response.success === false) {
390
+ handleServiceUserApiError(response, 'update');
391
+ return;
392
+ }
393
+ if (response && response.success === true) {
394
+ logger.log(formatSuccessLine('Service user groups updated.\n'));
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Run service-user update-redirect-uris: call PUT .../redirect-uris (min 1 URI)
400
+ * @async
401
+ * @param {Object} options - CLI options (controller, id, redirectUris required, min 1)
402
+ * @returns {Promise<void>}
403
+ */
404
+ async function runServiceUserUpdateRedirectUris(options = {}) {
405
+ const id = requireServiceUserId(options.id);
406
+ const redirectUris = parseList(options.redirectUris);
407
+ if (redirectUris.length === 0) {
408
+ logger.error(formatBlockingError('At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
409
+ process.exit(1);
410
+ }
411
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
412
+ const response = await updateRedirectUrisServiceUser(controllerUrl, authConfig, id, { redirectUris });
413
+ if (response && response.success === false) {
414
+ handleServiceUserApiError(response, 'update');
415
+ return;
416
+ }
417
+ if (response && response.success === true) {
418
+ logger.log(formatSuccessLine('Service user redirect URIs updated.\n'));
419
+ }
420
+ }
421
+
197
422
  module.exports = {
198
- runServiceUserCreate
423
+ runServiceUserCreate,
424
+ runServiceUserList,
425
+ runServiceUserRotateSecret,
426
+ runServiceUserDelete,
427
+ runServiceUserUpdateGroups,
428
+ runServiceUserUpdateRedirectUris
199
429
  };
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  'use strict';
10
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
10
11
 
11
12
  const path = require('path');
12
13
  const fs = require('fs');
@@ -84,6 +85,23 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
84
85
  return keys;
85
86
  }
86
87
 
88
+ /**
89
+ * Full upload to dataplane when --sync (same path as `aifabrix upload <systemKey>`).
90
+ * @param {string} systemKey
91
+ * @param {Object} options
92
+ * @returns {Promise<void>}
93
+ */
94
+ async function syncLocalIfRequested(systemKey, options) {
95
+ if (options.sync !== true) return;
96
+ logger.log(chalk.cyan('Syncing local config to dataplane…'));
97
+ const { uploadExternalSystem } = require('./upload');
98
+ await uploadExternalSystem(systemKey, {
99
+ verbose: !!options.verbose,
100
+ minimal: true
101
+ });
102
+ logger.log(formatSuccessLine('Sync complete'));
103
+ }
104
+
87
105
  /* eslint-disable max-lines-per-function, max-statements -- Load context, then loop over keys */
88
106
  /**
89
107
  * Runs E2E for all datasources of an external system. Uses each datasource's payloadTemplate (no extra params required).
@@ -95,6 +113,7 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
95
113
  * @param {boolean} [options.debug] - Include debug, write log
96
114
  * @param {boolean} [options.verbose] - Verbose output
97
115
  * @param {boolean} [options.async] - If false, sync mode (default true)
116
+ * @param {boolean} [options.sync] - When true, run full upload (`uploadExternalSystem`) before per-datasource E2E
98
117
  * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
99
118
  */
100
119
  async function runTestE2EForExternalSystem(externalSystem, options = {}) {
@@ -137,6 +156,8 @@ async function runTestE2EForExternalSystem(externalSystem, options = {}) {
137
156
  return { success: true, results: [] };
138
157
  }
139
158
 
159
+ await syncLocalIfRequested(systemKey, options);
160
+
140
161
  const results = [];
141
162
  const opts = {
142
163
  app: externalSystem,
@@ -150,7 +171,12 @@ async function runTestE2EForExternalSystem(externalSystem, options = {}) {
150
171
  const data = await runDatasourceTestE2E(key, opts);
151
172
  const steps = data.steps || data.completedActions || [];
152
173
  const failed = data.success === false || steps.some(s => s.success === false || s.error);
153
- results.push({ key, success: !failed });
174
+ results.push({
175
+ key,
176
+ success: !failed,
177
+ error: failed ? (data.error || 'E2E step failed') : undefined,
178
+ datasourceTestRun: data.datasourceTestRun
179
+ });
154
180
  } catch (err) {
155
181
  results.push({ key, success: false, error: err.message });
156
182
  }
@@ -1,3 +1,4 @@
1
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
1
2
  /**
2
3
  * AI Fabrix Builder - Up Commands Shared Helpers
3
4
  *
@@ -165,6 +166,30 @@ function patchEnvOutputPathForDeployOnly(appName) {
165
166
  }
166
167
  }
167
168
 
169
+ /**
170
+ * Removes builder app directories for the given app names. Only removes paths under the builder root
171
+ * to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
172
+ *
173
+ * @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
174
+ * @returns {Promise<void>}
175
+ * @throws {Error} If any path is outside builder root (path traversal attempt)
176
+ */
177
+ async function cleanBuilderAppDirs(appNames) {
178
+ if (!Array.isArray(appNames) || appNames.length === 0) return;
179
+ const builderRoot = path.resolve(pathsUtil.getBuilderRoot());
180
+ for (const appName of appNames) {
181
+ if (!appName || typeof appName !== 'string') continue;
182
+ const appPath = path.resolve(pathsUtil.getBuilderPath(appName));
183
+ if (!appPath.startsWith(builderRoot + path.sep) && appPath !== builderRoot) {
184
+ throw new Error(`Path ${appPath} is outside builder root ${builderRoot}; refusing to clean`);
185
+ }
186
+ if (fs.existsSync(appPath)) {
187
+ fs.rmSync(appPath, { recursive: true });
188
+ logger.log(chalk.blue(`Cleaned builder/${appName}`));
189
+ }
190
+ }
191
+ }
192
+
168
193
  /**
169
194
  * Ensures builder app directory exists from template if application config is missing.
170
195
  * If builder/<appName>/application config does not exist, copies from templates/applications/<appName>.
@@ -189,7 +214,7 @@ async function ensureAppFromTemplate(appName) {
189
214
  const primaryCopied = await ensureTemplateAtPath(appName, appPath);
190
215
  if (primaryCopied) {
191
216
  logger.log(chalk.blue(`Creating builder/${appName} from template...`));
192
- logger.log(chalk.green(`✓ Copied template for ${appName}`));
217
+ logger.log(formatSuccessLine(`Copied template for ${appName}`));
193
218
  }
194
219
 
195
220
  const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
@@ -197,7 +222,7 @@ async function ensureAppFromTemplate(appName) {
197
222
  const cwdCopied = await ensureTemplateAtPath(appName, cwdBuilderPath);
198
223
  if (cwdCopied) {
199
224
  logger.log(chalk.blue(`Creating builder/${appName} in project (from template)...`));
200
- logger.log(chalk.green(`✓ Copied template for ${appName} into builder/`));
225
+ logger.log(formatSuccessLine(`Copied template for ${appName} into builder/`));
201
226
  }
202
227
  }
203
228
 
@@ -206,6 +231,7 @@ async function ensureAppFromTemplate(appName) {
206
231
  }
207
232
 
208
233
  module.exports = {
234
+ cleanBuilderAppDirs,
209
235
  ensureAppFromTemplate,
210
236
  patchEnvOutputPathForDeployOnly,
211
237
  validateEnvOutputPathFolderOrNull,