@aifabrix/builder 2.47.4 → 2.49.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 (93) hide show
  1. package/jest.isolated-projects.js +7 -0
  2. package/jest.projects.js +16 -0
  3. package/lib/api/applications.api.js +23 -0
  4. package/lib/api/dev.api.js +1 -1
  5. package/lib/api/index.js +5 -2
  6. package/lib/api/types/dev.types.js +4 -3
  7. package/lib/app/deploy-config.js +6 -4
  8. package/lib/app/register.js +21 -19
  9. package/lib/app/rotate-secret.js +39 -16
  10. package/lib/app/run-container-start.js +0 -9
  11. package/lib/app/run-helpers.js +8 -4
  12. package/lib/app/run-reload-sync.js +16 -18
  13. package/lib/app/run-stop-containers.js +48 -5
  14. package/lib/app/run.js +6 -3
  15. package/lib/app/show.js +2 -2
  16. package/lib/build/index.js +12 -49
  17. package/lib/build/integration-docker-staging.js +35 -0
  18. package/lib/build/standard-docker-build.js +113 -0
  19. package/lib/cli/infra-guided-auth.js +42 -6
  20. package/lib/cli/infra-guided-footers.js +164 -0
  21. package/lib/cli/infra-guided-spinners.js +57 -0
  22. package/lib/cli/infra-guided.js +41 -179
  23. package/lib/cli/setup-app.js +5 -1
  24. package/lib/cli/setup-dev.js +3 -2
  25. package/lib/cli/setup-infra.js +3 -2
  26. package/lib/cli/setup-platform.js +3 -0
  27. package/lib/commands/dev-init.js +18 -0
  28. package/lib/commands/login-device.js +6 -2
  29. package/lib/commands/login.js +4 -1
  30. package/lib/commands/setup-modes.js +19 -3
  31. package/lib/commands/setup-platform-auth.js +25 -0
  32. package/lib/commands/setup-prompts-admin.js +9 -0
  33. package/lib/commands/setup-prompts-platform-mode.js +11 -1
  34. package/lib/commands/setup.js +60 -7
  35. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  36. package/lib/commands/up-dataplane-credentials.js +104 -6
  37. package/lib/commands/up-dataplane.js +74 -11
  38. package/lib/core/secrets-env-declarative-expand.js +35 -23
  39. package/lib/core/setup-platform-mode.js +59 -9
  40. package/lib/deployment/deployer-status.js +26 -3
  41. package/lib/deployment/deployer.js +7 -14
  42. package/lib/external-system/download-helpers.js +10 -1
  43. package/lib/infrastructure/compose.js +40 -12
  44. package/lib/infrastructure/services.js +15 -3
  45. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  46. package/lib/parameters/infra-kv-discovery.js +4 -3
  47. package/lib/utils/aifabrix-runtime-config-dir.js +2 -1
  48. package/lib/utils/app-bootstrap-api.js +53 -0
  49. package/lib/utils/app-run-containers.js +14 -3
  50. package/lib/utils/applications-config-defaults.js +19 -3
  51. package/lib/utils/compose-generate-docker-compose.js +18 -6
  52. package/lib/utils/compose-generator.js +34 -15
  53. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  54. package/lib/utils/config-paths.js +8 -3
  55. package/lib/utils/dev-ca-docker-augment.js +54 -0
  56. package/lib/utils/dev-user-groups.js +1 -0
  57. package/lib/utils/docker-daemon-tls-ca.js +363 -0
  58. package/lib/utils/docker-reload-mount.js +12 -1
  59. package/lib/utils/ensure-dev-certs-for-remote-docker.js +5 -0
  60. package/lib/utils/external-env-template.js +53 -18
  61. package/lib/utils/health-check-poll-ui.js +84 -0
  62. package/lib/utils/health-check-url.js +54 -34
  63. package/lib/utils/health-check.js +43 -16
  64. package/lib/utils/infra-optional-service-flags.js +31 -2
  65. package/lib/utils/mutagen.js +7 -4
  66. package/lib/utils/platform-controller-url.js +23 -1
  67. package/lib/utils/promote-dev-docker-image.js +170 -0
  68. package/lib/utils/refresh-remote-settings.js +67 -0
  69. package/lib/utils/remote-docker-env.js +22 -7
  70. package/lib/utils/remote-host-for-front-door.js +43 -0
  71. package/lib/utils/run-cli-flags.js +37 -1
  72. package/lib/utils/secrets-helpers.js +9 -14
  73. package/lib/utils/secrets-kv-refs.js +47 -3
  74. package/lib/utils/secrets-missing-error.js +1 -9
  75. package/lib/utils/token-manager-client-credentials.js +11 -1
  76. package/lib/utils/url-declarative-public-base.js +11 -4
  77. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  78. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  79. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  80. package/lib/utils/url-declarative-resolve.js +2 -0
  81. package/lib/utils/urls-local-registry-scan.js +95 -10
  82. package/lib/utils/urls-local-registry.js +24 -1
  83. package/lib/utils/worker-sidecars.js +28 -1
  84. package/lib/validation/external-system-auth-rules.js +45 -7
  85. package/package.json +2 -1
  86. package/scripts/test-dataplane-bootstrap.js +331 -0
  87. package/templates/applications/dataplane/env.template +22 -5
  88. package/templates/applications/dataplane/rbac.yaml +11 -0
  89. package/templates/applications/miso-controller/application.yaml +1 -1
  90. package/templates/applications/miso-controller/env.template +3 -0
  91. package/templates/infra/compose.yaml.hbs +13 -9
  92. package/templates/python/docker-compose.hbs +3 -0
  93. package/templates/typescript/docker-compose.hbs +3 -0
