@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
@@ -0,0 +1,107 @@
1
+ const chalk = require('chalk');
2
+ const { formatSuccessLine } = require('./cli-test-layout-chalk');
3
+ const logger = require('./logger');
4
+ const { execWithDockerEnv } = require('./docker-exec');
5
+
6
+ /**
7
+ * Checks if db-init container exists.
8
+ * @async
9
+ * @param {string} dbInitContainer
10
+ * @returns {Promise<boolean>}
11
+ */
12
+ async function checkDbInitContainerExists(dbInitContainer) {
13
+ try {
14
+ const { stdout } = await execWithDockerEnv(
15
+ `docker ps -a --filter "name=${dbInitContainer}" --format "{{.Names}}"`
16
+ );
17
+ return stdout.trim() === dbInitContainer;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Gets container exit code.
25
+ * @async
26
+ * @param {string} dbInitContainer
27
+ * @returns {Promise<string>}
28
+ */
29
+ async function getContainerExitCode(dbInitContainer) {
30
+ const { stdout: exitCode } = await execWithDockerEnv(
31
+ `docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`
32
+ );
33
+ return exitCode.trim();
34
+ }
35
+
36
+ /**
37
+ * Handles exited container status.
38
+ * @async
39
+ * @param {string} dbInitContainer
40
+ * @returns {Promise<boolean>}
41
+ */
42
+ async function handleExitedContainer(dbInitContainer) {
43
+ const { stdout: status } = await execWithDockerEnv(
44
+ `docker inspect --format='{{.State.Status}}' ${dbInitContainer}`
45
+ );
46
+ if (status.trim() === 'exited') {
47
+ const exitCode = await getContainerExitCode(dbInitContainer);
48
+ if (exitCode === '0') {
49
+ logger.log(formatSuccessLine('Database initialization already completed'));
50
+ } else {
51
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
52
+ }
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /**
59
+ * Waits for container to exit (best-effort).
60
+ * @async
61
+ * @param {string} dbInitContainer
62
+ * @param {number} maxAttempts
63
+ * @returns {Promise<void>}
64
+ */
65
+ async function waitForContainerExit(dbInitContainer, maxAttempts) {
66
+ for (let attempts = 0; attempts < maxAttempts; attempts++) {
67
+ const { stdout: currentStatus } = await execWithDockerEnv(
68
+ `docker inspect --format='{{.State.Status}}' ${dbInitContainer}`
69
+ );
70
+ if (currentStatus.trim() === 'exited') {
71
+ const exitCode = await getContainerExitCode(dbInitContainer);
72
+ if (exitCode === '0') {
73
+ logger.log(formatSuccessLine('Database initialization completed'));
74
+ } else {
75
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
76
+ }
77
+ return;
78
+ }
79
+ await new Promise(resolve => setTimeout(resolve, 1000));
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Checks if db-init container exists and waits for it to complete.
85
+ * @async
86
+ * @param {string} appName
87
+ * @returns {Promise<void>}
88
+ */
89
+ async function waitForDbInit(appName) {
90
+ const dbInitContainer = `aifabrix-${appName}-db-init`;
91
+ try {
92
+ if (!(await checkDbInitContainerExists(dbInitContainer))) {
93
+ return;
94
+ }
95
+
96
+ if (await handleExitedContainer(dbInitContainer)) {
97
+ return;
98
+ }
99
+
100
+ logger.log(chalk.blue('Waiting for database initialization to complete...'));
101
+ await waitForContainerExit(dbInitContainer, 30);
102
+ } catch {
103
+ // db-init container might not exist, which is fine
104
+ }
105
+ }
106
+
107
+ module.exports = { waitForDbInit };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Post-success warnings when public/Traefik health URL was skipped or not verified.
3
+ *
4
+ * @fileoverview Split from health-check.js for file size limits
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('./logger');
11
+
12
+ /**
13
+ * When Traefik URL is set, drop it if the hostname does not resolve; retain full URL for UX.
14
+ *
15
+ * @param {string} traefikUrl
16
+ * @param {boolean} debug
17
+ * @param {Function} isHostnameResolvableFn - (hostname, debug) => Promise<boolean>
18
+ * @returns {Promise<{ traefikUrl: string, skippedPublicHealthUrl: string }>}
19
+ */
20
+ async function filterTraefikUrlByDns(traefikUrl, debug, isHostnameResolvableFn) {
21
+ if (!traefikUrl) {
22
+ return { traefikUrl: '', skippedPublicHealthUrl: '' };
23
+ }
24
+ try {
25
+ const hn = new URL(traefikUrl).hostname;
26
+ const ok = await isHostnameResolvableFn(hn, debug);
27
+ if (!ok) {
28
+ return { traefikUrl: '', skippedPublicHealthUrl: traefikUrl };
29
+ }
30
+ } catch {
31
+ return { traefikUrl: '', skippedPublicHealthUrl: '' };
32
+ }
33
+ return { traefikUrl, skippedPublicHealthUrl: '' };
34
+ }
35
+
36
+ /**
37
+ * @param {Object} p
38
+ * @param {string} [p.skippedPublicHealthUrl]
39
+ * @param {string[]} [p.urlsToTry]
40
+ * @param {number} p.resolvedIndex
41
+ */
42
+ function logPublicHealthUrlWarningIfNeeded(p) {
43
+ const { skippedPublicHealthUrl, urlsToTry, resolvedIndex } = p;
44
+ if (typeof resolvedIndex !== 'number' || resolvedIndex < 0) return;
45
+
46
+ if (skippedPublicHealthUrl) {
47
+ logger.log(
48
+ chalk.yellow(
49
+ `⚠ Public URL was not verified (DNS): ${skippedPublicHealthUrl}. ` +
50
+ 'The application reported healthy via localhost only. Validate DNS names and Traefik routing for this host.'
51
+ )
52
+ );
53
+ return;
54
+ }
55
+ if (Array.isArray(urlsToTry) && urlsToTry.length > 1 && resolvedIndex > 0) {
56
+ const pub = urlsToTry[0];
57
+ logger.log(
58
+ chalk.yellow(
59
+ `⚠ Public URL was not verified: ${pub}. ` +
60
+ 'Health checks succeeded via localhost only. Validate Traefik routing, TLS, and DNS.'
61
+ )
62
+ );
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ filterTraefikUrlByDns,
68
+ logPublicHealthUrlWarningIfNeeded
69
+ };
@@ -63,9 +63,8 @@ function frontDoorPattern(appConfig) {
63
63
  }
64
64
 
65
65
  /**
66
- * Compute the Traefik front-door health check URL when applicable.
67
- *
68
- * Returns null when Traefik/frontDoorRouting isn't active or cannot be resolved.
66
+ * Public app URL (Traefik + frontDoor mount path), without the health path for CLI summaries.
67
+ * Requires infra Traefik on, `frontDoorRouting.enabled`, host, and pattern.
69
68
  *
70
69
  * @async
71
70
  * @param {string} appName
@@ -73,8 +72,8 @@ function frontDoorPattern(appConfig) {
73
72
  * @param {Object|null} appConfig
74
73
  * @returns {Promise<string|null>}
75
74
  */
76
- async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig) {
77
- if (!frontDoorEnabled(appConfig)) return null;
75
+ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig) {
76
+ if (!appConfig || !frontDoorEnabled(appConfig)) return null;
78
77
  const pattern = frontDoorPattern(appConfig);
79
78
  if (!pattern) return null;
80
79
 
@@ -82,14 +81,20 @@ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig)
82
81
  const userCfg = await coreConfig.getConfig();
83
82
  if (!(userCfg && userCfg.traefik)) return null;
84
83
 
84
+ const fd = appConfig.frontDoorRouting;
85
+ if (!fd || typeof fd !== 'object') return null;
86
+ const hostTemplate = typeof fd.host === 'string' ? fd.host.trim() : '';
87
+ if (!hostTemplate) return null;
88
+
89
+ const mountPath = normalizeFrontDoorPatternForHealth(pattern);
90
+
85
91
  const infraTlsEnabled = Boolean(userCfg && userCfg.tlsEnabled);
86
92
  const remoteServer = await coreConfig.getRemoteServer();
87
93
  const developerIdRaw = await coreConfig.getDeveloperId();
88
94
  const developerIdNum = parseDeveloperIdNum(developerIdRaw);
89
95
 
90
- // Health checks originate from the CLI on the host, not from inside a container.
96
+ // URLs are resolved for the CLI on the host, not from inside a container.
91
97
  const profile = 'local';
92
- const fd = appConfig.frontDoorRouting;
93
98
  const listenPort = Number(appConfig?.port || 3000);
94
99
 
95
100
  const publicBase = computePublicUrlBaseString({
@@ -105,15 +110,28 @@ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig)
105
110
  infraTlsEnabled
106
111
  });
107
112
 
113
+ return joinUrlPath(publicBase, mountPath);
114
+ }
115
+
116
+ /**
117
+ * Traefik public URL including health path.
118
+ *
119
+ * @async
120
+ * @param {string} appName
121
+ * @param {number} healthCheckPort
122
+ * @param {Object|null} appConfig
123
+ * @returns {Promise<string|null>}
124
+ */
125
+ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig) {
126
+ const baseWithFrontDoor = await computeTraefikPublicAppUrl(appName, healthCheckPort, appConfig);
127
+ if (!baseWithFrontDoor) return null;
108
128
  const healthCheckPath = appConfig?.healthCheck?.path || '/health';
109
- const mountPath = normalizeFrontDoorPatternForHealth(pattern);
110
- const baseWithFrontDoor = joinUrlPath(publicBase, mountPath);
111
129
  return joinUrlPath(baseWithFrontDoor, healthCheckPath);
112
130
  }
113
131
 
114
132
  module.exports = {
115
133
  joinUrlPath,
116
134
  normalizeFrontDoorPatternForHealth,
135
+ computeTraefikPublicAppUrl,
117
136
  computeTraefikHealthCheckUrl
118
137
  };
119
-
@@ -12,16 +12,25 @@ const { formatSuccessLine } = require('./cli-test-layout-chalk');
12
12
  const http = require('http');
13
13
  const https = require('https');
14
14
  const net = require('net');
15
+ const dns = require('dns');
15
16
  const chalk = require('chalk');
16
17
  const logger = require('./logger');
17
18
  const { execWithDockerEnv } = require('./docker-exec');
18
- const { computeTraefikHealthCheckUrl } = require('./health-check-url');
19
+ /** Namespace require so tests can `jest.spyOn` on `./health-check-url` exports. */
20
+ const healthCheckUrl = require('./health-check-url');
21
+ const { computePathActive } = require('./url-declarative-url-flags');
22
+ const { isFrontDoorRoutingEnabledInDoc } = require('./url-declarative-vdir-inactive-env');
23
+ const { waitForDbInit } = require('./health-check-db-init');
24
+ const {
25
+ filterTraefikUrlByDns,
26
+ logPublicHealthUrlWarningIfNeeded
27
+ } = require('./health-check-public-warn');
19
28
 
20
29
  /**
21
30
  * Compute the health check URL for an app.
22
31
  *
23
- * - Default (no Traefik front-door): http://localhost:<port><healthPath>
24
- * - With Traefik + app frontDoorRouting.enabled: <publicBase><frontDoorRouting.pattern><healthPath>
32
+ * - Default (path inactive): http://localhost:<port><healthPath> (e.g. Keycloak with KC_HTTP_RELATIVE_PATH=/)
33
+ * - Path active (Traefik on frontDoorRouting.enabled): localhost probe uses same vdir as the container (e.g. /auth/health/ready)
25
34
  *
26
35
  * @async
27
36
  * @param {string} appName
@@ -29,117 +38,68 @@ const { computeTraefikHealthCheckUrl } = require('./health-check-url');
29
38
  * @param {Object|null} appConfig
30
39
  * @param {Object} opts
31
40
  * @param {Object} [opts.runOptions] - runApp options (may include env + effectiveEnvironmentScopedResources)
41
+ * @param {boolean} [opts.skipTraefikPublicUrl] - Omit Traefik URL (localhost leg in dual-probe flow only).
32
42
  * @returns {Promise<string>}
33
43
  */
34
44
  async function computeHealthCheckUrl(appName, healthCheckPort, appConfig, _opts = {}) {
35
- const healthCheckPath = appConfig?.healthCheck?.path || '/health';
45
+ const rawHealthPath = appConfig?.healthCheck?.path || '/health';
36
46
 
37
- // Traefik front-door branch: probe via resolved public host + path (e.g. https://dev02.builder02.local/auth/health/ready)
38
- try {
39
- const traefikUrl = await computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig);
40
- if (traefikUrl) return traefikUrl;
41
- } catch {
42
- // If any resolver step fails, fall back to localhost probing.
43
- }
44
-
45
- // Default: probe local published port.
46
- return `http://localhost:${healthCheckPort}${healthCheckPath}`;
47
- }
48
-
49
- /**
50
- * Checks if db-init container exists and waits for it to complete
51
- * @async
52
- * @function waitForDbInit
53
- * @param {string} appName - Application name
54
- * @throws {Error} If db-init fails
55
- */
56
- /**
57
- * Checks if db-init container exists
58
- * @async
59
- * @function checkDbInitContainerExists
60
- * @param {string} dbInitContainer - Container name
61
- * @returns {Promise<boolean>} True if container exists
62
- */
63
- async function checkDbInitContainerExists(dbInitContainer) {
64
- try {
65
- const { stdout } = await execWithDockerEnv(`docker ps -a --filter "name=${dbInitContainer}" --format "{{.Names}}"`);
66
- return stdout.trim() === dbInitContainer;
67
- } catch {
68
- return false;
69
- }
70
- }
71
-
72
- /**
73
- * Gets container exit code
74
- * @async
75
- * @function getContainerExitCode
76
- * @param {string} dbInitContainer - Container name
77
- * @returns {Promise<string>} Exit code
78
- */
79
- async function getContainerExitCode(dbInitContainer) {
80
- const { stdout: exitCode } = await execWithDockerEnv(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
81
- return exitCode.trim();
82
- }
83
-
84
- /**
85
- * Handles exited container status
86
- * @async
87
- * @function handleExitedContainer
88
- * @param {string} dbInitContainer - Container name
89
- * @returns {Promise<boolean>} True if handled (container already exited)
90
- */
91
- async function handleExitedContainer(dbInitContainer) {
92
- const { stdout: status } = await execWithDockerEnv(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
93
- if (status.trim() === 'exited') {
94
- const exitCode = await getContainerExitCode(dbInitContainer);
95
- if (exitCode === '0') {
96
- logger.log(formatSuccessLine('Database initialization already completed'));
97
- } else {
98
- logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
47
+ function computeLocalhostHealthPath() {
48
+ // Plan 124 pathActive: prepend front-door pattern when Traefik on; keep bare /health for miso/dataplane style.
49
+ try {
50
+ const runOptions = _opts && typeof _opts === 'object' && _opts.runOptions ? _opts.runOptions : null;
51
+ const traefikOn = Boolean(runOptions && runOptions.traefikEnabled === true);
52
+ const fd = appConfig && appConfig.frontDoorRouting ? appConfig.frontDoorRouting : null;
53
+ const pattern = fd && typeof fd.pattern === 'string' ? fd.pattern : null;
54
+ const pathActive = computePathActive(traefikOn, isFrontDoorRoutingEnabledInDoc(appConfig || null));
55
+ const shouldMount = pathActive && Boolean(pattern) && rawHealthPath !== '/health';
56
+ if (!shouldMount) return rawHealthPath;
57
+ const { joinUrlPath, normalizeFrontDoorPatternForHealth } = require('./health-check-url');
58
+ const mountPath = normalizeFrontDoorPatternForHealth(pattern);
59
+ return joinUrlPath(mountPath, rawHealthPath);
60
+ } catch {
61
+ return rawHealthPath;
99
62
  }
100
- return true;
101
63
  }
102
- return false;
103
- }
104
64
 
105
- /**
106
- * Waits for container to exit
107
- * @async
108
- * @function waitForContainerExit
109
- * @param {string} dbInitContainer - Container name
110
- * @param {number} maxAttempts - Maximum attempts
111
- */
112
- async function waitForContainerExit(dbInitContainer, maxAttempts) {
113
- for (let attempts = 0; attempts < maxAttempts; attempts++) {
114
- const { stdout: currentStatus } = await execWithDockerEnv(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
115
- if (currentStatus.trim() === 'exited') {
116
- const exitCode = await getContainerExitCode(dbInitContainer);
117
- if (exitCode === '0') {
118
- logger.log(formatSuccessLine('Database initialization completed'));
119
- } else {
120
- logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
121
- }
122
- return;
65
+ const localhostHealthPath = computeLocalhostHealthPath();
66
+
67
+ // Local readiness probes localhost; optional Traefik URL is for display / dual-probe (see waitForHealthCheck).
68
+ async function maybeGetTraefikUrl() {
69
+ if (_opts && _opts.skipTraefikPublicUrl) return '';
70
+ const runOptions = (_opts && typeof _opts === 'object') ? _opts.runOptions : null;
71
+ const wantsTraefik =
72
+ Boolean(runOptions) &&
73
+ (runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true) &&
74
+ isFrontDoorRoutingEnabledInDoc(appConfig);
75
+ if (!wantsTraefik) return '';
76
+ try {
77
+ return await healthCheckUrl.computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig);
78
+ } catch {
79
+ return '';
123
80
  }
124
- await new Promise(resolve => setTimeout(resolve, 1000));
125
81
  }
82
+
83
+ const traefikUrl = await maybeGetTraefikUrl();
84
+ if (traefikUrl) return traefikUrl;
85
+
86
+ return `http://localhost:${healthCheckPort}${localhostHealthPath}`;
126
87
  }
127
88
 
128
- async function waitForDbInit(appName) {
129
- const dbInitContainer = `aifabrix-${appName}-db-init`;
89
+ async function isHostnameResolvable(hostname, debug) {
90
+ if (!hostname) return false;
91
+ const hn = String(hostname).trim().toLowerCase();
92
+ if (!hn) return false;
93
+ if (hn === 'localhost' || hn === '127.0.0.1' || hn === '::1') return true;
130
94
  try {
131
- if (!(await checkDbInitContainerExists(dbInitContainer))) {
132
- return;
133
- }
134
-
135
- if (await handleExitedContainer(dbInitContainer)) {
136
- return;
95
+ await dns.promises.lookup(hn);
96
+ return true;
97
+ } catch (err) {
98
+ // ENOTFOUND: caller may log a single post-success warning with the full public health URL.
99
+ if (debug && !(err && err.code === 'ENOTFOUND')) {
100
+ logger.log(chalk.gray(`[DEBUG] DNS lookup failed for ${hostname}: ${err.message}`));
137
101
  }
138
-
139
- logger.log(chalk.blue('Waiting for database initialization to complete...'));
140
- await waitForContainerExit(dbInitContainer, 30);
141
- } catch (error) {
142
- // db-init container might not exist, which is fine
102
+ return false;
143
103
  }
144
104
  }
145
105
 
@@ -411,24 +371,96 @@ async function performHealthCheckAttempt(healthCheckUrl, attempt, maxAttempts, d
411
371
  return false;
412
372
  }
413
373
 
374
+ async function computePreferredHealthCheckUrls(appName, healthCheckPort, config, runOptions, debug) {
375
+ const localhostUrl = await computeHealthCheckUrl(appName, healthCheckPort, config, {
376
+ runOptions: runOptions && typeof runOptions === 'object' ? runOptions : {},
377
+ skipTraefikPublicUrl: true
378
+ });
379
+
380
+ let traefikUrl = '';
381
+ /** Full Traefik/public health URL when DNS fails — used for one post-success warning. */
382
+ let skippedPublicHealthUrl = '';
383
+ const wantsTraefikFirst = Boolean(
384
+ runOptions && (runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true)
385
+ ) && isFrontDoorRoutingEnabledInDoc(config);
386
+ if (wantsTraefikFirst) {
387
+ try {
388
+ traefikUrl = await healthCheckUrl.computeTraefikHealthCheckUrl(appName, healthCheckPort, config);
389
+ } catch {
390
+ traefikUrl = '';
391
+ }
392
+ }
393
+
394
+ const filtered = await filterTraefikUrlByDns(traefikUrl, debug, isHostnameResolvable);
395
+ traefikUrl = filtered.traefikUrl;
396
+ skippedPublicHealthUrl = filtered.skippedPublicHealthUrl;
397
+
398
+ const urlsToTry = traefikUrl ? [traefikUrl, localhostUrl] : [localhostUrl];
399
+ if (urlsToTry.length > 1) {
400
+ logger.log(
401
+ chalk.gray(
402
+ `ℹ Health check order: Traefik/DNS (${urlsToTry[0]}), then localhost (${urlsToTry[1]}).`
403
+ )
404
+ );
405
+ }
406
+ if (debug) {
407
+ logger.log(chalk.gray(`[DEBUG] Health check URLs: ${urlsToTry.join(' | ')}`));
408
+ }
409
+ return { urlsToTry, skippedPublicHealthUrl };
410
+ }
411
+
412
+ async function performHealthCheckAttemptForUrls(urlsToTry, attempt, maxAttempts, debug) {
413
+ for (let i = 0; i < urlsToTry.length; i++) {
414
+ const url = urlsToTry[i];
415
+ const passed = await performHealthCheckAttempt(url, attempt, maxAttempts, debug);
416
+ if (passed) {
417
+ return { ok: true, resolvedIndex: i };
418
+ }
419
+ }
420
+ return { ok: false, resolvedIndex: -1 };
421
+ }
422
+
414
423
  async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
415
424
  await waitForDbInit(appName);
416
425
 
417
426
  const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
418
427
  const { maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
419
- const healthCheckUrl = await computeHealthCheckUrl(appName, healthCheckPort, config, { runOptions });
420
- if (debug) {
421
- logger.log(chalk.gray(`[DEBUG] Health check URL: ${healthCheckUrl}`));
428
+ const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
429
+ appName,
430
+ healthCheckPort,
431
+ config,
432
+ runOptions,
433
+ debug
434
+ );
435
+
436
+ if (skippedPublicHealthUrl && urlsToTry.length === 1) {
437
+ logger.log(
438
+ chalk.gray(
439
+ `ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
440
+ `Probing ${urlsToTry[0]} only until the app responds.`
441
+ )
442
+ );
422
443
  }
423
444
 
424
445
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
425
- const passed = await performHealthCheckAttempt(healthCheckUrl, attempts, maxAttempts, debug);
426
- if (passed) {
446
+ const attemptResult = await performHealthCheckAttemptForUrls(urlsToTry, attempts, maxAttempts, debug);
447
+ if (attemptResult.ok) {
448
+ logPublicHealthUrlWarningIfNeeded({
449
+ skippedPublicHealthUrl,
450
+ urlsToTry,
451
+ resolvedIndex: attemptResult.resolvedIndex
452
+ });
427
453
  return;
428
454
  }
429
455
 
430
456
  if (attempts < maxAttempts - 1) {
431
- logger.log(chalk.yellow(`Waiting for health check... (${attempts + 1}/${maxAttempts}) ${healthCheckUrl}`));
457
+ const probeHint =
458
+ urlsToTry.length > 1
459
+ ? `trying ${urlsToTry[0]}, then ${urlsToTry[1]}`
460
+ : (urlsToTry[0] || 'health URL');
461
+ logger.log(
462
+ chalk.yellow(`Waiting for health check… (${attempts + 1}/${maxAttempts}) (${probeHint})`)
463
+ );
432
464
  await new Promise(resolve => setTimeout(resolve, 2000));
433
465
  }
434
466
  }
@@ -20,6 +20,8 @@ const CATEGORIES = [
20
20
  {
21
21
  name: 'Infrastructure (Local Development)',
22
22
  commands: [
23
+ { name: 'setup' },
24
+ { name: 'teardown' },
23
25
  { name: 'up-infra' },
24
26
  { name: 'up-platform' },
25
27
  { name: 'up-miso' },
@@ -72,7 +74,7 @@ const CATEGORIES = [
72
74
  { name: 'app' },
73
75
  { name: 'credential' },
74
76
  { name: 'deployment' },
75
- { name: 'service-user' }
77
+ { name: 'integration-client' }
76
78
  ]
77
79
  },
78
80
  {
@@ -96,6 +98,8 @@ const CATEGORIES = [
96
98
  { name: 'delete', term: 'delete <systemKey>' },
97
99
  { name: 'repair', term: 'repair <systemKey>' },
98
100
  { name: 'datasource' },
101
+ { name: 'dimension' },
102
+ { name: 'dimension-value' },
99
103
  { name: 'test', term: 'test <app>' },
100
104
  { name: 'test-e2e', term: 'test-e2e <app>' },
101
105
  { name: 'test-integration', term: 'test-integration <app>' }
@@ -11,18 +11,18 @@
11
11
 
12
12
  /**
13
13
  * Builds a developer-scoped image name for local Docker builds.
14
- * Format: "<base>-dev<developerId>".
15
- * If developerId is missing, non-numeric, or 0 → "<base>-extra".
14
+ * Format: "<base>-dev<developerId>" when developerId is a positive integer.
15
+ * If developerId is missing, non-numeric, or 0 → returns baseName (manifest image; no dev suffix).
16
16
  *
17
17
  * @function buildDevImageName
18
18
  * @param {string} baseName - Base image name (no registry), e.g., "myapp"
19
19
  * @param {(string|number|null|undefined)} developerId - Developer identifier
20
- * @returns {string} Developer-scoped image name
20
+ * @returns {string} Developer-scoped image name or base name when id is 0 / absent
21
21
  *
22
22
  * @example
23
23
  * buildDevImageName('myapp', 123) // "myapp-dev123"
24
- * buildDevImageName('myapp', '0') // "myapp-extra"
25
- * buildDevImageName('myapp') // "myapp-extra"
24
+ * buildDevImageName('myapp', '0') // "myapp"
25
+ * buildDevImageName('myapp') // "myapp"
26
26
  */
27
27
  function buildDevImageName(baseName, developerId) {
28
28
  const id =
@@ -37,13 +37,40 @@ function buildDevImageName(baseName, developerId) {
37
37
  }
38
38
 
39
39
  if (!Number.isFinite(id) || id === 0) {
40
- return `${baseName}-extra`;
40
+ return baseName;
41
41
  }
42
42
 
43
43
  return `${baseName}-dev${id}`;
44
44
  }
45
45
 
46
+ /**
47
+ * Developer-scoped repository path for run/build resolution (may include registry prefix).
48
+ * For qualified paths (slashes), only the last segment gets `-dev<id>`; id 0 returns path unchanged.
49
+ *
50
+ * @param {string} repositoryPath - Full repository path (e.g. "reg/ns/app" or "app")
51
+ * @param {(string|number|null|undefined)} developerId - Developer id from config
52
+ * @returns {string}
53
+ */
54
+ function buildDevImageRepositoryPath(repositoryPath, developerId) {
55
+ if (!repositoryPath || typeof repositoryPath !== 'string') {
56
+ throw new Error('Repository path is required and must be a string');
57
+ }
58
+ const idNum =
59
+ typeof developerId === 'number' ? developerId : parseInt(String(developerId), 10);
60
+ if (!Number.isFinite(idNum) || idNum === 0) {
61
+ return repositoryPath;
62
+ }
63
+ const idx = repositoryPath.lastIndexOf('/');
64
+ const tail = idx === -1 ? repositoryPath : repositoryPath.slice(idx + 1);
65
+ const scopedTail = buildDevImageName(tail, developerId);
66
+ if (idx === -1) {
67
+ return scopedTail;
68
+ }
69
+ return `${repositoryPath.slice(0, idx)}/${scopedTail}`;
70
+ }
71
+
46
72
  module.exports = {
47
- buildDevImageName
73
+ buildDevImageName,
74
+ buildDevImageRepositoryPath
48
75
  };
49
76