@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
@@ -10,10 +10,7 @@ const config = require('../core/config');
10
10
  const devConfig = require('../utils/dev-config');
11
11
  const pathsUtil = require('../utils/paths');
12
12
  const { loadConfigFile } = require('../utils/config-format');
13
- const { computeAppBaseUrl, resolvePlatformControllerUrl } = require('../utils/platform-controller-url');
14
13
  const { withMutedLogger } = require('../utils/with-muted-logger');
15
- const { formatProgress, formatSuccessLine, successGlyph } = require('../utils/cli-test-layout-chalk');
16
- const ora = require('ora');
17
14
  const infra = require('../infrastructure');
18
15
  const { execWithDockerEnv } = require('../utils/docker-exec');
19
16
  const { getLogLevel, passesLevelFilter } = require('../commands/app-logs');
@@ -23,6 +20,26 @@ const {
23
20
  runGuidedAuthStep,
24
21
  runGuidedDataplaneWithAuthRecovery
25
22
  } = require('./infra-guided-auth');
23
+ const {
24
+ startSpinner,
25
+ stopSpinnerSuccess,
26
+ createSpinnerTextUpdater
27
+ } = require('./infra-guided-spinners');
28
+ const {
29
+ SEPARATOR,
30
+ title,
31
+ logSection,
32
+ logFooterStart,
33
+ logFooterEnd,
34
+ resolvePlatformControllerUrlForDisplay,
35
+ logPlatformReadyFooter,
36
+ logMisoReadyFooter,
37
+ logDataplaneReadyFooter,
38
+ logPlatformSetupDurationHint,
39
+ logGuidedPlatformSetupHeader,
40
+ logUpPlatformForceCleanSummary,
41
+ logStoppedServices
42
+ } = require('./infra-guided-footers');
26
43
  const healthCheck = require('../utils/health-check');
27
44
  const { prepareUrlsLocalRegistryForUpPlatform } = require('../commands/up-common');
28
45
  const { assertDevInfraUp } = require('../commands/dev-infra-gate');
@@ -30,32 +47,6 @@ const {
30
47
  emitSystemBuilderAppManifestLineIfTTY
31
48
  } = require('../utils/manifest-source-emit');
32
49
 
