@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
@@ -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
  }
@@ -311,31 +331,39 @@ async function performHealthCheckAttemptForUrls(urlsToTry, attempt, maxAttempts,
311
331
  return { ok: false, resolvedIndex: -1 };
312
332
  }
313
333
 
314
- async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
315
- await waitForDbInit(appName);
316
-
317
- const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
318
- const { maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
319
- const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
320
- appName,
321
- healthCheckPort,
322
- config,
323
- runOptions,
324
- debug
325
- );
326
-
327
- if (skippedPublicHealthUrl && urlsToTry.length === 1) {
328
- logger.log(
329
- chalk.gray(
330
- `ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
331
- `Probing ${urlsToTry[0]} only until the app responds.`
332
- )
333
- );
334
+ function resolveHealthCheckPollOptions(runOptions = {}) {
335
+ const pollOptions = {};
336
+ if (runOptions.silentPoll === true) {
337
+ pollOptions.silent = true;
338
+ }
339
+ if (typeof runOptions.onPollText === 'function') {
340
+ pollOptions.onPollText = runOptions.onPollText;
334
341
  }
342
+ return pollOptions;
343
+ }
344
+
345
+ async function pollUntilHealthCheckPasses({
346
+ urlsToTry,
347
+ maxAttempts,
348
+ timeout,
349
+ debug,
350
+ skippedPublicHealthUrl,
351
+ pollOptions = {}
352
+ }) {
353
+ const poll = createHealthCheckPollHandlers(maxAttempts, pollOptions);
354
+ const pollAttemptOpts = { suppressSuccessLog: true };
335
355
 
336
356
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
337
- const attemptResult = await performHealthCheckAttemptForUrls(urlsToTry, attempts, maxAttempts, debug);
357
+ const attemptResult = await performHealthCheckAttemptForUrls(
358
+ urlsToTry,
359
+ attempts,
360
+ maxAttempts,
361
+ debug,
362
+ pollAttemptOpts
363
+ );
338
364
  if (attemptResult.ok) {
365
+ poll.finish();
366
+ logHealthCheckSuccessIfAllowed();
339
367
  logPublicHealthUrlWarningIfNeeded({
340
368
  skippedPublicHealthUrl,
341
369
  urlsToTry,
@@ -345,23 +373,51 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
345
373
  }
346
374
 
347
375
  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
- );
376
+ poll.onWaiting(attempts);
355
377
  await new Promise(resolve => setTimeout(resolve, 2000));
356
378
  }
357
379
  }
358
380
 
381
+ poll.finish();
382
+
359
383
  if (debug) {
360
384
  logger.log(chalk.gray(`[DEBUG] Health check failed after ${maxAttempts} attempts`));
361
385
  }
362
386
  throw new Error(`Health check timeout after ${timeout} seconds`);
363
387
  }
364
388
 
389
+ async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
390
+ await waitForDbInit(appName);
391
+
392
+ const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
393
+ const { maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
394
+ const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
395
+ appName,
396
+ healthCheckPort,
397
+ config,
398
+ runOptions,
399
+ debug
400
+ );
401
+
402
+ if (skippedPublicHealthUrl && urlsToTry.length === 1 && debug) {
403
+ logger.log(
404
+ chalk.gray(
405
+ `ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
406
+ `Probing ${urlsToTry[0]} only until the app responds.`
407
+ )
408
+ );
409
+ }
410
+
411
+ await pollUntilHealthCheckPasses({
412
+ urlsToTry,
413
+ maxAttempts,
414
+ timeout,
415
+ debug,
416
+ skippedPublicHealthUrl,
417
+ pollOptions: resolveHealthCheckPollOptions(runOptions)
418
+ });
419
+ }
420
+
365
421
  /**
366
422
  * Checks if port is available
367
423
  * @param {number} port - Port number to check
@@ -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
 
@@ -459,13 +459,23 @@ function isSystemBuilderAppName(appName) {
459
459
  }
460
460
 
461
461
  /**
462
- * `builder/<appName>` under {@link getAppsMaterializationParent} (same tree as {@link getSystemBuilderRoot}).
462
+ * Home / work materialization path: `(aifabrix-work | aifabrix-home)/builder/<appName>`.
463
+ * Used by setup, teardown, and template copy — never the product-repo `cwd/builder` tree.
463
464
  *
464
465
  * @param {string} appName
465
466
  * @returns {string}
466
467
  */
