@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
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @fileoverview Footer and header layout for guided infra/platform CLI flows.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const chalk = require('chalk');
8
+ const logger = require('../utils/logger');
9
+ const config = require('../core/config');
10
+ const { computeAppBaseUrl, resolvePlatformControllerUrl } = require('../utils/platform-controller-url');
11
+ const { successGlyph } = require('../utils/cli-test-layout-chalk');
12
+
13
+ const SEPARATOR = '────────────────────────────────────────';
14
+
15
+ function title(text) {
16
+ return chalk.bold(text);
17
+ }
18
+
19
+ function logSection(label, value) {
20
+ logger.log(title(label));
21
+ logger.log(` ${value}`);
22
+ logger.log('');
23
+ }
24
+
25
+ function logFooterStart(label) {
26
+ logger.log('');
27
+ logger.log(SEPARATOR);
28
+ logger.log(title(label));
29
+ logger.log(SEPARATOR);
30
+ logger.log('');
31
+ }
32
+
33
+ function logFooterEnd() {
34
+ logger.log(SEPARATOR);
35
+ }
36
+
37
+ /**
38
+ * @async
39
+ * @returns {Promise<string>}
40
+ */
41
+ async function resolvePlatformControllerUrlForDisplay() {
42
+ const stored = await config.getPlatformControllerUrl();
43
+ if (stored) {
44
+ return stored;
45
+ }
46
+ return resolvePlatformControllerUrl();
47
+ }
48
+
49
+ async function logPlatformReadyFooter() {
50
+ const [controllerUrl, dataplaneUrl, keycloakUrl] = await Promise.all([
51
+ resolvePlatformControllerUrlForDisplay(),
52
+ computeAppBaseUrl('dataplane'),
53
+ computeAppBaseUrl('keycloak')
54
+ ]);
55
+ const cfg = await config.getConfig();
56
+ const env = cfg && cfg.environment ? cfg.environment : 'dev';
57
+
58
+ logFooterStart('Platform Ready');
59
+ logSection('Environment', env);
60
+ logSection('Miso Controller', controllerUrl);
61
+ logSection('Dataplane (DEV)', dataplaneUrl);
62
+ logSection('Keycloak', keycloakUrl);
63
+ logSection('API Documentation', `${dataplaneUrl}/api/docs`);
64
+ logSection('Knowledge Base', 'https://docs.aifabrix.ai');
65
+ logSection('Getting Started', 'https://docs.aifabrix.ai/get-started');
66
+ logFooterEnd();
67
+ }
68
+
69
+ async function logMisoReadyFooter() {
70
+ const [controllerUrl, keycloakUrl] = await Promise.all([
71
+ resolvePlatformControllerUrlForDisplay(),
72
+ computeAppBaseUrl('keycloak')
73
+ ]);
74
+ logFooterStart('Miso Ready');
75
+ logSection('Miso Controller', controllerUrl);
76
+ logSection('Keycloak', keycloakUrl);
77
+ logFooterEnd();
78
+ }
79
+
80
+ async function logDataplaneReadyFooter() {
81
+ const dataplaneUrl = await computeAppBaseUrl('dataplane');
82
+ const cfg = await config.getConfig();
83
+ const env = cfg && cfg.environment ? cfg.environment : 'dev';
84
+ logFooterStart('Dataplane Ready');
85
+ logSection('Environment', env);
86
+ logSection('Dataplane (DEV)', dataplaneUrl);
87
+ logSection('API Documentation', `${dataplaneUrl}/api/docs`);
88
+ logFooterEnd();
89
+ }
90
+
91
+ /**
92
+ * @param {'platform'|'dataplane'} [scope]
93
+ */
94
+ function logPlatformSetupDurationHint(scope = 'platform') {
95
+ const line =
96
+ scope === 'dataplane'
97
+ ? 'Installation may take a few minutes. Authenticating and starting Dataplane — please wait.'
98
+ : 'Installation may take a few minutes. Keycloak, Miso Controller, and Dataplane are starting — please wait.';
99
+ logger.log(chalk.yellow(`⚠ ${line}`));
100
+ }
101
+
102
+ function logGuidedPlatformSetupHeader() {
103
+ logger.log('');
104
+ logger.log(title('AI Fabrix Platform Setup'));
105
+ logger.log(SEPARATOR);
106
+ logger.log('');
107
+ logPlatformSetupDurationHint('platform');
108
+ logger.log('');
109
+ }
110
+
111
+ /**
112
+ * @param {{ deviceCleared: number, clientCleared: number, environment: string, defaultControllerUrl: string }} forceSummary
113
+ * @param {string[]} cleanedApps
114
+ */
115
+ function logUpPlatformForceCleanSummary(forceSummary, cleanedApps) {
116
+ const { isSetupQuietOutput } = require('../utils/setup-quiet-output');
117
+ if (isSetupQuietOutput()) return;
118
+ const check = successGlyph();
119
+ logger.log(chalk.bold('Clean installation files'));
120
+ logger.log(
121
+ chalk.gray(
122
+ ` ${check} Cleared ${forceSummary.deviceCleared} device token(s) and ${forceSummary.clientCleared} client token(s)`
123
+ )
124
+ );
125
+ logger.log(chalk.gray(` ${check} Environment set to ${forceSummary.environment}`));
126
+ logger.log(
127
+ chalk.gray(
128
+ ` ${check} Default controller set to ${forceSummary.defaultControllerUrl} (run aifabrix login after platform is up)`
129
+ )
130
+ );
131
+ for (const appName of cleanedApps || []) {
132
+ logger.log(chalk.gray(` ${check} Cleaned builder/${appName}`));
133
+ }
134
+ logger.log('');
135
+ }
136
+
137
+ /**
138
+ * @param {Object} cfg
139
+ */
140
+ function logStoppedServices(cfg) {
141
+ logger.log(title('Stopped'));
142
+ logger.log(' postgres');
143
+ logger.log(' redis');
144
+ if (cfg.pgadmin !== false) logger.log(' pgadmin');
145
+ if (cfg.redisCommander !== false) logger.log(' redis-commander');
146
+ if (cfg.traefik) logger.log(' traefik');
147
+ logger.log('');
148
+ }
149
+
150
+ module.exports = {
151
+ SEPARATOR,
152
+ title,
153
+ logSection,
154
+ logFooterStart,
155
+ logFooterEnd,
156
+ resolvePlatformControllerUrlForDisplay,
157
+ logPlatformReadyFooter,
158
+ logMisoReadyFooter,
159
+ logDataplaneReadyFooter,
160
+ logPlatformSetupDurationHint,
161
+ logGuidedPlatformSetupHeader,
162
+ logUpPlatformForceCleanSummary,
163
+ logStoppedServices
164
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Spinner helpers for guided infra/platform CLI flows.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const ora = require('ora');
8
+ const logger = require('../utils/logger');
9
+ const { formatProgress, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
10
+
11
+ function shouldUseSpinner() {
12
+ return Boolean(process && process.stdout && process.stdout.isTTY);
13
+ }
14
+
15
+ /**
16
+ * @param {string} text
17
+ * @returns {import('ora').Ora|null}
18
+ */
19
+ function startSpinner(text) {
20
+ if (!shouldUseSpinner()) {
21
+ logger.log(formatProgress(text));
22
+ return null;
23
+ }
24
+ return ora({ text, spinner: 'dots' }).start();
25
+ }
26
+
27
+ /**
28
+ * @param {import('ora').Ora|null} spinner
29
+ * @param {string} text
30
+ */
31
+ function stopSpinnerSuccess(spinner, text) {
32
+ if (!spinner) {
33
+ logger.log(formatSuccessLine(text));
34
+ return;
35
+ }
36
+ spinner.succeed(text);
37
+ }
38
+
39
+ /**
40
+ * @param {import('ora').Ora|null} spinner
41
+ * @returns {(text: string) => void}
42
+ */
43
+ function createSpinnerTextUpdater(spinner) {
44
+ return (text) => {
45
+ if (spinner) {
46
+ spinner.text = text;
47
+ return;
48
+ }
49
+ logger.log(formatProgress(text));
50
+ };
51
+ }
52
+
53
+ module.exports = {
54
+ startSpinner,
55
+ stopSpinnerSuccess,
56
+ createSpinnerTextUpdater
57
+ };
@@ -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);
@@ -113,6 +104,9 @@ async function waitForAppReady(appName, opts = {}) {
113
104
  if (userCfg && userCfg.traefik === true) {
114
105
  healthRunOpts.traefikEnabled = true;
115
106
  }
107
+ if (typeof opts.onPollText === 'function') {
108
+ healthRunOpts.onPollText = opts.onPollText;
109
+ }
116
110
  await healthCheck.waitForHealthCheck(appName, timeoutSeconds, hostPort, appConfig, false, healthRunOpts);
117
111
  }
118
112
 
@@ -124,84 +118,10 @@ function resolveInfraHost(remoteServer) {
124
118
  const u = new URL(withScheme);
125
119
  return u.hostname || 'localhost';
126
120
  } catch {
127
- // Fallback for non-URL strings.
128
121
  return raw.replace(/^https?:\/\//i, '').split('/')[0].split(':')[0] || 'localhost';
129
122
  }
130
123
  }
131
124
 
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
125
  async function getInfraHostAndPorts() {
206
126
  const developerId = await config.getDeveloperId();
207
127
  const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
@@ -263,23 +183,21 @@ async function runGuidedUpMiso(options, handleUpMiso) {
263
183
  stopSpinnerSuccess(kcSpin, 'Keycloak ready');
264
184
  logger.log('');
265
185
  const mcSpin = startSpinner('Starting Miso Controller...');
266
- await waitForAppReady('miso-controller', { timeoutSeconds: 120 });
186
+ await waitForAppReady('miso-controller', {
187
+ timeoutSeconds: 120,
188
+ onPollText: createSpinnerTextUpdater(mcSpin)
189
+ });
267
190
  stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
268
191
  await logMisoReadyFooter();
269
192
  await validateAppErrorLogs(['miso-controller']);
270
193
  }
271
194
 
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
195
  async function assertDevInfraForGuidedDataplane() {
277
196
  const infraSpin = startSpinner('Checking infrastructure...');
278
197
  try {
279
198
  await assertDevInfraUp({ quietSuccess: true });
280
199
  stopSpinnerSuccess(infraSpin, 'Infrastructure is up');
281
200
  } catch (infraErr) {
282
- // Match up-miso: one error block from handleCommandError only (no spinner.fail / extra hints).
283
201
  if (infraSpin) infraSpin.stop();
284
202
  throw infraErr;
285
203
  }
@@ -293,17 +211,18 @@ async function runGuidedUpDataplane(options, handleUpDataplane) {
293
211
  logPlatformSetupDurationHint('dataplane');
294
212
  logger.log('');
295
213
  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
214
  await runGuidedAuthStep(handleLogin, {}, { startSpinner, stopSpinnerSuccess });
301
215
 
302
216
  logger.log('');
303
217
  emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
304
218
  const dpSpin = startSpinner('Starting Dataplane...');
305
- await withMutedLogger(() =>
306
- handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
219
+ const controllerUrl = await resolveGuidedAuthControllerUrl({});
220
+ await runGuidedDataplaneWithAuthRecovery(
221
+ { ...options, platformInstall: true, skipInfraCheck: true },
222
+ handleUpDataplane,
223
+ handleLogin,
224
+ controllerUrl,
225
+ { onSpinnerText: createSpinnerTextUpdater(dpSpin) }
307
226
  );
308
227
  stopSpinnerSuccess(dpSpin, 'Dataplane ready');
309
228
  await refreshDataplaneVersionAfterPlatformInstall();
@@ -311,54 +230,6 @@ async function runGuidedUpDataplane(options, handleUpDataplane) {
311
230
  await logDataplaneReadyFooter();
312
231
  }
313
232
 
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
233
  async function refreshDataplaneVersionAfterPlatformInstall() {
363
234
  try {
364
235
  const { tryRefreshDataplaneVersionAfterLogin } = require('../commands/auth-status-dataplane-version');
@@ -377,7 +248,10 @@ async function runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso) {
377
248
  logger.log('');
378
249
  emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
379
250
  const mcSpin = startSpinner('Starting Miso Controller...');
380
- await waitForAppReady('miso-controller', { timeoutSeconds: 120 });
251
+ await waitForAppReady('miso-controller', {
252
+ timeoutSeconds: 120,
253
+ onPollText: createSpinnerTextUpdater(mcSpin)
254
+ });
381
255
  stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
382
256
  logger.log('');
383
257
  }
@@ -386,20 +260,27 @@ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handle
386
260
  logger.log('');
387
261
  emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
388
262
  const dpSpin = startSpinner('Starting Dataplane...');
263
+ const onSpinnerText = createSpinnerTextUpdater(dpSpin);
389
264
  const platformMode = await config.getSetupPlatformMode();
390
265
  if (platformMode === 'full') {
391
- // Full platform: three dataplane containers + per-env registrations.
392
266
  const envKeys = ['pro', 'dev', 'tst'];
393
267
  for (const envKey of envKeys) {
394
268
  await runGuidedDataplaneWithAuthRecovery(
395
269
  { ...options, envKey, runEnvKey: envKey, allowNonDev: true },
396
270
  handleUpDataplane,
397
271
  handleLoginFn,
398
- controllerUrl
272
+ controllerUrl,
273
+ { onSpinnerText }
399
274
  );
400
275
  }
401
276
  } else {
402
- await runGuidedDataplaneWithAuthRecovery(options, handleUpDataplane, handleLoginFn, controllerUrl);
277
+ await runGuidedDataplaneWithAuthRecovery(
278
+ options,
279
+ handleUpDataplane,
280
+ handleLoginFn,
281
+ controllerUrl,
282
+ { onSpinnerText }
283
+ );
403
284
  }
404
285
  stopSpinnerSuccess(dpSpin, 'Dataplane ready');
405
286
  await refreshDataplaneVersionAfterPlatformInstall();
@@ -423,6 +304,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
423
304
  const realmSpin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
424
305
  await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
425
306
  stopSpinnerSuccess(realmSpin, 'Keycloak realm ready');
307
+ logger.log('');
426
308
  }
427
309
  await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
428
310
  const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
@@ -431,17 +313,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
431
313
  await logPlatformReadyFooter();
432
314
  }
433
315
 
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) {
316
+ async function runGuidedDownInfra(appName, options, infraMod, appLib) {
445
317
  logger.log('');
446
318
  logger.log(title('AI Fabrix Shutdown'));
447
319
  logger.log(SEPARATOR);
@@ -453,8 +325,8 @@ async function runGuidedDownInfra(appName, options, infra, appLib) {
453
325
  await appLib.downApp(appName, { volumes: !!options.volumes });
454
326
  return;
455
327
  }
456
- if (options.volumes) await infra.stopInfraWithVolumes();
457
- else await infra.stopInfra();
328
+ if (options.volumes) await infraMod.stopInfraWithVolumes();
329
+ else await infraMod.stopInfra();
458
330
  });
459
331
  stopSpinnerSuccess(spin, 'Infrastructure stopped');
460
332
 
@@ -479,4 +351,3 @@ module.exports = {
479
351
  runGuidedUpPlatform,
480
352
  runGuidedDownInfra
481
353
  };
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(
@@ -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) => {
@@ -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