@@ -101,6 +101,7 @@ function hostnameMatchesDockerHost(dockerHost) {
101
101
  const hnShort = hn.split('.')[0];
102
102
  const hShort = h.split('.')[0];
103
103
  if (h === hnShort || hn === hShort) return true;
104
+ if (hShort && hnShort && hShort === hnShort) return true;
104
105
  return false;
105
106
  }
106
107
 
@@ -120,8 +121,18 @@ function isReloadBindMountOnEngineHost(dockerEndpoint) {
120
121
  return hostnameMatchesDockerHost(host);
121
122
  }
122
123
 
124
+ /**
125
+ * True when host refers to this machine (loopback, FQDN, or short hostname match).
126
+ * @param {string} host
127
+ * @returns {boolean}
128
+ */
129
+ function isHostOnThisMachine(host) {
130
+ return hostnameMatchesDockerHost(host);
131
+ }
132
+
123
133
  module.exports = {
124
134
  extractHostFromDockerEndpoint,
125
135
  isReloadBindMountOnEngineHost,
126
- isLocalhostSyncSshHost
136
+ isLocalhostSyncSshHost,
137
+ isHostOnThisMachine
127
138
  };
@@ -27,6 +27,7 @@ const {
27
27
  normalizePemNewlines
28
28
  } = require('./dev-cert-helper');
29
29
  const { isSslUntrustedError, fetchInstallCa } = require('./dev-ca-install');
30
+ const { augmentAndSaveCaPemForRemoteDocker } = require('./dev-ca-docker-augment');
30
31
 