468
+ function getHomeBuilderAppPath(appName) {
469
+ return path.join(getAppsMaterializationParent(), 'builder', appName);
470
+ }
471
+
472
+ /**
473
+ * @deprecated Prefer {@link getHomeBuilderAppPath}; same path.
474
+ * @param {string} appName
475
+ * @returns {string}
476
+ */
467
477
  function getProjectBuilderAppPath(appName) {
468
- return path.join(getIntegrationBuilderBaseDir(), 'builder', appName);
478
+ return getHomeBuilderAppPath(appName);
469
479
  }
470
480
 
471
481
  /**
@@ -630,38 +640,25 @@ function resolveBuildContext(configDir, buildContext) {
630
640
  }
631
641
 
632
642
  /**
633
- * `cwd/builder/<appName>` when present as a directory and allowed (platform empty stubs skipped).
643
+ * `cwd/builder/<appName>` when the running folder has a resolvable application manifest.
634
644
  *
635
645
  * @param {string} appName
636
646
  * @returns {string|null}
637
647
  */
638
- function tryCwdBuilderPathOrNull(appName) {
639
- const cwdApp = path.join(path.resolve(process.cwd()), 'builder', appName);
640
- try {
641
- if (!nodeFs().existsSync(cwdApp)) {
642
- return null;
643
- }
644
- const st = nodeFs().statSync(cwdApp);
645
- if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) {
646
- return null;
647
- }
648
- if (isSystemBuilderAppName(appName) && !isProjectBuilderAppDirectory(cwdApp)) {
649
- return null;
650
- }
651
- return cwdApp;
652
- } catch {
648
+ function getCwdBuilderAppPath(appName) {
649
+ if (!appName || typeof appName !== 'string') {
653
650
  return null;
654
651
  }
652
+ const cwdApp = path.join(path.resolve(process.cwd()), 'builder', appName);
653
+ return isProjectBuilderAppDirectory(cwdApp) ? cwdApp : null;
655
654
  }
656
655
 
657
656
  /**
658
- * Gets the builder folder path:
659
- * 1. Plan 141 Tier‑1: `cwd/builder/<app>` or `cwd/integration/<app>` when a resolvable application manifest exists.
660
- * 2. Else **`cwd/builder/<app>`** when that directory exists. For **platform** apps (`keycloak`,
661
- * `miso-controller`, `dataplane`), an **empty** cwd stub is ignored so materialization under
662
- * `(work|home)/builder` still wins.
663
- * 3. Else **`(aifabrix-work or aifabrix-home)/builder/<appName>`** — used by setup / `up-platform` /
664
- * `up-miso` / `up-dataplane` for template materialization.
657
+ * Gets the builder folder path for run/deploy/register:
658
+ * 1. **`cwd/builder/<app>`** when a resolvable application manifest exists in the running folder.
659
+ * 2. Else **`(aifabrix-work | aifabrix-home)/builder/<app>`** (setup materializes templates there).
660
+ *
661
+ * Teardown and setup `--force` clean use {@link getHomeBuilderAppPath} only (never product-repo trees).
665
662
  *
666
663
  * @param {string} appName - Application name
667
664
  * @returns {string} Absolute path to builder directory
@@ -670,20 +667,11 @@ function getBuilderPath(appName) {
670
667
  if (!appName || typeof appName !== 'string') {
671
668
  throw new Error('App name is required and must be a string');
672
669
  }
673
- const { resolveApplicationManifestPathSync } = require('./manifest-location');
674
- const manifestHit = resolveApplicationManifestPathSync({
675
- targetKey: appName,
676
- mode: 'auto',
677
- cwd: process.cwd()
678
- });
679
- if (manifestHit && (manifestHit.tier === 'cwd-builder' || manifestHit.tier === 'cwd-integration')) {
680
- return manifestHit.absolutePath;
681
- }
682
- const cwdHit = tryCwdBuilderPathOrNull(appName);
670
+ const cwdHit = getCwdBuilderAppPath(appName);
683
671
  if (cwdHit) {
684
672
  return cwdHit;
685
673
  }
686
- return path.join(getAppsMaterializationParent(), 'builder', appName);
674
+ return getHomeBuilderAppPath(appName);
687
675
  }
688
676
 
689
677
  /**
@@ -918,6 +906,8 @@ module.exports = {
918
906
  getIntegrationBuilderBaseDir,
919
907
  SYSTEM_BUILDER_APP_KEYS,
920
908
  isSystemBuilderAppName,
909
+ getHomeBuilderAppPath,
910
+ getCwdBuilderAppPath,
921
911
  getProjectBuilderAppPath,
922
912
  getSystemPlatformMaterializationParent,
923
913
  getSystemBuilderRoot,
@@ -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,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Refresh ~/.aifabrix/config.yaml from GET /api/dev/settings when remote dev auth is available.
3
+ *
4
+ * @fileoverview Merge builder-server settings (developer-root-domain, sync paths, etc.)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const config = require('../core/config');
13
+ const devApi = require('../api/dev.api');
14
+ const { getRemoteDevAuth } = require('./remote-dev-auth');
15
+ const { getCertDir, readClientKeyPem } = require('./dev-cert-helper');
16
+ const { getConfigDirForPaths } = require('./paths');
17
+ const logger = require('./logger');
18
+
19
+ /**
20
+ * @typedef {Object} RefreshRemoteSettingsResult
21
+ * @property {boolean} refreshed
22
+ * @property {string} [reason]
23
+ * @property {Object} [settings]
24
+ * @property {Error} [error]
25
+ */
26
+
27
+ /**
28
+ * Fetch GET /api/dev/settings and merge into config when remote-server + client cert exist.
29
+ *
30
+ * @async
31
+ * @param {{ quiet?: boolean }} [opts]
32
+ * @returns {Promise<RefreshRemoteSettingsResult>}
33
+ */
34
+ async function refreshRemoteSettingsFromBuilderIfPossible(opts = {}) {
35
+ const quiet = opts.quiet !== false;
36
+ const auth = await getRemoteDevAuth();
37
+ if (!auth) {
38
+ return { refreshed: false, reason: 'no-remote-auth' };
39
+ }
40
+
41
+ const devId = await config.getDeveloperId();
42
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
43
+ const clientKeyPem = readClientKeyPem(certDir);
44
+
45
+ try {
46
+ const settings = await devApi.getSettings(
47
+ auth.serverUrl,
48
+ auth.clientCertPem,
49
+ clientKeyPem || undefined,
50
+ auth.serverCaPem || undefined
51
+ );
52
+ await config.mergeRemoteSettings(settings);
53
+ if (!quiet) {
54
+ logger.log(chalk.gray('Synced developer settings from Builder Server'));
55
+ }
56
+ return { refreshed: true, settings };
57
+ } catch (error) {
58
+ if (!quiet) {
59
+ logger.warn(
60
+ chalk.yellow(`Could not refresh settings from Builder Server: ${error.message}`)
61
+ );
62
+ }
63
+ return { refreshed: false, reason: 'fetch-failed', error };
64
+ }
65
+ }
66
+
67
+ module.exports = { refreshRemoteSettingsFromBuilderIfPossible };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Resolve hostname for `${REMOTE_HOST}` in frontDoorRouting.host templates.
3
+ * Prefers `developer-root-domain` from builder-server settings over `remote-server` hostname.
4
+ *
5
+ * @fileoverview App public hostname root (separate from builder API host)
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { hostnameFromUrl } = require('./config-paths');
13
+
14
+ /**
15
+ * Hostname for `${REMOTE_HOST}` expansion (Traefik labels, url:// public, setup health).
16
+ *
17
+ * @param {Object} [input]
18
+ * @param {string|null|undefined} input.remoteServer - Builder API base URL
19
+ * @param {string|null|undefined} input.developerRootDomain - App hostname root from GET /api/dev/settings
20
+ * @returns {string}
21
+ */
22
+ function resolveRemoteHostForFrontDoor(input = {}) {
23
+ const root = String(input.developerRootDomain || '').trim();
24
+ if (root) {
25
+ return root.replace(/^\.+|\.+$/g, '');
26
+ }
27
+ return hostnameFromUrl(input.remoteServer) || '';
28
+ }
29
+
30
+ /**
31
+ * Whether front-door host expansion can resolve `${REMOTE_HOST}` (root domain or remote-server).
32
+ *
33
+ * @param {Object} [input]
34
+ * @returns {boolean}
35
+ */
36
+ function hasFrontDoorRemoteHostContext(input = {}) {
37
+ return Boolean(resolveRemoteHostForFrontDoor(input));
38
+ }
39
+
40
+ module.exports = {
41
+ resolveRemoteHostForFrontDoor,
42
+ hasFrontDoorRemoteHostContext
43
+ };
@@ -24,6 +24,42 @@ function isRunCliNoProxy(options) {
24
24
  return options.noProxy === true;
25
25
  }
26
26
 
27
+ /**
28
+ * Whether `aifabrix run` should persist `applications.<app>.proxy: true`.
29
+ * Proxy / front-door URL hints require infra Traefik (`traefik: true` in config).
30
+ * Direct-port (`setupPlatformMode: single`) always persists false even though Commander defaults `--proxy` on.
31
+ *
32
+ * @param {Object} [options] - Commander action options
33
+ * @param {Object|null|undefined} [userCfg] - Parsed `~/.aifabrix/config.yaml`
34
+ * @returns {boolean}
35
+ */
36
+ function resolveRunProxyPersistEnabled(options, userCfg) {
37
+ if (isRunCliNoProxy(options)) {
38
+ return false;
39
+ }
40
+ return Boolean(userCfg && userCfg.traefik === true);
41
+ }
42
+
43
+ /**
44
+ * Effective no-proxy for env/url expansion during run.
45
+ *
46
+ * @param {Object} [options]
47
+ * @param {Object|null|undefined} [userCfg]
48
+ * @param {boolean} savedProxyHint - `applications.<app>.proxy` from config
49
+ * @returns {boolean}
50
+ */
51
+ function resolveRunEffectiveNoProxy(options, userCfg, savedProxyHint) {
52
+ if (isRunCliNoProxy(options)) {
53
+ return true;
54
+ }
55
+ if (!userCfg || userCfg.traefik !== true) {
56
+ return true;
57
+ }
58
+ return !savedProxyHint;
59
+ }
60
+
27
61
  module.exports = {
28
- isRunCliNoProxy
62
+ isRunCliNoProxy,
63
+ resolveRunProxyPersistEnabled,
64
+ resolveRunEffectiveNoProxy
29
65
  };
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
12
+ const { hasFrontDoorRemoteHostContext } = require('./remote-host-for-front-door');
12
13
  const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
13
14
  const {
14
15
  normalizeSetupPlatformMode,
@@ -23,11 +24,12 @@ const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
23
24
  * @param {Object} opts
24
25
  * @param {string|number|null|undefined} opts.developerIdRaw
25
26
  * @param {string|null|undefined} opts.remoteServer
27
+ * @param {string|null|undefined} [opts.developerRootDomain]
26
28
  * @returns {string}
27
29
  */
28
30
  function expandFrontDoorHostTemplateForUrls(template, opts) {
29
- const { developerIdRaw, remoteServer } = opts || {};
30
- return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer);
31
+ const { developerIdRaw, remoteServer, developerRootDomain } = opts || {};
32
+ return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer, developerRootDomain);
31
33
  }
