@aifabrix/builder 2.44.4 → 2.44.6

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 (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  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 +44 -12
  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 +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -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
+ };
@@ -73,7 +73,16 @@ function frontDoorPattern(appConfig) {
73
73
  * @param {Object|null} appConfig
74
74
  * @returns {Promise<string|null>}
75
75
  */
76
- async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig) {
76
+ /**
77
+ * Public app URL (Traefik + frontDoor mount path), without the health path — for CLI summaries.
78
+ *
79
+ * @async
80
+ * @param {string} appName
81
+ * @param {number} healthCheckPort
82
+ * @param {Object|null} appConfig
83
+ * @returns {Promise<string|null>}
84
+ */
85
+ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig) {
77
86
  if (!frontDoorEnabled(appConfig)) return null;
78
87
  const pattern = frontDoorPattern(appConfig);
79
88
  if (!pattern) return null;
@@ -87,7 +96,7 @@ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig)
87
96
  const developerIdRaw = await coreConfig.getDeveloperId();
88
97
  const developerIdNum = parseDeveloperIdNum(developerIdRaw);
89
98
 
90
- // Health checks originate from the CLI on the host, not from inside a container.
99
+ // URLs are resolved for the CLI on the host, not from inside a container.
91
100
  const profile = 'local';
92
101
  const fd = appConfig.frontDoorRouting;
93
102
  const listenPort = Number(appConfig?.port || 3000);
@@ -105,15 +114,21 @@ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig)
105
114
  infraTlsEnabled
106
115
  });
107
116
 
108
- const healthCheckPath = appConfig?.healthCheck?.path || '/health';
109
117
  const mountPath = normalizeFrontDoorPatternForHealth(pattern);
110
- const baseWithFrontDoor = joinUrlPath(publicBase, mountPath);
118
+ return joinUrlPath(publicBase, mountPath);
119
+ }
120
+
121
+ async function computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig) {
122
+ const baseWithFrontDoor = await computeTraefikPublicAppUrl(appName, healthCheckPort, appConfig);
123
+ if (!baseWithFrontDoor) return null;
124
+ const healthCheckPath = appConfig?.healthCheck?.path || '/health';
111
125
  return joinUrlPath(baseWithFrontDoor, healthCheckPath);
112
126
  }
113
127
 
114
128
  module.exports = {
115
129
  joinUrlPath,
116
130
  normalizeFrontDoorPatternForHealth,
131
+ computeTraefikPublicAppUrl,
117
132
  computeTraefikHealthCheckUrl
118
133
  };
119
134
 
@@ -12,16 +12,24 @@ 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
19
  const { computeTraefikHealthCheckUrl } = require('./health-check-url');
20
+ const { computePathActive } = require('./url-declarative-url-flags');
21
+ const { isFrontDoorRoutingEnabledInDoc } = require('./url-declarative-vdir-inactive-env');
22
+ const { waitForDbInit } = require('./health-check-db-init');
23
+ const {
24
+ filterTraefikUrlByDns,
25
+ logPublicHealthUrlWarningIfNeeded
26
+ } = require('./health-check-public-warn');
19
27
 
20
28
  /**
21
29
  * Compute the health check URL for an app.
22
30
  *
23
- * - Default (no Traefik front-door): http://localhost:<port><healthPath>
24
- * - With Traefik + app frontDoorRouting.enabled: <publicBase><frontDoorRouting.pattern><healthPath>
31
+ * - Default (path inactive): http://localhost:<port><healthPath> (e.g. Keycloak with KC_HTTP_RELATIVE_PATH=/)
32
+ * - Path active (Traefik on frontDoorRouting.enabled): localhost probe uses same vdir as the container (e.g. /auth/health/ready)
25
33
  *
26
34
  * @async
27
35
  * @param {string} appName
@@ -29,117 +37,67 @@ const { computeTraefikHealthCheckUrl } = require('./health-check-url');
29
37
  * @param {Object|null} appConfig
30
38
  * @param {Object} opts
31
39
  * @param {Object} [opts.runOptions] - runApp options (may include env + effectiveEnvironmentScopedResources)
40
+ * @param {boolean} [opts.skipTraefikPublicUrl] - Omit Traefik URL (localhost leg in dual-probe flow only).
32
41
  * @returns {Promise<string>}
33
42
  */
