@aifabrix/builder 2.48.0 → 2.49.1

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 (83) hide show
  1. package/.cursor/skills/README.md +14 -0
  2. package/jest.isolated-projects.js +3 -0
  3. package/jest.projects.js +12 -0
  4. package/lib/api/applications.api.js +23 -0
  5. package/lib/api/dev.api.js +1 -1
  6. package/lib/api/index.js +5 -2
  7. package/lib/api/types/dev.types.js +3 -2
  8. package/lib/app/deploy-config.js +6 -4
  9. package/lib/app/register.js +1 -1
  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/cli/infra-guided-auth.js +50 -12
  17. package/lib/cli/infra-guided-footers.js +164 -0
  18. package/lib/cli/infra-guided-spinners.js +57 -0
  19. package/lib/cli/infra-guided.js +52 -181
  20. package/lib/cli/setup-app.js +1 -1
  21. package/lib/cli/setup-dev.js +3 -2
  22. package/lib/cli/setup-infra.js +3 -2
  23. package/lib/cli/setup-platform.js +3 -0
  24. package/lib/commands/login-device.js +6 -2
  25. package/lib/commands/login.js +4 -1
  26. package/lib/commands/setup-modes.js +19 -3
  27. package/lib/commands/setup-platform-auth.js +25 -0
  28. package/lib/commands/setup-prompts-admin.js +9 -0
  29. package/lib/commands/setup.js +60 -8
  30. package/lib/commands/teardown.js +3 -5
  31. package/lib/commands/up-common.js +8 -11
  32. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  33. package/lib/commands/up-dataplane-credentials.js +19 -4
  34. package/lib/commands/up-dataplane.js +155 -24
  35. package/lib/commands/up-miso.js +6 -2
  36. package/lib/core/secrets-env-declarative-expand.js +35 -23
  37. package/lib/core/setup-platform-mode.js +43 -0
  38. package/lib/deployment/deployer-status.js +26 -3
  39. package/lib/deployment/deployer.js +7 -14
  40. package/lib/external-system/download-helpers.js +10 -1
  41. package/lib/infrastructure/compose.js +40 -12
  42. package/lib/infrastructure/services.js +15 -3
  43. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  44. package/lib/utils/app-bootstrap-api.js +53 -0
  45. package/lib/utils/app-run-containers.js +14 -3
  46. package/lib/utils/applications-config-defaults.js +19 -3
  47. package/lib/utils/compose-generate-docker-compose.js +16 -5
  48. package/lib/utils/compose-generator.js +34 -15
  49. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  50. package/lib/utils/config-paths.js +6 -1
  51. package/lib/utils/dev-user-groups.js +1 -0
  52. package/lib/utils/docker-daemon-tls-ca.js +94 -57
  53. package/lib/utils/docker-reload-mount.js +12 -1
  54. package/lib/utils/external-env-template.js +53 -18
  55. package/lib/utils/health-check-poll-ui.js +119 -0
  56. package/lib/utils/health-check-url.js +54 -34
  57. package/lib/utils/health-check.js +91 -35
  58. package/lib/utils/infra-optional-service-flags.js +31 -2
  59. package/lib/utils/mutagen.js +7 -4
  60. package/lib/utils/paths.js +26 -36
  61. package/lib/utils/platform-controller-url.js +23 -1
  62. package/lib/utils/refresh-remote-settings.js +67 -0
  63. package/lib/utils/remote-host-for-front-door.js +43 -0
  64. package/lib/utils/run-cli-flags.js +37 -1
  65. package/lib/utils/url-declarative-public-base.js +11 -4
  66. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  67. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  68. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  69. package/lib/utils/url-declarative-resolve.js +2 -0
  70. package/lib/utils/urls-local-registry-scan.js +95 -10
  71. package/lib/utils/urls-local-registry.js +24 -1
  72. package/lib/utils/worker-sidecars.js +2 -2
  73. package/lib/validation/external-system-auth-rules.js +45 -7
  74. package/package.json +2 -1
  75. package/scripts/test-dataplane-bootstrap.js +331 -0
  76. package/templates/applications/dataplane/application.yaml +1 -1
  77. package/templates/applications/dataplane/env.template +24 -6
  78. package/templates/applications/dataplane/rbac.yaml +11 -0
  79. package/templates/applications/miso-controller/application.yaml +1 -1
  80. package/templates/applications/miso-controller/env.template +5 -0
  81. package/templates/infra/compose.yaml.hbs +13 -9
  82. package/templates/python/docker-compose.hbs +3 -0
  83. package/templates/typescript/docker-compose.hbs +3 -0