33
- const SEPARATOR = '────────────────────────────────────────';
34
-
35
- function title(text) {
36
- return chalk.bold(text);
37
- }
38
-
39
- function shouldUseSpinner() {
40
- return Boolean(process && process.stdout && process.stdout.isTTY);
41
- }
42
-
43
- function startSpinner(text) {
44
- if (!shouldUseSpinner()) {
45
- logger.log(formatProgress(text));
46
- return null;
47
- }
48
- return ora({ text, spinner: 'dots' }).start();
49
- }
50
-
51
- function stopSpinnerSuccess(spinner, text) {
52
- if (!spinner) {
53
- logger.log(formatSuccessLine(text));
54
- return;
55
- }
56
- spinner.succeed(text);
57
- }
58
-
59
50
  async function getRunningContainerNameForApp(appName) {
60
51
  const apps = await infra.getAppStatus();
61
52
  const hit = (apps || []).find(a => a && a.name === appName && a.container);
@@ -124,84 +115,10 @@ function resolveInfraHost(remoteServer) {
124
115
  const u = new URL(withScheme);
125
116
  return u.hostname || 'localhost';
126
117
  } catch {
127
- // Fallback for non-URL strings.
128
118
  return raw.replace(/^https?:\/\//i, '').split('/')[0].split(':')[0] || 'localhost';
129
119
  }
130
120
  }
131
121
 
132
- /**
133
- * Prefer stored platform controller URL, then compute from routing rules.
134
- * @async
135
- * @returns {Promise<string>}
136
- */
137
- async function resolvePlatformControllerUrlForDisplay() {
138
- const stored = await config.getPlatformControllerUrl();
139
- if (stored) {
140
- return stored;
141
- }
142
- return resolvePlatformControllerUrl();
143
- }
144
-
145
- function logSection(label, value) {
146
- logger.log(title(label));
147
- logger.log(` ${value}`);
148
- logger.log('');
149
- }
150
-
151
- function logFooterStart(label) {
152
- logger.log('');
153
- logger.log(SEPARATOR);
154
- logger.log(title(label));
155
- logger.log(SEPARATOR);
156
- logger.log('');
157
- }
158
-
159
- function logFooterEnd() {
160
- logger.log(SEPARATOR);
161
- }
162
-
163
- async function logPlatformReadyFooter() {
164
- const [controllerUrl, dataplaneUrl, keycloakUrl] = await Promise.all([
165
- resolvePlatformControllerUrlForDisplay(),
166
- computeAppBaseUrl('dataplane'),
167
- computeAppBaseUrl('keycloak')
168
- ]);
169
- const cfg = await config.getConfig();
170
- const env = (cfg && cfg.environment) ? cfg.environment : 'dev';
171
-
172
- logFooterStart('Platform Ready');
173
- logSection('Environment', env);
174
- logSection('Miso Controller', controllerUrl);
175
- logSection('Dataplane (DEV)', dataplaneUrl);
176
- logSection('Keycloak', keycloakUrl);
177
- logSection('API Documentation', `${dataplaneUrl}/api/docs`);
178
- logSection('Knowledge Base', 'https://docs.aifabrix.ai');
179
- logSection('Getting Started', 'https://docs.aifabrix.ai/get-started');
180
- logFooterEnd();
181
- }
182
-
183
- async function logMisoReadyFooter() {
184
- const [controllerUrl, keycloakUrl] = await Promise.all([
185
- resolvePlatformControllerUrlForDisplay(),
186
- computeAppBaseUrl('keycloak')
187
- ]);
188
- logFooterStart('Miso Ready');
189
- logSection('Miso Controller', controllerUrl);
190
- logSection('Keycloak', keycloakUrl);
191
- logFooterEnd();
192
- }
193
-
194
- async function logDataplaneReadyFooter() {
195
- const dataplaneUrl = await computeAppBaseUrl('dataplane');
196
- const cfg = await config.getConfig();
197
- const env = (cfg && cfg.environment) ? cfg.environment : 'dev';
198
- logFooterStart('Dataplane Ready');
199
- logSection('Environment', env);
200
- logSection('Dataplane (DEV)', dataplaneUrl);
201
- logSection('API Documentation', `${dataplaneUrl}/api/docs`);
202
- logFooterEnd();
203
- }
204
-
205
122
  async function getInfraHostAndPorts() {
206
123
  const developerId = await config.getDeveloperId();
207
124
  const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
@@ -269,17 +186,12 @@ async function runGuidedUpMiso(options, handleUpMiso) {
269
186
  await validateAppErrorLogs(['miso-controller']);
270
187
  }
271
188
 
272
- /**
273
- * Infra must be checked before auth: device login talks to the controller, which needs
274
- * Postgres/Redis when the controller runs locally — a down infra looks like "network error".
275
- */
276
189
  async function assertDevInfraForGuidedDataplane() {
277
190
  const infraSpin = startSpinner('Checking infrastructure...');
278
191
  try {
279
192
  await assertDevInfraUp({ quietSuccess: true });
280
193
  stopSpinnerSuccess(infraSpin, 'Infrastructure is up');
281
194
  } catch (infraErr) {
282
- // Match up-miso: one error block from handleCommandError only (no spinner.fail / extra hints).
283
195
  if (infraSpin) infraSpin.stop();
284
196
  throw infraErr;
285
197
  }
@@ -293,17 +205,18 @@ async function runGuidedUpDataplane(options, handleUpDataplane) {
293
205
  logPlatformSetupDurationHint('dataplane');
294
206
  logger.log('');
295
207
  await assertDevInfraForGuidedDataplane();
296
- // Avoid an outer long-running spinner here: up-dataplane has multiple internal
297
- // phases and may print some output. An outer spinner causes "spinner over spinner"
298
- // rendering artifacts. Instead, do the same single auth step as up-platform, then
299
- // show one spinner for the dataplane install/run phase.
300
208
  await runGuidedAuthStep(handleLogin, {}, { startSpinner, stopSpinnerSuccess });
301
209
 
302
210
  logger.log('');
303
211
  emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
304
212
  const dpSpin = startSpinner('Starting Dataplane...');
305
- await withMutedLogger(() =>
306
- handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
213
+ const controllerUrl = await resolveGuidedAuthControllerUrl({});
214
+ await runGuidedDataplaneWithAuthRecovery(
215
+ { ...options, platformInstall: true, skipInfraCheck: true },
216
+ handleUpDataplane,
217
+ handleLogin,
218
+ controllerUrl,
219
+ { onSpinnerText: createSpinnerTextUpdater(dpSpin) }
307
220
  );
308
221
  stopSpinnerSuccess(dpSpin, 'Dataplane ready');
309
222
  await refreshDataplaneVersionAfterPlatformInstall();
@@ -311,54 +224,6 @@ async function runGuidedUpDataplane(options, handleUpDataplane) {
311
224
  await logDataplaneReadyFooter();
312
225
  }
313
226
 
314
- /**
315
- * Guided-mode summary after `up-platform --force` (printed under the platform header).
316
- * @param {{ deviceCleared: number, clientCleared: number, environment: string, defaultControllerUrl: string }} forceSummary
317
- * @param {string[]} cleanedApps
318
- */
319
- function logUpPlatformForceCleanSummary(forceSummary, cleanedApps) {
320
- const { isSetupQuietOutput } = require('../utils/setup-quiet-output');
321
- if (isSetupQuietOutput()) return;
322
- const check = successGlyph();
323
- logger.log(chalk.bold('Clean installation files'));
324
- logger.log(
325
- chalk.gray(
326
- ` ${check} Cleared ${forceSummary.deviceCleared} device token(s) and ${forceSummary.clientCleared} client token(s)`
327
- )
328
- );
329
- logger.log(chalk.gray(` ${check} Environment set to ${forceSummary.environment}`));
330
- logger.log(
331
- chalk.gray(
332
- ` ${check} Default controller set to ${forceSummary.defaultControllerUrl} (run aifabrix login after platform is up)`
333
- )
334
- );
335
- for (const appName of cleanedApps || []) {
336
- logger.log(chalk.gray(` ${check} Cleaned builder/${appName}`));
337
- }
338
- logger.log('');
339
- }
340
-
341
- /**
342
- * Yellow notice before long-running platform/dataplane install steps (setup and guided up-*).
343
- * @param {'platform'|'dataplane'} [scope]
344
- */
345
- function logPlatformSetupDurationHint(scope = 'platform') {
346
- const line =
347
- scope === 'dataplane'
348
- ? 'Installation may take a few minutes. Authenticating and starting Dataplane — please wait.'
349
- : 'Installation may take a few minutes. Keycloak, Miso Controller, and Dataplane are starting — please wait.';
350
- logger.log(chalk.yellow(`⚠ ${line}`));
351
- }
352
-
353
- function logGuidedPlatformSetupHeader() {
354
- logger.log('');
355
- logger.log(title('AI Fabrix Platform Setup'));
356
- logger.log(SEPARATOR);
357
- logger.log('');
358
- logPlatformSetupDurationHint('platform');
359
- logger.log('');
360
- }
361
-
362
227
  async function refreshDataplaneVersionAfterPlatformInstall() {
363
228
  try {
364
229
  const { tryRefreshDataplaneVersionAfterLogin } = require('../commands/auth-status-dataplane-version');
@@ -386,20 +251,27 @@ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handle
386
251
  logger.log('');
387
252
  emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
388
253
  const dpSpin = startSpinner('Starting Dataplane...');
254
+ const onSpinnerText = createSpinnerTextUpdater(dpSpin);
389
255
  const platformMode = await config.getSetupPlatformMode();
390
256
  if (platformMode === 'full') {
391
- // Full platform: three dataplane containers + per-env registrations.
392
257
  const envKeys = ['pro', 'dev', 'tst'];
393
258
  for (const envKey of envKeys) {
394
259
  await runGuidedDataplaneWithAuthRecovery(
395
260
  { ...options, envKey, runEnvKey: envKey, allowNonDev: true },
396
261
  handleUpDataplane,
397
262
  handleLoginFn,
398
- controllerUrl
263
+ controllerUrl,
264
+ { onSpinnerText }
399
265
  );
400
266
  }
401
267
  } else {
402
- await runGuidedDataplaneWithAuthRecovery(options, handleUpDataplane, handleLoginFn, controllerUrl);
268
+ await runGuidedDataplaneWithAuthRecovery(
269
+ options,
270
+ handleUpDataplane,
271
+ handleLoginFn,
272
+ controllerUrl,
273
+ { onSpinnerText }
274
+ );
403
275
  }
404
276
  stopSpinnerSuccess(dpSpin, 'Dataplane ready');
405
277
  await refreshDataplaneVersionAfterPlatformInstall();
@@ -423,6 +295,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
423
295
  const realmSpin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
424
296
  await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
425
297
  stopSpinnerSuccess(realmSpin, 'Keycloak realm ready');
298
+ logger.log('');
426
299
  }
427
300
  await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
428
301
  const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
@@ -431,17 +304,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
431
304
  await logPlatformReadyFooter();
432
305
  }
433
306
 
434
- function logStoppedServices(cfg) {
435
- logger.log(title('Stopped'));
436
- logger.log(' postgres');
437
- logger.log(' redis');
438
- if (cfg.pgadmin !== false) logger.log(' pgadmin');
439
- if (cfg.redisCommander !== false) logger.log(' redis-commander');
440
- if (cfg.traefik) logger.log(' traefik');
441
- logger.log('');
442
- }
443
-
444
- async function runGuidedDownInfra(appName, options, infra, appLib) {
307
+ async function runGuidedDownInfra(appName, options, infraMod, appLib) {
445
308
  logger.log('');
446
309
  logger.log(title('AI Fabrix Shutdown'));
447
310
  logger.log(SEPARATOR);
@@ -453,8 +316,8 @@ async function runGuidedDownInfra(appName, options, infra, appLib) {
453
316
  await appLib.downApp(appName, { volumes: !!options.volumes });
454
317
  return;
455
318
  }
456
- if (options.volumes) await infra.stopInfraWithVolumes();
457
- else await infra.stopInfra();
319
+ if (options.volumes) await infraMod.stopInfraWithVolumes();
320
+ else await infraMod.stopInfra();
458
321
  });
459
322
  stopSpinnerSuccess(spin, 'Infrastructure stopped');
460
323
 
@@ -479,4 +342,3 @@ module.exports = {
479
342
  runGuidedUpPlatform,
480
343
  runGuidedDownInfra
481
344
  };
482
-
@@ -297,7 +297,7 @@ Examples:
297
297
  .option('--reload', 'In dev: mount workspace into container (Mutagen only if docker-endpoint is a remote host)')
298
298
  .option(
299
299
  '--proxy',
300
- 'Use Traefik/front-door public URL hints when infra has Traefik and application.yaml enables frontDoorRouting (default: on)',
300
+ 'Use Traefik/front-door public URL hints when infra has Traefik (ignored when traefik: false in config)',
301
301
  true
302
302
  )
303
303
  .option(
@@ -326,6 +326,10 @@ function setupBuildRunLogsDownCommands(program) {
326
326
  '--base',
327
327
  'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
328
328
  )
329
+ .option(
330
+ '--from-dev',
331
+ 'With --base: tag the existing developer-scoped image as the base image instead of rebuilding when possible'
332
+ )
329
333
  .action(async(appName, options) => {
330
334
  try {
331
335
  const imageTag = await app.buildApp(appName, options);
@@ -39,6 +39,7 @@ Examples:
39
39
  $ aifabrix dev add --developer-id 02 --name "Jane Doe" --email jane@example.com
40
40
  $ aifabrix dev add --developer-id 03 --name "Build admin" --email admin@example.com --groups admin,developer
41
41
  $ aifabrix dev add --developer-id 04 --name "CI user" --email ci@example.com --groups secret-manager,developer
42
+ $ aifabrix dev add --developer-id 05 --name "Integrator" --email integrator@example.com --groups developer,secret-reader
42
43
 
43
44
  Requires a configured remote Builder Server and an admin client certificate (same machine setup as dev list / dev pin). After add, run dev pin <id> once to create a PIN for aifabrix dev init.
44
45
  `;
@@ -274,7 +275,7 @@ function setupDevListAddCommands(dev) {
274
275
  .requiredOption('--developer-id <id>', 'Unique id, digits only (e.g. 02); used with dev init --developer-id')
275
276
  .requiredOption('--name <name>', 'Display name')
276
277
  .requiredOption('--email <email>', 'Email address')
277
- .option('--groups <items>', 'Comma-separated roles: admin, secret-manager, developer, docker', 'developer')
278
+ .option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker', 'developer')
278
279
  .addHelpText('after', DEV_ADD_HELP_AFTER)
279
280
  .action(async(options) => {
280
281
  try {
@@ -297,7 +298,7 @@ function setupDevUpdatePinDeleteCommands(dev) {
297
298
  .option('--developer-id <id>', 'Developer ID (same as dev add)')
298
299
  .option('--name <name>', 'Display name')
299
300
  .option('--email <email>', 'Email address')
300
- .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, developer, docker)')
301
+ .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, secret-reader, developer, docker)')
301
302
  .addHelpText('after', DEV_UPDATE_HELP_AFTER)
302
303
  .action(async(developerId, options) => {
303
304
  try {
@@ -34,7 +34,8 @@ const {
34
34
 
35
35
  const {
36
36
  computeEffectiveInfraOptionalFlags,
37
- persistMissingInfraOptionalServiceFlags
37
+ persistMissingInfraOptionalServiceFlags,
38
+ resolveEffectiveTlsEnabled
38
39
  } = require('../utils/infra-optional-service-flags');
39
40
 
40
41
  const UP_INFRA_HELP_AFTER = `
@@ -137,7 +138,7 @@ async function runUpInfraCommand(options) {
137
138
  const { passwordBundleFromCliOptions } = require('../core/admin-secrets-bundle');
138
139
  const bundle = passwordBundleFromCliOptions(options);
139
140
  const adminPass = options.adminPassword || options.adminPwd;
140
- const tlsEnabledEffective = cfg.tlsEnabled === true;
141
+ const tlsEnabledEffective = resolveEffectiveTlsEnabled(cfg, options);
141
142
  const effective = computeEffectiveInfraOptionalFlags(cfg, options);
142
143
  await infra.startInfra(developerId, {
143
144
  traefik: effective.traefik,
@@ -29,6 +29,8 @@ What this command does:
29
29
  2) Wipe data — drop every database and DB user, then up-infra + up-platform --force.
30
30
  3) Clean installation files — remove user-local secrets, then up-infra + up-platform --force.
31
31
  4) Update images — pull the latest infra and platform images, then up-infra + up-platform.
32
+ - With --mode fresh: first-time install when no infra is running (non-interactive; use with --admin-email and --admin-password).
33
+ - With --mode update-images: refresh images when infra is already running (non-interactive).
32
34
 
33
35
  AI tool keys are read from the user-local file (~/.aifabrix/secrets.local.yaml) merged with the shared aifabrix-secrets file. If either source already provides keys, the AI tool prompt is skipped.
34
36
 
@@ -59,6 +61,7 @@ function registerSetup(program) {
59
61
  .option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
60
62
  .option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
61
63
  .option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
64
+ .option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
62
65
  .option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
63
66
  .addHelpText('after', SETUP_HELP_AFTER)
64
67
  .action(async(options) => {
@@ -16,6 +16,7 @@ const {
16
16
  getCertDir,
17
17
  readClientCertPem,
18
18
  readClientKeyPem,
19
+ readServerCaPem,
19
20
  getCertValidNotAfter,
20
21
  normalizePemNewlines,
21
22
  mergeCaPemBlocks
@@ -40,6 +41,7 @@ const {
40
41
  formatEnsureServerTrustedFailure
41
42
  } = require('../utils/dev-init-health-messages');
42
43
  const { getBadRequestHint, logCertTroubleshootingHint } = require('../utils/dev-init-cert-hints');
44
+ const { augmentAndSaveCaPemForRemoteDocker } = require('../utils/dev-ca-docker-augment');
43
45
 
44
46
  /**
45
47
  * Install dev CA into the OS trust store. On Linux without sudo, log and return false so the caller can use in-process PEM.
@@ -371,6 +373,11 @@ async function runDevInit(options) {
371
373
  await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
372
374
 
373
375
  await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, serverCaPemForTls || undefined);
376
+ await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
377
+ settings: issueResponse.settings,
378
+ remoteServerUrl: baseUrl,
379
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
380
+ });
374
381
  await _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId, serverCaPemForTls || undefined);
375
382
  const sshInfo = await mergeDevSshConfigAfterInit(baseUrl, devId);
376
383
  logOnboardingFinished(sshInfo);
@@ -413,6 +420,11 @@ async function runCertificateRefresh(auth) {
413
420
  const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
414
421
  await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
415
422
  await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
423
+ await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
424
+ settings: issueResponse.settings,
425
+ remoteServerUrl: auth.serverUrl,
426
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
427
+ });
416
428
  logger.log(formatSuccessLine('Certificate refreshed and config updated from server.\n'));
417
429
  }
418
430
 
@@ -433,6 +445,12 @@ async function fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, de
433
445
  auth.serverCaPem || undefined
434
446
  );
435
447
  await config.mergeRemoteSettings(settings);
448
+ const existingCa = readServerCaPem(getCertDir(getConfigDirForPaths(), devId));
449
+ await augmentAndSaveCaPemForRemoteDocker(getConfigDirForPaths(), devId, existingCa, {
450
+ settings,
451
+ remoteServerUrl: auth.serverUrl,
452
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
453
+ });
436
454
  logger.log(formatSuccessLine('Config updated from server.\n'));
437
455
  await displayDevConfig(devId);
438
456
  }
@@ -96,7 +96,7 @@ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, ex
96
96
  logger.log(chalk.gray(`Environment: ${envKey}`));
97
97
  }
98
98
  logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
99
- } else {
99
+ } else if (!opts.suppressSuccessLine && !opts.guidedInstall) {
100
100
  logger.log(formatSuccessLine('Authentication successful'));
101
101
  }
102
102
  }
@@ -116,8 +116,10 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
116
116
  const spinner = useSpinner
117
117
  ? ora({ text: 'Waiting for approval', spinner: 'dots' }).start()
118
118
  : null;
119
- if (!useSpinner) {
119
+ if (!useSpinner && !opts.onProgressText) {
120
120
  logger.log('Waiting for approval...');
121
+ } else if (opts.onProgressText) {
122
+ opts.onProgressText('Waiting for approval...');
121
123
  }
122
124
 
123
125
  let pollCount = 0;
@@ -125,6 +127,8 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
125
127
  pollCount++;
126
128
  if (spinner) {
127
129
  spinner.text = `Waiting for approval (attempt ${pollCount})...`;
130
+ } else if (opts.onProgressText) {
131
+ opts.onProgressText(`Waiting for approval (attempt ${pollCount})...`);
128
132
  }
129
133
  };
130
134
 
@@ -163,7 +163,10 @@ async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
163
163
  return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope, {
164
164
  compact: Boolean(options.compact),
165
165
  exitOnFailure: options.exitOnFailure !== false,
166
- tokenControllerUrl: options.tokenControllerUrl
166
+ tokenControllerUrl: options.tokenControllerUrl,
167
+ guidedInstall: options.guidedInstall === true,
168
+ onProgressText: options.onProgressText,
169
+ suppressSuccessLine: options.suppressSuccessLine === true
167
170
  });
168
171
  }
169
172
 
@@ -42,11 +42,19 @@ const postgresBootstrap = require('../utils/postgres-platform-bootstrap');
42
42
  const appLib = require('../app');
43
43
  const setupPrompts = require('./setup-prompts');
44
44
  const { ensureSetupPlatformAuth, syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
45
+ const {
46
+ refreshRemoteSettingsFromBuilderIfPossible
47
+ } = require('../utils/refresh-remote-settings');
45
48
  const { handleLogin } = require('./login');
46
49
  const infraGuided = require('../cli/infra-guided');
50
+ const {
51
+ applyPlatformTopologyFlagsToConfig,
52
+ platformTopologyNeedsReconcile
53
+ } = require('../core/setup-platform-mode');
47
54
  const {
48
55
  computeEffectiveInfraOptionalFlags,
49
- persistMissingInfraOptionalServiceFlags
56
+ persistMissingInfraOptionalServiceFlags,
57
+ topologyFlagsForConfig
50
58
  } = require('../utils/infra-optional-service-flags');
51
59
 
52
60
  /** Builder app keys touched by `up-platform --force`. */
@@ -197,11 +205,19 @@ async function maybePromptForExistingBuilderDir({ force, skipBuilderConflictProm
197
205
  */
198
206
  async function startInfraFromConfig(overrides = {}) {
199
207
  await config.ensureSecretsEncryptionKey();
200
- const cfg = await config.getConfig();
208
+ await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
209
+ let cfg = await config.getConfig();
210
+ if (platformTopologyNeedsReconcile(cfg)) {
211
+ applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
212
+ await config.saveConfig(cfg);
213
+ cfg = await config.getConfig();
214
+ }
201
215
  const emailOverride = String(overrides.adminEmail || '').trim();
202
216
  const emailFromConfig = typeof cfg.adminEmail === 'string' ? cfg.adminEmail.trim() : '';
203
217
  const adminEmailMerged = emailOverride || emailFromConfig || undefined;
204
218
  const effective = computeEffectiveInfraOptionalFlags(cfg, {});
219
+ const topology = topologyFlagsForConfig(cfg);
220
+ const tlsEnabled = topology ? topology.tlsEnabled : cfg.tlsEnabled === true;
205
221
  const legacyPwd =
206
222
  overrides.passwordBundle && overrides.passwordBundle.mode === 'single'
207
223
  ? overrides.passwordBundle.password
@@ -215,7 +231,7 @@ async function startInfraFromConfig(overrides = {}) {
215
231
  adminPwd: legacyPwd,
216
232
  adminEmail: adminEmailMerged,
217
233
  passwordBundle: overrides.passwordBundle,
218
- tlsEnabled: cfg.tlsEnabled === true
234
+ tlsEnabled
219
235
  });
220
236
  });
221
237
  await persistMissingInfraOptionalServiceFlags(cfg, effective);
@@ -14,6 +14,14 @@ const { resolvePlatformControllerUrl } = require('../utils/platform-controller-u
14
14
  const { isPlatformAuthValidForController } = require('../utils/controller-url');
15
15
  const { infoLine } = require('../utils/cli-test-layout-chalk');
16
16
  const logger = require('../utils/logger');
17
+ const chalk = require('chalk');
18
+ const {
19
+ refreshRemoteSettingsFromBuilderIfPossible
20
+ } = require('../utils/refresh-remote-settings');
21
+ const {
22
+ applyPlatformTopologyFlagsToConfig,
23
+ platformTopologyNeedsReconcile
24
+ } = require('../core/setup-platform-mode');
17
25
 
18
26
  /**
19
27
  * Recompute `controller` and `platform-controller` from traefik / setupPlatformMode / front-door config.
@@ -24,6 +32,23 @@ const logger = require('../utils/logger');
24
32
  * @returns {Promise<string>} Normalized platform controller URL
25
33
  */
26
34
  async function syncPlatformControllerUrlsInConfig() {
35
+ const refresh = await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
36
+ const cfg = await config.getConfig();
37
+ if (platformTopologyNeedsReconcile(cfg)) {
38
+ applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
39
+ await config.saveConfig(cfg);
40
+ }
41
+ const remoteServer = await config.getRemoteServer();
42
+ const rootDomain = await config.getDeveloperRootDomain();
43
+ if (remoteServer && !rootDomain && !refresh.refreshed) {
44
+ logger.warn(
45
+ chalk.yellow(
46
+ 'Platform URLs may be wrong: developer-root-domain is missing. ' +
47
+ 'Run "aifabrix dev refresh" (or "aifabrix dev init") to sync from the Builder Server.'
48
+ )
49
+ );
50
+ }
51
+
27
52
  const platformControllerUrl = await resolvePlatformControllerUrl();
28
53
  await config.setPlatformControllerUrl(platformControllerUrl);
29
54
  await config.setControllerUrl(platformControllerUrl);
@@ -225,6 +225,15 @@ async function promptAdminCredentialsWithProfile(options = {}) {
225
225
  await config.setSetupInstallationProfile(profile);
226
226
 
227
227
  const adminEmail = await promptAdminEmail(options);
228
+ const cliPassword = String(options.adminPassword || '').trim();
229
+ if (cliPassword) {
230
+ validatePassword(cliPassword);
231
+ const passwordBundle = isProInstallationProfile(profile)
232
+ ? singlePasswordBundle(cliPassword)
233
+ : singlePasswordBundle(cliPassword);
234
+ return { adminEmail, passwordBundle, profile };
235
+ }
236
+
228
237
  const passwordBundle = isProInstallationProfile(profile)
229
238
  ? await promptProPasswordBundle(options)
230
239
  : await promptDevPasswordBundle();
@@ -9,12 +9,14 @@
9
9
  'use strict';
10
10
 
11
11
  const inquirer = require('inquirer');
12
+ const config = require('../core/config');
12
13
  const {
13
14
  normalizeSetupPlatformMode,
14
15
  SETUP_PLATFORM_MODE,
15
16
  SETUP_PLATFORM_MODE_CLI_VALUES,
16
17
  getSetupPlatformModePromptChoices
17
18
  } = require('../core/setup-platform-mode');
19
+ const { isRemoteServerConfigured } = require('../utils/applications-config-defaults');
18
20
 
19
21
  /**
20
22
  * Resolve platform mode from CLI options, if provided.
@@ -51,12 +53,20 @@ async function promptSetupPlatformMode(options = {}) {
51
53
  const fromCli = resolvePlatformModeFromOptions(options);
52
54
  if (fromCli) return fromCli;
53
55
 
56
+ const remoteServer = await config.getRemoteServer();
57
+ const choices = getSetupPlatformModePromptChoices({
58
+ remoteServerConfigured: isRemoteServerConfigured(remoteServer)
59
+ });
60
+ if (choices.length === 1) {
61
+ return SETUP_PLATFORM_MODE.SINGLE;
62
+ }
63
+
54
64
  const { mode } = await inquirer.prompt([
55
65
  {
56
66
  type: 'list',
57
67
  name: 'mode',
58
68
  message: 'Platform mode:',
59
- choices: getSetupPlatformModePromptChoices(),
69
+ choices,
60
70
  default: SETUP_PLATFORM_MODE.SINGLE
61
71
  }
62
72
  ]);