34
43
  async function computeHealthCheckUrl(appName, healthCheckPort, appConfig, _opts = {}) {
35
- const healthCheckPath = appConfig?.healthCheck?.path || '/health';
44
+ const rawHealthPath = appConfig?.healthCheck?.path || '/health';
36
45
 
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.
46
+ function computeLocalhostHealthPath() {
47
+ // Plan 124 pathActive: prepend front-door pattern when Traefik on; keep bare /health for miso/dataplane style.
48
+ try {
49
+ const runOptions = _opts && typeof _opts === 'object' && _opts.runOptions ? _opts.runOptions : null;
50
+ const traefikOn = Boolean(runOptions && runOptions.traefikEnabled === true);
51
+ const fd = appConfig && appConfig.frontDoorRouting ? appConfig.frontDoorRouting : null;
52
+ const pattern = fd && typeof fd.pattern === 'string' ? fd.pattern : null;
53
+ const pathActive = computePathActive(traefikOn, isFrontDoorRoutingEnabledInDoc(appConfig || null));
54
+ const shouldMount = pathActive && Boolean(pattern) && rawHealthPath !== '/health';
55
+ if (!shouldMount) return rawHealthPath;
56
+ const { joinUrlPath, normalizeFrontDoorPatternForHealth } = require('./health-check-url');
57
+ const mountPath = normalizeFrontDoorPatternForHealth(pattern);
58
+ return joinUrlPath(mountPath, rawHealthPath);
59
+ } catch {
60
+ return rawHealthPath;
61
+ }
43
62
  }
44
63
 
45
- // Default: probe local published port.
46
- return `http://localhost:${healthCheckPort}${healthCheckPath}`;
47
- }
64
+ const localhostHealthPath = computeLocalhostHealthPath();
48
65
 
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}`));
66
+ // Local readiness probes localhost; optional Traefik URL is for display / dual-probe (see waitForHealthCheck).
67
+ async function maybeGetTraefikUrl() {
68
+ if (_opts && _opts.skipTraefikPublicUrl) return '';
69
+ const runOptions = (_opts && typeof _opts === 'object') ? _opts.runOptions : null;
70
+ const wantsTraefik =
71
+ Boolean(runOptions) &&
72
+ (runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true);
73
+ if (!wantsTraefik) return '';
74
+ try {
75
+ return await computeTraefikHealthCheckUrl(appName, healthCheckPort, appConfig);
76
+ } catch {
77
+ return '';
99
78
  }
100
- return true;
101
79
  }
102
- return false;
103
- }
104
80
 
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;
123
- }
124
- await new Promise(resolve => setTimeout(resolve, 1000));
125
- }
81
+ const traefikUrl = await maybeGetTraefikUrl();
82
+ if (traefikUrl) return traefikUrl;
83
+
84
+ return `http://localhost:${healthCheckPort}${localhostHealthPath}`;
126
85
  }
127
86
 