32
34
 
33
35
  function hostPortForProfile(profile, listenPort, developerIdNum) {
@@ -174,6 +176,7 @@ function buildTraefikPublicBaseIfApplicable(opts) {
174
176
  hostTemplate,
175
177
  developerIdRaw,
176
178
  remoteServer,
179
+ developerRootDomain,
177
180
  profile,
178
181
  developerIdNum
179
182
  } = opts;
@@ -181,7 +184,10 @@ function buildTraefikPublicBaseIfApplicable(opts) {
181
184
  if (!traefik || !pathActive || !hostTemplate || !String(hostTemplate).trim()) {
182
185
  return null;
183
186
  }
184
- if (templateUsesRemoteHostPlaceholder(hostTemplate) && !String(remoteServer || '').trim()) {
187
+ if (
188
+ templateUsesRemoteHostPlaceholder(hostTemplate) &&
189
+ !hasFrontDoorRemoteHostContext({ remoteServer, developerRootDomain })
190
+ ) {
185
191
  // Local full-platform (no remote domain): use the single localhost front door port (3000 + devId×100).
186
192
  return localhostFrontDoorBaseForMissingRemote({
187
193
  profile,
@@ -190,7 +196,8 @@ function buildTraefikPublicBaseIfApplicable(opts) {
190
196
  }
191
197
  const expanded = expandFrontDoorHostTemplateForUrls(hostTemplate, {
192
198
  developerIdRaw,
193
- remoteServer
199
+ remoteServer,
200
+ developerRootDomain
194
201
  });
195
202
  if (!expanded) {
196
203
  return null;