@@ -51,6 +51,68 @@ function collectIssuerChainPems(peerCert) {
51
51
  return pems;
52
52
  }
53
53
 
54
+ /**
55
+ * @param {string} rest - Host[:port] after tcp://
56
+ * @returns {{ host: string, port: number }|null}
57
+ */
58
+ function parseBracketedTcpHostPort(rest) {
59
+ if (!rest.startsWith('[')) return null;
60
+ const end = rest.indexOf(']');
61
+ if (end === -1) return null;
62
+ const host = rest.slice(1, end);
63
+ let port = DEFAULT_DOCKER_TLS_PORT;
64
+ const after = rest.slice(end + 1);
65
+ if (after.startsWith(':')) {
66
+ const p = parseInt(after.slice(1), 10);
67
+ if (!Number.isNaN(p)) port = p;
68
+ }
69
+ return { host, port };
70
+ }
71
+
72
+ /**
73
+ * @param {string} rest - Host[:port] after tcp://
74
+ * @returns {{ host: string, port: number }}
75
+ */
76
+ function parsePlainTcpHostPort(rest) {
77
+ const colonIdx = rest.lastIndexOf(':');
78
+ if (colonIdx > 0 && /^\d+$/.test(rest.slice(colonIdx + 1))) {
79
+ return {
80
+ host: rest.slice(0, colonIdx),
81
+ port: parseInt(rest.slice(colonIdx + 1), 10)
82
+ };
83
+ }
84
+ return { host: rest, port: DEFAULT_DOCKER_TLS_PORT };
85
+ }
86
+
87
+ /**
88
+ * @param {string} trimmed
89
+ * @returns {{ host: string, port: number }|null}
90
+ */
91
+ function parseTcpSchemeEndpoint(trimmed) {
92
+ const rest = trimmed.slice(6).split('/')[0];
93
+ return parseBracketedTcpHostPort(rest) || parsePlainTcpHostPort(rest);
94
+ }
95
+
96
+ /**
97
+ * @param {string} trimmed
98
+ * @returns {{ host: string, port: number }|null}
99
+ */
100
+ function parseDockerEndpointViaUrl(trimmed) {
101
+ try {
102
+ const u = new URL(trimmed.includes('://') ? trimmed : `tcp://${trimmed}`);
103
+ const host = String(u.hostname || '').trim();
104
+ if (!host) return null;
105
+ let port = DEFAULT_DOCKER_TLS_PORT;
106
+ if (u.port) {
107
+ const p = parseInt(u.port, 10);
108
+ if (!Number.isNaN(p)) port = p;
109
+ }
110
+ return { host, port };
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
54
116
  /**
55
117
  * Parse docker-endpoint (tcp://host:port) into TLS connect parameters.
56
118
  * @param {string} endpoint
@@ -62,45 +124,11 @@ function parseDockerTlsEndpoint(endpoint) {
62
124
  const lower = trimmed.toLowerCase();
63
125
  if (lower.startsWith('unix:')) return null;
64
126
 
65
- let host = '';
66
- let port = DEFAULT_DOCKER_TLS_PORT;
67
-
68
- if (lower.startsWith('tcp://')) {
69
- const rest = trimmed.slice(6).split('/')[0];
70
- if (rest.startsWith('[')) {
71
- const end = rest.indexOf(']');
72
- if (end === -1) return null;
73
- host = rest.slice(1, end);
74
- const after = rest.slice(end + 1);
75
- if (after.startsWith(':')) {
76
- const p = parseInt(after.slice(1), 10);
77
- if (!Number.isNaN(p)) port = p;
78
- }
79
- } else {
80
- const colonIdx = rest.lastIndexOf(':');
81
- if (colonIdx > 0 && /^\d+$/.test(rest.slice(colonIdx + 1))) {
82
- port = parseInt(rest.slice(colonIdx + 1), 10);
83
- host = rest.slice(0, colonIdx);
84
- } else {
85
- host = rest;
86
- }
87
- }
88
- } else {
89
- try {
90
- const u = new URL(trimmed.includes('://') ? trimmed : `tcp://${trimmed}`);
91
- host = u.hostname;
92
- if (u.port) {
93
- const p = parseInt(u.port, 10);
94
- if (!Number.isNaN(p)) port = p;
95
- }
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- host = String(host || '').trim();
102
- if (!host) return null;
103
- return { host, port, servername: host };
127
+ const parsed = lower.startsWith('tcp://')
128
+ ? parseTcpSchemeEndpoint(trimmed)
129
+ : parseDockerEndpointViaUrl(trimmed);
130
+ if (!parsed || !parsed.host) return null;
131
+ return { host: parsed.host, port: parsed.port, servername: parsed.host };
104
132
  }
105
133
 
106
134
  /**
@@ -262,6 +290,32 @@ async function writeDockerDaemonCaMarker(certDir) {
262
290
  await fs.writeFile(markerPath, `augmentedAt=${new Date().toISOString()}\n`, { mode: 0o600 });
263
291
  }
264
292
 
293
+ /**
294
+ * @param {string} certDir
295
+ * @param {string} existingCaPem
296
+ * @param {string} dockerEndpoint
297
+ * @param {{ timeoutMs?: number }} fetchOpts
298
+ * @returns {Promise<boolean>}
299
+ */
300
+ async function augmentCaPemForDockerVerify(certDir, existingCaPem, dockerEndpoint, fetchOpts) {
301
+ const fs = require('fs');
302
+ const path = require('path');
303
+ if (await caPemTrustsDockerDaemon(existingCaPem, dockerEndpoint, fetchOpts)) {
304
+ await writeDockerDaemonCaMarker(certDir);
305
+ return true;
306
+ }
307
+ const augmented = await augmentCaPemWithDockerDaemonChain(existingCaPem, dockerEndpoint, fetchOpts);
308
+ if (!augmented || augmented === existingCaPem) return false;
309
+ const { normalizePemNewlines } = require('./dev-cert-helper');
310
+ await fs.promises.writeFile(
311
+ path.join(certDir, 'ca.pem'),
312
+ normalizePemNewlines(augmented),
313
+ { mode: 0o600 }
314
+ );
315
+ await writeDockerDaemonCaMarker(certDir);
316
+ return true;
317
+ }
318
+
265
319
  /**
266
320
  * Decide whether Docker CLI should verify the daemon certificate (DOCKER_TLS_VERIFY=1).
267
321
  * When ca.pem holds only the internal issue-cert CA but the daemon uses a public/wildcard cert,
@@ -288,24 +342,7 @@ async function resolveRemoteDockerDaemonVerify(ctx) {
288
342
  if (!dockerEndpoint) return true;
289
343
 
290
344
  const fetchOpts = { timeoutMs: DEFAULT_FETCH_TIMEOUT_MS };
291
- if (await caPemTrustsDockerDaemon(existingCaPem, dockerEndpoint, fetchOpts)) {
292
- await writeDockerDaemonCaMarker(certDir);
293
- return true;
294
- }
295
-
296
- const augmented = await augmentCaPemWithDockerDaemonChain(existingCaPem, dockerEndpoint, fetchOpts);
297
- if (augmented && augmented !== existingCaPem) {
298
- const { normalizePemNewlines } = require('./dev-cert-helper');
299
- await fs.promises.writeFile(
300
- path.join(certDir, 'ca.pem'),
301
- normalizePemNewlines(augmented),
302
- { mode: 0o600 }
303
- );
304
- await writeDockerDaemonCaMarker(certDir);
305
- return true;
306
- }
307
-
308
- return false;
345
+ return augmentCaPemForDockerVerify(certDir, existingCaPem, dockerEndpoint, fetchOpts);
309
346
  }
310
347
 
311
348
  module.exports = {
@@ -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
  };
@@ -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,119 @@
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
+ function createSilentHealthCheckPollHandlers() {
30
+ return {
31
+ usesSpinner: false,
32
+ onWaiting: () => {},
33
+ finish: () => {}
34
+ };
35
+ }
36
+
37
+ function createDelegatedHealthCheckPollHandlers(maxAttempts, onPollText) {
38
+ return {
39
+ usesSpinner: false,
40
+ onWaiting: (attemptIndex) => {
41
+ onPollText(buildHealthCheckPollText(attemptIndex, maxAttempts));
42
+ },
43
+ finish: () => {}
44
+ };
45
+ }
46
+
47
+ function createOraHealthCheckPollHandlers(maxAttempts) {
48
+ const ora = require('ora');
49
+ const spinner = ora({
50
+ text: buildHealthCheckPollText(0, maxAttempts),
51
+ spinner: 'dots'
52
+ });
53
+ let started = false;
54
+ return {
55
+ usesSpinner: true,
56
+ onWaiting: (attemptIndex) => {
57
+ if (!started) {
58
+ spinner.start();
59
+ started = true;
60
+ }
61
+ spinner.text = buildHealthCheckPollText(attemptIndex, maxAttempts);
62
+ },
63
+ finish: () => {
64
+ if (started) {
65
+ spinner.stop();
66
+ process.stdout.write('\n');
67
+ }
68
+ }
69
+ };
70
+ }
71
+
72
+ function createCarriageReturnHealthCheckPollHandlers(maxAttempts) {
73
+ let lastLine = '';
74
+ return {
75
+ usesSpinner: false,
76
+ onWaiting: (attemptIndex) => {
77
+ const line = buildHealthCheckPollText(attemptIndex, maxAttempts);
78
+ if (process.stdout && process.stdout.isTTY) {
79
+ process.stdout.write(`\r${line}`);
80
+ lastLine = line;
81
+ return;
82
+ }
83
+ logger.log(line);
84
+ },
85
+ finish: () => {
86
+ if (lastLine && process.stdout && process.stdout.isTTY) {
87
+ process.stdout.write('\n');
88
+ lastLine = '';
89
+ }
90
+ }
91
+ };
92
+ }
93
+
94
+ /**
95
+ * @param {number} maxAttempts
96
+ * @param {{ silent?: boolean, onPollText?: (text: string) => void }} [options]
97
+ * @returns {{ usesSpinner: boolean, onWaiting: (attemptIndex: number) => void, finish: () => void }}
98
+ */
99
+ function createHealthCheckPollHandlers(maxAttempts, options = {}) {
100
+ const silent = options && options.silent === true;
101
+ const onPollText = options && typeof options.onPollText === 'function' ? options.onPollText : null;
102
+
103
+ if (silent) {
104
+ return createSilentHealthCheckPollHandlers();
105
+ }
106
+ if (onPollText) {
107
+ return createDelegatedHealthCheckPollHandlers(maxAttempts, onPollText);
108
+ }
109
+ if (shouldUseHealthCheckPollSpinner()) {
110
+ return createOraHealthCheckPollHandlers(maxAttempts);
111
+ }
112
+ return createCarriageReturnHealthCheckPollHandlers(maxAttempts);
113
+ }
114
+
115
+ module.exports = {
116
+ buildHealthCheckPollText,
117
+ createHealthCheckPollHandlers,
118
+ shouldUseHealthCheckPollSpinner
119
+ };
@@ -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
  /**