128
- async function waitForDbInit(appName) {
129
- const dbInitContainer = `aifabrix-${appName}-db-init`;
87
+ async function isHostnameResolvable(hostname, debug) {
88
+ if (!hostname) return false;
89
+ const hn = String(hostname).trim().toLowerCase();
90
+ if (!hn) return false;
91
+ if (hn === 'localhost' || hn === '127.0.0.1' || hn === '::1') return true;
130
92
  try {
131
- if (!(await checkDbInitContainerExists(dbInitContainer))) {
132
- return;
133
- }
134
-
135
- if (await handleExitedContainer(dbInitContainer)) {
136
- return;
93
+ await dns.promises.lookup(hn);
94
+ return true;
95
+ } catch (err) {
96
+ // ENOTFOUND: caller may log a single post-success warning with the full public health URL.
97
+ if (debug && !(err && err.code === 'ENOTFOUND')) {
98
+ logger.log(chalk.gray(`[DEBUG] DNS lookup failed for ${hostname}: ${err.message}`));
137
99
  }
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
100
+ return false;
143
101
  }
144
102
  }
145
103
 
@@ -411,24 +369,96 @@ async function performHealthCheckAttempt(healthCheckUrl, attempt, maxAttempts, d
411
369
  return false;
412
370
  }
413
371
 
372
+ async function computePreferredHealthCheckUrls(appName, healthCheckPort, config, runOptions, debug) {
373
+ const localhostUrl = await computeHealthCheckUrl(appName, healthCheckPort, config, {
374
+ runOptions: runOptions && typeof runOptions === 'object' ? runOptions : {},
375
+ skipTraefikPublicUrl: true
376
+ });
377
+
378
+ let traefikUrl = '';
379
+ /** Full Traefik/public health URL when DNS fails — used for one post-success warning. */
380
+ let skippedPublicHealthUrl = '';
381
+ const wantsTraefikFirst = Boolean(
382
+ runOptions && (runOptions.probeViaTraefik === true || runOptions.traefikEnabled === true)
383
+ );
384
+ if (wantsTraefikFirst) {
385
+ try {
386
+ traefikUrl = await computeTraefikHealthCheckUrl(appName, healthCheckPort, config);
387
+ } catch {
388
+ traefikUrl = '';
389
+ }
390
+ }
391
+
392
+ const filtered = await filterTraefikUrlByDns(traefikUrl, debug, isHostnameResolvable);
393
+ traefikUrl = filtered.traefikUrl;
394
+ skippedPublicHealthUrl = filtered.skippedPublicHealthUrl;
395
+
396
+ const urlsToTry = traefikUrl ? [traefikUrl, localhostUrl] : [localhostUrl];
397
+ if (urlsToTry.length > 1) {
398
+ logger.log(
399
+ chalk.gray(
400
+ `ℹ Health check order: Traefik/DNS (${urlsToTry[0]}), then localhost (${urlsToTry[1]}).`
401
+ )
402
+ );
403
+ }
404
+ if (debug) {
405
+ logger.log(chalk.gray(`[DEBUG] Health check URLs: ${urlsToTry.join(' | ')}`));
406
+ }
407
+ return { urlsToTry, skippedPublicHealthUrl };
408
+ }
409
+
410
+ async function performHealthCheckAttemptForUrls(urlsToTry, attempt, maxAttempts, debug) {
411
+ for (let i = 0; i < urlsToTry.length; i++) {
412
+ const url = urlsToTry[i];
413
+ const passed = await performHealthCheckAttempt(url, attempt, maxAttempts, debug);
414
+ if (passed) {
415
+ return { ok: true, resolvedIndex: i };
416
+ }
417
+ }
418
+ return { ok: false, resolvedIndex: -1 };
419
+ }
420
+
414
421
  async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
415
422
  await waitForDbInit(appName);
416
423
 
417
424
  const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
418
425
  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}`));
426
+ const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
427
+ appName,
428
+ healthCheckPort,
429
+ config,
430
+ runOptions,
431
+ debug
432
+ );
433
+
434
+ if (skippedPublicHealthUrl && urlsToTry.length === 1) {
435
+ logger.log(
436
+ chalk.gray(
437
+ `ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
438
+ `Probing ${urlsToTry[0]} only until the app responds.`
439
+ )
440
+ );
422
441
  }
423
442
 
424
443
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
425
- const passed = await performHealthCheckAttempt(healthCheckUrl, attempts, maxAttempts, debug);
426
- if (passed) {
444
+ const attemptResult = await performHealthCheckAttemptForUrls(urlsToTry, attempts, maxAttempts, debug);
445
+ if (attemptResult.ok) {
446
+ logPublicHealthUrlWarningIfNeeded({
447
+ skippedPublicHealthUrl,
448
+ urlsToTry,
449
+ resolvedIndex: attemptResult.resolvedIndex
450
+ });
427
451
  return;
428
452
  }
429
453
 
430
454
  if (attempts < maxAttempts - 1) {
431
- logger.log(chalk.yellow(`Waiting for health check... (${attempts + 1}/${maxAttempts}) ${healthCheckUrl}`));
455
+ const probeHint =
456
+ urlsToTry.length > 1
457
+ ? `trying ${urlsToTry[0]}, then ${urlsToTry[1]}`
458
+ : (urlsToTry[0] || 'health URL');
459
+ logger.log(
460
+ chalk.yellow(`Waiting for health check… (${attempts + 1}/${maxAttempts}) (${probeHint})`)
461
+ );
432
462
  await new Promise(resolve => setTimeout(resolve, 2000));
433
463
  }
434
464
  }