31
32
  /**
32
33
  * Read one-time issue-cert PIN from env or first non-empty line of AIFABRIX_DEV_ISSUE_PIN_FILE.
@@ -184,6 +185,10 @@ async function ensureDevCertsIfNeededForRemoteDocker() {
184
185
  const issueResponse = await requestIssueCert(ctx.baseUrl, ctx.devId, pin, csrPem, serverCaPem);
185
186
  const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
186
187
  await saveDevCertMaterial(ctx.configDir, ctx.devId, issueResponse.certificate, keyPem, caPem);
188
+ await augmentAndSaveCaPemForRemoteDocker(ctx.configDir, ctx.devId, caPem, {
189
+ remoteServerUrl: ctx.baseUrl,
190
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
191
+ });
187
192
  logger.log(
188
193
  formatSuccessLine('Saved TLS material to ') + chalk.cyan(ctx.certDir) + chalk.green(' for remote Docker.\n')
189
194
  );
@@ -54,6 +54,36 @@ const FALLBACK_SECURITY_BY_AUTH = {
54
54
  none: []
55
55
  };
56
56
 
57
+ /**
58
+ * @param {Array<{name: string, value: string}>} authSecureVars
59
+ * @param {Record<string, unknown>} security
60
+ * @param {string} prefix
61
+ * @param {string} systemKey
62
+ */
63
+ function pushSecureVarsFromSecurityMap(authSecureVars, security, prefix, systemKey) {
64
+ for (const key of Object.keys(security)) {
65
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
66
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
67
+ authSecureVars.push({ name: envName, value: pathVal || `kv://${systemKey}/${key}` });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {Array<{name: string, value: string}>} authSecureVars
73
+ * @param {string} authMethod
74
+ * @param {string} prefix
75
+ * @param {string} systemKey
76
+ */
77
+ function pushFallbackSecureVars(authSecureVars, authMethod, prefix, systemKey) {
78
+ const keys = FALLBACK_SECURITY_BY_AUTH[authMethod] || FALLBACK_SECURITY_BY_AUTH.apikey;
79
+ for (const key of keys) {
80
+ authSecureVars.push({
81
+ name: `KV_${prefix}_${securityKeyToVar(key)}`,
82
+ value: `kv://${systemKey}/${key}`
83
+ });
84
+ }
85
+ }
86
+
57
87
  /**
58
88
  * Builds authSecureVars array from system authentication.security (or fallback by auth type).
59
89
  * @param {Object} system - System object with key and authentication
@@ -65,6 +95,9 @@ function buildAuthSecureVarsFromSystem(system) {
65
95
  const prefix = systemKeyToKvPrefix(systemKey);
66
96
  if (!prefix) return authSecureVars;
67
97
  const authBlock = system?.authentication || system?.auth;
98
+ if (authBlock?.authType === 'apiProvider' || authBlock?.authType === 'none') {
99
+ return authSecureVars;
100
+ }
68
101
  const credential = getAuthCredentialTemplate(authBlock);
69
102
  const security = credential?.security || authBlock?.security;
70
103
  const authMethod = (
@@ -74,19 +107,9 @@ function buildAuthSecureVarsFromSystem(system) {
74
107
  'apikey'
75
108
  ).toLowerCase();
76
109
  if (security && typeof security === 'object' && Object.keys(security).length > 0) {
77
- for (const key of Object.keys(security)) {
78
- const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
79
- const pathVal = kvEnvKeyToPath(envName, systemKey);
80
- authSecureVars.push({ name: envName, value: pathVal || `kv://${systemKey}/${key}` });
81
- }
110
+ pushSecureVarsFromSecurityMap(authSecureVars, security, prefix, systemKey);
82
111
  } else {
83
- const keys = FALLBACK_SECURITY_BY_AUTH[authMethod] || FALLBACK_SECURITY_BY_AUTH.apikey;
84
- for (const key of keys) {
85
- authSecureVars.push({
86
- name: `KV_${prefix}_${securityKeyToVar(key)}`,
87
- value: `kv://${systemKey}/${key}`
88
- });
89
- }
112
+ pushFallbackSecureVars(authSecureVars, authMethod, prefix, systemKey);
90
113
  }
91
114
  return authSecureVars;
92
115
  }
@@ -116,13 +139,25 @@ function buildConfigurationEntries(system) {
116
139
  * @returns {{ authMethod: string, authSecureVars: Array<{name: string, value: string}>, authNonSecureVarNames: string[], configuration: Array<{name: string, value: string, comment: string}> }}
117
140
  */
118
141
  function buildExternalEnvTemplateContext(system) {
119
- const authMethod = (system?.authentication?.method ||
120
- system?.authentication?.type ||
121
- system?.auth?.method ||
122
- system?.auth?.type ||
123
- 'apikey').toLowerCase();
142
+ const authBlock = system?.authentication || system?.auth;
143
+ const credential = getAuthCredentialTemplate(authBlock);
144
+ let authMethod = 'apikey';
145
+ if (authBlock?.authType === 'apiProvider') {
146
+ authMethod = 'apiprovider';
147
+ } else if (authBlock?.authType === 'none') {
148
+ authMethod = 'none';
149
+ } else {
150
+ authMethod = (
151
+ credential?.method ||
152
+ authBlock?.method ||
153
+ authBlock?.type ||
154
+ 'apikey'
155
+ ).toLowerCase();
156
+ }
124
157
  const authSecureVars = buildAuthSecureVarsFromSystem(system);
125
- const authVars = system?.authentication?.variables || system?.auth?.variables || {};
158
+ const authVars =
159
+ (credential?.variables && typeof credential.variables === 'object' ? credential.variables : null) ||
160
+ (authBlock?.variables && typeof authBlock.variables === 'object' ? authBlock.variables : {});
126
161
  const authNonSecureVarNames = Object.keys(authVars);
127
162
  const configuration = buildConfigurationEntries(system);
128
163
  return {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * In-place health-check wait line (TTY ora or carriage-return refresh).
3
+ *
4
+ * @fileoverview Health check poll presentation helpers
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ function shouldUseHealthCheckPollSpinner() {
15
+ return Boolean(process && process.stdout && process.stdout.isTTY);
16
+ }
17
+
18
+ /**
19
+ * @param {number} attemptIndex - Zero-based attempt index after a failed probe
20
+ * @param {number} maxAttempts
21
+ * @returns {string}
22
+ */
23
+ function buildHealthCheckPollText(attemptIndex, maxAttempts) {
24
+ return chalk.yellow(
25
+ `Waiting for health check… (${attemptIndex + 1}/${maxAttempts})`
26
+ );
27
+ }
28
+
29
+ /**
30
+ * @param {number} maxAttempts
31
+ * @returns {{ usesSpinner: boolean, onWaiting: (attemptIndex: number) => void, finish: () => void }}
32
+ */
33
+ function createHealthCheckPollHandlers(maxAttempts) {
34
+ if (shouldUseHealthCheckPollSpinner()) {
35
+ const ora = require('ora');
36
+ const spinner = ora({
37
+ text: buildHealthCheckPollText(0, maxAttempts),
38
+ spinner: 'dots'
39
+ });
40
+ let started = false;
41
+ return {
42
+ usesSpinner: true,
43
+ onWaiting: (attemptIndex) => {
44
+ if (!started) {
45
+ spinner.start();
46
+ started = true;
47
+ }
48
+ spinner.text = buildHealthCheckPollText(attemptIndex, maxAttempts);
49
+ },
50
+ finish: () => {
51
+ if (started) {
52
+ spinner.stop();
53
+ process.stdout.write('\n');
54
+ }
55
+ }
56
+ };
57
+ }
58
+
59
+ let lastLine = '';
60
+ return {
61
+ usesSpinner: false,
62
+ onWaiting: (attemptIndex) => {
63
+ const line = buildHealthCheckPollText(attemptIndex, maxAttempts);
64
+ if (process.stdout && process.stdout.isTTY) {
65
+ process.stdout.write(`\r${line}`);
66
+ lastLine = line;
67
+ return;
68
+ }
69
+ logger.log(line);
70
+ },
71
+ finish: () => {
72
+ if (lastLine && process.stdout && process.stdout.isTTY) {
73
+ process.stdout.write('\n');
74
+ lastLine = '';
75
+ }
76
+ }
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ buildHealthCheckPollText,
82
+ createHealthCheckPollHandlers,
83
+ shouldUseHealthCheckPollSpinner
84
+ };
@@ -63,56 +63,76 @@ function frontDoorPattern(appConfig) {
63
63
  }
64
64
 
65
65
  /**
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.
68
- *
69
- * @async
70
- * @param {string} appName
71
- * @param {number} healthCheckPort
72
66
  * @param {Object|null} appConfig
73
- * @returns {Promise<string|null>}
67
+ * @returns {{ fd: Object, mountPath: string }|null}
74
68
  */
75
- async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig) {
69
+ function readTraefikFrontDoorMount(appConfig) {
76
70
  if (!appConfig || !frontDoorEnabled(appConfig)) return null;
77
71
  const pattern = frontDoorPattern(appConfig);
78
72
  if (!pattern) return null;
79
-
80
- const coreConfig = require('../core/config');
81
- const userCfg = await coreConfig.getConfig();
82
- if (!(userCfg && userCfg.traefik)) return null;
83
-
84
73
  const fd = appConfig.frontDoorRouting;
85
- if (!fd || typeof fd !== 'object') return null;
86
74
  const hostTemplate = typeof fd.host === 'string' ? fd.host.trim() : '';
87
75
  if (!hostTemplate) return null;
76
+ return { fd, mountPath: normalizeFrontDoorPatternForHealth(pattern) };
77
+ }
88
78
 
89
- const mountPath = normalizeFrontDoorPatternForHealth(pattern);
90
-
91
- const infraTlsEnabled = Boolean(userCfg && userCfg.tlsEnabled);
92
- const remoteServer = await coreConfig.getRemoteServer();
93
- const developerIdRaw = await coreConfig.getDeveloperId();
94
- const developerIdNum = parseDeveloperIdNum(developerIdRaw);
95
- const setupPlatformMode = await coreConfig.getSetupPlatformMode();
79
+ /**
80
+ * @param {Object} appConfig
81
+ * @returns {Promise<Object|null>}
82
+ */
83
+ async function loadTraefikPublicUrlContext(appConfig) {
84
+ const coreConfig = require('../core/config');
85
+ const userCfg = await coreConfig.getConfig();
86
+ if (!(userCfg && userCfg.traefik)) return null;
87
+ const [remoteServer, developerRootDomain, developerIdRaw, setupPlatformMode] = await Promise.all([
88
+ coreConfig.getRemoteServer(),
89
+ coreConfig.getDeveloperRootDomain(),
90
+ coreConfig.getDeveloperId(),
91
+ coreConfig.getSetupPlatformMode()
92
+ ]);
93
+ return {
94
+ infraTlsEnabled: Boolean(userCfg && userCfg.tlsEnabled),
95
+ remoteServer,
96
+ developerRootDomain,
97
+ developerIdRaw,
98
+ developerIdNum: parseDeveloperIdNum(developerIdRaw),
99
+ setupPlatformMode,
100
+ listenPort: Number(appConfig?.port || 3000)
101
+ };
102
+ }
96
103
 
97
- // URLs are resolved for the CLI on the host, not from inside a container.
98
- const profile = 'local';
99
- const listenPort = Number(appConfig?.port || 3000);
104
+ /**
105
+ * Public app URL (Traefik + frontDoor mount path), without the health path — for CLI summaries.
106
+ * Requires infra Traefik on, `frontDoorRouting.enabled`, host, and pattern.
107
+ *
108
+ * @async
109
+ * @param {string} appName
110
+ * @param {number} healthCheckPort
111
+ * @param {Object|null} appConfig
112
+ * @returns {Promise<string|null>}
113
+ */
114
+ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig) {
115
+ const frontDoor = readTraefikFrontDoorMount(appConfig);
116
+ if (!frontDoor) return null;
117
+ const ctx = await loadTraefikPublicUrlContext(appConfig);
118
+ if (!ctx) return null;
100
119
 
101
120
  const publicBase = computePublicUrlBaseString({
102
121
  traefik: true,
103
122
  pathActive: true,
104
- hostTemplate: fd.host,
105
- tls: fd.tls,
106
- developerIdRaw,
107
- remoteServer,
108
- profile,
109
- listenPort,
110
- developerIdNum,
111
- infraTlsEnabled,
112
- setupPlatformMode
123
+ hostTemplate: frontDoor.fd.host,
124
+ tls: frontDoor.fd.tls,
125
+ developerIdRaw: ctx.developerIdRaw,
126
+ remoteServer: ctx.remoteServer,
127
+ developerRootDomain: ctx.developerRootDomain,
128
+ profile: 'local',
129
+ listenPort: ctx.listenPort,
130
+ developerIdNum: ctx.developerIdNum,
131
+ infraTlsEnabled: ctx.infraTlsEnabled,
132
+ setupPlatformMode: ctx.setupPlatformMode
113
133
  });
114
134
 
115
- return joinUrlPath(publicBase, mountPath);
135
+ return joinUrlPath(publicBase, frontDoor.mountPath);
116
136
  }
117
137
 
118
138
  /**
@@ -24,6 +24,7 @@ const {
24
24
  filterTraefikUrlByDns,
25
25
  logPublicHealthUrlWarningIfNeeded
26
26
  } = require('./health-check-public-warn');
27
+ const { createHealthCheckPollHandlers } = require('./health-check-poll-ui');
27
28
 
28
29
  /**
29
30
  * Compute the health check URL for an app.
@@ -228,6 +229,13 @@ function buildHealthCheckConfig(healthCheckPort, config, timeout, debug) {
228
229
  return { maxAttempts };
229
230
  }
230
231
 
232
+ function logHealthCheckSuccessIfAllowed() {
233
+ const { isSetupQuietOutput } = require('./setup-quiet-output');
234
+ if (!isSetupQuietOutput()) {
235
+ logger.log(formatSuccessLine('Application is healthy'));
236
+ }
237
+ }
238
+
231
239
  /**
232
240
  * Performs a single health check attempt
233
241
  * @async
@@ -236,18 +244,24 @@ function buildHealthCheckConfig(healthCheckPort, config, timeout, debug) {
236
244
  * @param {number} attempt - Attempt number
237
245
  * @param {number} maxAttempts - Maximum attempts
238
246
  * @param {boolean} debug - Debug flag
247
+ * @param {{ suppressSuccessLog?: boolean }} [attemptOpts]
239
248
  * @returns {Promise<boolean>} True if health check passed
240
249
  */
241
- async function performHealthCheckAttempt(healthCheckUrl, attempt, maxAttempts, debug) {
250
+ async function performHealthCheckAttempt(
251
+ healthCheckUrl,
252
+ attempt,
253
+ maxAttempts,
254
+ debug,
255
+ attemptOpts = {}
256
+ ) {
242
257
  try {
243
258
  if (debug) {
244
259
  logger.log(chalk.gray(`[DEBUG] Health check attempt ${attempt + 1}/${maxAttempts}`));
245
260
  }
246
261
  const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl, debug);
247
262
  if (healthCheckPassed) {
248
- const { isSetupQuietOutput } = require('./setup-quiet-output');
249
- if (!isSetupQuietOutput()) {
250
- logger.log(formatSuccessLine('Application is healthy'));
263
+ if (!attemptOpts.suppressSuccessLog) {
264
+ logHealthCheckSuccessIfAllowed();
251
265
  }
252
266
  if (debug) {
253
267
  logger.log(chalk.gray(`[DEBUG] Health check passed after ${attempt + 1} attempt(s)`));
@@ -287,7 +301,7 @@ async function computePreferredHealthCheckUrls(appName, healthCheckPort, config,
287
301
  skippedPublicHealthUrl = filtered.skippedPublicHealthUrl;
288
302
 
289
303
  const urlsToTry = traefikUrl ? [traefikUrl, localhostUrl] : [localhostUrl];
290
- if (urlsToTry.length > 1) {
304
+ if (urlsToTry.length > 1 && debug) {
291
305
  logger.log(
292
306
  chalk.gray(
293
307
  `ℹ Health check order: Traefik/DNS (${urlsToTry[0]}), then localhost (${urlsToTry[1]}).`
@@ -300,10 +314,16 @@ async function computePreferredHealthCheckUrls(appName, healthCheckPort, config,
300
314
  return { urlsToTry, skippedPublicHealthUrl };
301
315
  }
302
316
 
303
- async function performHealthCheckAttemptForUrls(urlsToTry, attempt, maxAttempts, debug) {
317
+ async function performHealthCheckAttemptForUrls(
318
+ urlsToTry,
319
+ attempt,
320
+ maxAttempts,
321
+ debug,
322
+ attemptOpts = {}
323
+ ) {
304
324
  for (let i = 0; i < urlsToTry.length; i++) {
305
325
  const url = urlsToTry[i];
306
- const passed = await performHealthCheckAttempt(url, attempt, maxAttempts, debug);
326
+ const passed = await performHealthCheckAttempt(url, attempt, maxAttempts, debug, attemptOpts);
307
327
  if (passed) {
308
328
  return { ok: true, resolvedIndex: i };
309
329
  }
@@ -324,7 +344,7 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
324
344
  debug
325
345
  );
326
346
 
327
- if (skippedPublicHealthUrl && urlsToTry.length === 1) {
347
+ if (skippedPublicHealthUrl && urlsToTry.length === 1 && debug) {
328
348
  logger.log(
329
349
  chalk.gray(
330
350
  `ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
@@ -333,9 +353,20 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
333
353
  );
334
354
  }
335
355
 
356
+ const poll = createHealthCheckPollHandlers(maxAttempts);
357
+ const pollAttemptOpts = { suppressSuccessLog: true };
358
+
336
359
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
337
- const attemptResult = await performHealthCheckAttemptForUrls(urlsToTry, attempts, maxAttempts, debug);
360
+ const attemptResult = await performHealthCheckAttemptForUrls(
361
+ urlsToTry,
362
+ attempts,
363
+ maxAttempts,
364
+ debug,
365
+ pollAttemptOpts
366
+ );
338
367
  if (attemptResult.ok) {
368
+ poll.finish();
369
+ logHealthCheckSuccessIfAllowed();
339
370
  logPublicHealthUrlWarningIfNeeded({
340
371
  skippedPublicHealthUrl,
341
372
  urlsToTry,
@@ -345,17 +376,13 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
345
376
  }
346
377
 
347
378
  if (attempts < maxAttempts - 1) {
348
- const probeHint =
349
- urlsToTry.length > 1
350
- ? `trying ${urlsToTry[0]}, then ${urlsToTry[1]}`
351
- : (urlsToTry[0] || 'health URL');
352
- logger.log(
353
- chalk.yellow(`Waiting for health check… (${attempts + 1}/${maxAttempts}) (${probeHint})`)
354
- );
379
+ poll.onWaiting(attempts);
355
380
  await new Promise(resolve => setTimeout(resolve, 2000));
356
381
  }
357
382
  }
358
383
 
384
+ poll.finish();
385
+
359
386
  if (debug) {
360
387
  logger.log(chalk.gray(`[DEBUG] Health check failed after ${maxAttempts} attempts`));
361
388
  }
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const config = require('../core/config');
12
+ const { flagsForSetupPlatformMode } = require('../core/setup-platform-mode');
12
13
 
13
14
  /**
14
15
  * Resolves effective boolean from CLI option vs config.
@@ -30,14 +31,40 @@ function resolveInfraOptionalFlag(optValue, cfgValue, defaultWhenUndef = true) {
30
31
  * @param {Object} [options] - Commander options (omit for setup-modes)
31
32
  * @returns {{ traefik: boolean, pgadmin: boolean, redisCommander: boolean }}
32
33
  */
34
+ function topologyFlagsForConfig(cfg) {
35
+ if (!cfg || !cfg.setupPlatformMode) {
36
+ return null;
37
+ }
38
+ return flagsForSetupPlatformMode(cfg.setupPlatformMode);
39
+ }
40
+
33
41
  function computeEffectiveInfraOptionalFlags(cfg, options = {}) {
42
+ const topology = topologyFlagsForConfig(cfg);
43
+ const traefikCandidate = resolveInfraOptionalFlag(options.traefik, cfg.traefik, false);
44
+ const traefik =
45
+ topology && !topology.traefik && options.traefik !== true ? false : traefikCandidate;
34
46
  return {
35
- traefik: resolveInfraOptionalFlag(options.traefik, cfg.traefik, false),
47
+ traefik,
36
48
  pgadmin: resolveInfraOptionalFlag(options.pgAdmin, cfg.pgadmin, true),
37
49
  redisCommander: resolveInfraOptionalFlag(options.redisAdmin, cfg.redisCommander, true)
38
50
  };
39
51
  }
40
52
 
53
+ /**
54
+ * Effective TLS flag for infra start: setup platform mode is authoritative when Traefik/TLS mode is off.
55
+ *
56
+ * @param {Object} cfg
57
+ * @param {Object} [options] - Commander options (`tls` / `--no-tls`)
58
+ * @returns {boolean}
59
+ */
60
+ function resolveEffectiveTlsEnabled(cfg, options = {}) {
61
+ if (options.tls === true) return true;
62
+ if (options.tls === false) return false;
63
+ const topology = topologyFlagsForConfig(cfg);
64
+ if (topology && !topology.tlsEnabled) return false;
65
+ return cfg.tlsEnabled === true;
66
+ }
67
+
41
68
  /**
42
69
  * Writes `traefik` / `pgadmin` / `redisCommander` when missing so config matches compose defaults.
43
70
  *
@@ -65,5 +92,7 @@ async function persistMissingInfraOptionalServiceFlags(cfg, effective) {
65
92
  module.exports = {
66
93
  resolveInfraOptionalFlag,
67
94
  computeEffectiveInfraOptionalFlags,
68
- persistMissingInfraOptionalServiceFlags
95
+ resolveEffectiveTlsEnabled,
96
+ persistMissingInfraOptionalServiceFlags,
97
+ topologyFlagsForConfig
69
98
  };
@@ -104,10 +104,13 @@ function getSyncSshUrl(syncSshUser, syncSshHost, remotePath) {
104
104
  * @returns {Promise<string[]>}
105
105
  */
106
106
  async function listSyncSessionNames(mutagenPath) {
107
- const { stdout } = await execAsync(`"${mutagenPath}" sync list --template '{{.Name}}'`, {
108
- encoding: 'utf8',
109
- timeout: 5000
110
- });
107
+ const { stdout } = await execAsync(
108
+ `"${mutagenPath}" sync list --template '{{range .}}{{.Name}}{{"\\n"}}{{end}}'`,
109
+ {
110
+ encoding: 'utf8',
111
+ timeout: 5000
112
+ }
113
+ );
111
114
  return (stdout || '').trim().split('\n').filter(Boolean);
112
115
  }
113
116
 
@@ -43,6 +43,25 @@ function loadBundledApplicationVariables(appName) {
43
43
  return variables && typeof variables === 'object' ? variables : null;
44
44
  }
45
45
 
46
+ /**
47
+ * Merge platform routing defaults from bundled template when builder copy omits them.
48
+ *
49
+ * @param {string} appName
50
+ * @param {Object} variables
51
+ * @returns {Object}
52
+ */
53
+ function mergeBundledPlatformRoutingDefaults(appName, variables) {
54
+ const bundled = loadBundledApplicationVariables(appName);
55
+ if (!bundled || typeof bundled !== 'object') {
56
+ return variables;
57
+ }
58
+ const merged = { ...variables };
59
+ if (!merged.frontDoorRouting && bundled.frontDoorRouting) {
60
+ merged.frontDoorRouting = bundled.frontDoorRouting;
61
+ }
62
+ return merged;
63
+ }
64
+
46
65
  /**
47
66
  * Load platform app variables from materialized builder config, or from bundled template
48
67
  * when `aifabrix setup` runs before `up-platform` has copied templates.
@@ -55,7 +74,8 @@ function loadAppVariables(appName) {
55
74
  if (fs.existsSync(builderPath)) {
56
75
  try {
57
76
  const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
58
- return loadConfigFile(configPath) || {};
77
+ const fromBuilder = loadConfigFile(configPath) || {};
78
+ return mergeBundledPlatformRoutingDefaults(appName, fromBuilder);
59
79
  } catch (err) {
60
80
  const msg = err && err.message ? String(err.message) : '';
61
81
  if (!msg.includes('Application config not found')) {
@@ -102,6 +122,7 @@ async function computeAppBaseUrl(appName) {
102
122
  typeof developerIdRaw === 'string' ? parseInt(developerIdRaw, 10) : developerIdRaw;
103
123
  const userCfg = await config.getConfig();
104
124
  const remoteServer = await config.getRemoteServer();
125
+ const developerRootDomain = await config.getDeveloperRootDomain();
105
126
  const infraTlsEnabled = Boolean(userCfg && userCfg.tlsEnabled);
106
127
  const setupPlatformMode = await config.getSetupPlatformMode();
107
128
 
@@ -117,6 +138,7 @@ async function computeAppBaseUrl(appName) {
117
138
  tls: fd ? fd.tls : true,
118
139
  developerIdRaw,
119
140
  remoteServer,
141
+ developerRootDomain,
120
142
  profile: 'docker',
121
143
  listenPort: basePort,
122
144
  developerIdNum,