@@ -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
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Timestamped backups under integration/<app>/backup/ (same layout as datasource capability copy).
3
+ *
4
+ * @fileoverview Backup before mutating integration JSON/YAML
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * True if path exists and is a regular file (not mocked by typical existsSync spies in unit tests).
16
+ * @param {string} filePath
17
+ * @returns {boolean}
18
+ */
19
+ function isRegularFile(filePath) {
20
+ try {
21
+ return fs.statSync(filePath).isFile();
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Copies filePath into neighbor backup/ with ISO timestamp suffix.
29
+ * @param {string} filePath - Absolute path to file to copy
30
+ * @param {boolean} noBackup - When true, skip and return null
31
+ * @returns {string|null} Destination path or null
32
+ */
33
+ function writeBackup(filePath, noBackup) {
34
+ if (noBackup) {
35
+ return null;
36
+ }
37
+ const dir = path.dirname(filePath);
38
+ const backupDir = path.join(dir, 'backup');
39
+ fs.mkdirSync(backupDir, { recursive: true });
40
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
41
+ const base = path.basename(filePath);
42
+ const dest = path.join(backupDir, `${base}.${ts}.bak`);
43
+ fs.copyFileSync(filePath, dest);
44
+ return dest;
45
+ }
46
+
47
+ /**
48
+ * Backs up an existing file once per repair run (dedupes by absolute path).
49
+ * @param {string} filePath - Path to file that will be overwritten
50
+ * @param {{ dryRun?: boolean, noBackup?: boolean, backupPaths?: string[], backedUpFiles?: Set<string> }} ctx
51
+ * @returns {string|null}
52
+ */
53
+ function backupIntegrationFile(filePath, ctx) {
54
+ const { dryRun, noBackup, backupPaths, backedUpFiles } = ctx || {};
55
+ if (dryRun || noBackup || !filePath || !isRegularFile(filePath)) {
56
+ return null;
57
+ }
58
+ const abs = path.resolve(filePath);
59
+ if (backedUpFiles) {
60
+ if (backedUpFiles.has(abs)) return null;
61
+ backedUpFiles.add(abs);
62
+ }
63
+ const dest = writeBackup(filePath, false);
64
+ if (dest && Array.isArray(backupPaths)) {
65
+ backupPaths.push(dest);
66
+ }
67
+ return dest;
68
+ }
69
+
70
+ module.exports = {
71
+ writeBackup,
72
+ backupIntegrationFile,
73
+ isRegularFile
74
+ };
@@ -9,6 +9,7 @@
9
9
 
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
+ const http = require('http');
12
13
  const https = require('https');
13
14
  const { getAifabrixHome } = require('./paths');
14
15
  const { exec } = require('child_process');
@@ -88,10 +89,36 @@ function fetchLatestRelease() {
88
89
  * @param {(msg: string) => void} [log] - Optional progress logger
89
90
  * @returns {Promise<void>}
90
91
  */
91
- function downloadToFile(url, destPath, log) {
92
+ /**
93
+ * @param {string} url
94
+ * @param {string} destPath
95
+ * @param {(msg: string) => void} [log]
96
+ * @param {number} [redirectsLeft]
97
+ * @returns {Promise<void>}
98
+ */
99
+ function downloadToFile(url, destPath, log, redirectsLeft = 8) {
92
100
  return new Promise((resolve, reject) => {
101
+ if (redirectsLeft <= 0) {
102
+ reject(new Error('Download redirect loop'));
103
+ return;
104
+ }
93
105
  const file = fs.createWriteStream(destPath, { flags: 'w' });
94
- const req = https.get(url, { headers: { 'User-Agent': 'aifabrix-builder-cli' } }, (res) => {
106
+ const lib = url.startsWith('http:') ? http : https;
107
+ const req = lib.get(url, { headers: { 'User-Agent': 'aifabrix-builder-cli' } }, (res) => {
108
+ const loc = res.headers.location;
109
+ if ([301, 302, 303, 307, 308].includes(res.statusCode || 0) && loc) {
110
+ file.close();
111
+ fs.unlink(destPath, () => {});
112
+ let nextUrl;
113
+ try {
114
+ nextUrl = new URL(loc, url).href;
115
+ } catch (e) {
116
+ reject(new Error(`Invalid redirect Location: ${loc}`));
117
+ return;
118
+ }
119
+ downloadToFile(nextUrl, destPath, log, redirectsLeft - 1).then(resolve).catch(reject);
120
+ return;
121
+ }
95
122
  if (res.statusCode !== 200) {
96
123
  file.close();
97
124
  fs.unlink(destPath, () => {});
@@ -111,7 +138,7 @@ function downloadToFile(url, destPath, log) {
111
138
  req.setTimeout(120000, () => {
112
139
  req.destroy(); reject(new Error('Download timeout'));
113
140
  });
114
- if (typeof log === 'function') log('Downloading Mutagen...');
141
+ if (typeof log === 'function' && redirectsLeft === 8) log('Downloading Mutagen...');
115
142
  });
116
143
  }
117
144