@aifabrix/builder 2.44.6 → 2.45.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 (86) hide show
  1. package/.cursor/rules/cli-layout.mdc +7 -3
  2. package/jest.projects.js +56 -0
  3. package/lib/app/helpers.js +3 -3
  4. package/lib/app/index.js +3 -3
  5. package/lib/app/register.js +7 -6
  6. package/lib/app/restart-display.js +52 -21
  7. package/lib/app/rotate-secret.js +7 -6
  8. package/lib/app/run-helpers.js +15 -8
  9. package/lib/app/run.js +57 -9
  10. package/lib/app/show-display.js +7 -0
  11. package/lib/app/show.js +87 -5
  12. package/lib/build/index.js +9 -5
  13. package/lib/cli/infra-guided.js +42 -27
  14. package/lib/cli/installation-log-command.js +73 -0
  15. package/lib/cli/setup-app.js +11 -1
  16. package/lib/cli/setup-auth.js +94 -49
  17. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  18. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  19. package/lib/cli/setup-infra.js +60 -119
  20. package/lib/cli/setup-platform.js +1 -1
  21. package/lib/cli/setup-utility-resolve.js +132 -0
  22. package/lib/cli/setup-utility.js +65 -51
  23. package/lib/commands/app-logs.js +81 -33
  24. package/lib/commands/auth-config.js +116 -18
  25. package/lib/commands/setup-modes.js +19 -6
  26. package/lib/commands/setup-prompts.js +41 -8
  27. package/lib/commands/setup.js +114 -9
  28. package/lib/commands/teardown.js +54 -5
  29. package/lib/commands/up-common.js +48 -14
  30. package/lib/commands/up-dataplane.js +21 -18
  31. package/lib/commands/up-miso.js +12 -8
  32. package/lib/commands/upload.js +5 -3
  33. package/lib/core/audit-logger.js +1 -34
  34. package/lib/core/config-admin-email.js +56 -0
  35. package/lib/core/config-normalize.js +60 -0
  36. package/lib/core/config-registered-controller-urls.js +54 -0
  37. package/lib/core/config.js +33 -50
  38. package/lib/core/secrets-ensure-infra.js +1 -1
  39. package/lib/core/secrets-env-content.js +86 -90
  40. package/lib/core/secrets-env-declarative-expand.js +170 -0
  41. package/lib/core/secrets-env-write.js +2 -0
  42. package/lib/core/secrets-load.js +106 -102
  43. package/lib/external-system/deploy.js +5 -1
  44. package/lib/internal/node-fs.js +2 -0
  45. package/lib/schema/application-schema.json +4 -0
  46. package/lib/schema/infra.parameter.yaml +10 -0
  47. package/lib/utils/app-config-resolver.js +24 -1
  48. package/lib/utils/applications-config-defaults.js +206 -0
  49. package/lib/utils/auth-config-validator.js +2 -12
  50. package/lib/utils/bash-secret-env.js +1 -1
  51. package/lib/utils/compose-generate-docker-compose.js +111 -6
  52. package/lib/utils/compose-generator.js +17 -8
  53. package/lib/utils/controller-url.js +50 -7
  54. package/lib/utils/env-copy.js +99 -14
  55. package/lib/utils/env-template.js +5 -1
  56. package/lib/utils/health-check-url.js +18 -15
  57. package/lib/utils/health-check.js +7 -5
  58. package/lib/utils/infra-optional-service-flags.js +69 -0
  59. package/lib/utils/installation-log-core.js +282 -0
  60. package/lib/utils/installation-log-record.js +237 -0
  61. package/lib/utils/installation-log.js +123 -0
  62. package/lib/utils/log-redaction.js +105 -0
  63. package/lib/utils/manifest-location.js +164 -0
  64. package/lib/utils/manifest-source-emit.js +162 -0
  65. package/lib/utils/paths.js +238 -89
  66. package/lib/utils/remote-secrets-loader.js +7 -1
  67. package/lib/utils/run-cli-flags.js +29 -0
  68. package/lib/utils/secrets-canonical.js +10 -3
  69. package/lib/utils/secrets-path.js +3 -4
  70. package/lib/utils/secrets-utils.js +20 -10
  71. package/lib/utils/system-builder-root.js +10 -2
  72. package/lib/utils/url-declarative-public-base.js +80 -12
  73. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  74. package/lib/utils/url-declarative-resolve-build.js +24 -393
  75. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  76. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  77. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  78. package/lib/utils/url-declarative-resolve.js +47 -7
  79. package/lib/utils/url-declarative-runtime-base-path.js +21 -1
  80. package/lib/utils/urls-local-registry-scan.js +103 -0
  81. package/lib/utils/urls-local-registry.js +161 -90
  82. package/package.json +3 -1
  83. package/templates/applications/dataplane/application.yaml +4 -0
  84. package/templates/applications/miso-controller/application.yaml +2 -0
  85. package/templates/applications/miso-controller/env.template +27 -29
  86. package/.npmrc.token +0 -1
package/lib/app/show.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  const path = require('path');
16
16
  const logger = require('../utils/logger');
17
- const { detectAppType, resolveApplicationConfigPath } = require('../utils/paths');
17
+ const { detectAppType, resolveApplicationConfigPath, getBuilderPath } = require('../utils/paths');
18
18
  const { loadConfigFile } = require('../utils/config-format');
19
19
  const generator = require('../generator');
20
20
  const { getConfig, normalizeControllerUrl } = require('../core/config');
@@ -38,6 +38,58 @@ const {
38
38
  sanitizeCertificationForJson
39
39
  } = require('./certification-show-enrich');
40
40
 
41
+ /**
42
+ * Attach `runReloadDefault` / `runProxyDefault` from ~/.aifabrix `applications.<appKey>`.
43
+ * @param {Object} summary
44
+ * @param {string} appKey
45
+ * @param {Object} userCfg
46
+ */
47
+ function attachRunDefaultsFromUserConfig(summary, appKey, userCfg) {
48
+ const { isApplicationsReloadDefaultOn, getApplicationsRunProxyHint } = require('../utils/applications-config-defaults');
49
+ if (isApplicationsReloadDefaultOn(userCfg, appKey)) {
50
+ summary.runReloadDefault = true;
51
+ }
52
+ summary.runProxyDefault = getApplicationsRunProxyHint(userCfg, appKey);
53
+ }
54
+
55
+ /**
56
+ * Overlay `application.url` / `application.internalUrl` from declarative url:// rules (same profile as run `.env`, default `docker`).
57
+ * Used for offline and **online** show so controller metadata does not stale local proxy/TLS hints.
58
+ * @param {Object} summary
59
+ * @param {string} appKey
60
+ */
61
+ async function attachDeclarativeUrlsToShowApplication(summary, appKey) {
62
+ if (!summary || summary.isExternal) return;
63
+ const t = summary.application && summary.application.type;
64
+ if (String(t || 'webapp').toLowerCase() === 'external') return;
65
+ try {
66
+ const { resolveDeclarativeShowUrlsForApp } = require('../core/secrets-env-declarative-expand');
67
+ const appPath = getBuilderPath(appKey);
68
+ const variablesPath = resolveApplicationConfigPath(appPath);
69
+ const urls = await resolveDeclarativeShowUrlsForApp(appKey, appPath, variablesPath, 'docker');
70
+ if (!urls) return;
71
+ summary.application.url = urls.publicUrl;
72
+ summary.application.internalUrl = urls.internalUrl;
73
+ } catch {
74
+ /* display-only */
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Local manifest provenance for `--json` (plan 141); null when the app is not on disk locally.
80
+ * @param {string} appKey
81
+ * @returns {Promise<{ tier: string, tierLabel: string, configPath: string }|null>}
82
+ */
83
+ async function tryGetLocalManifestSourceForJson(appKey) {
84
+ try {
85
+ const { getManifestSourcePayload } = require('../utils/manifest-source-emit');
86
+ const { appPath } = await detectAppType(appKey);
87
+ return getManifestSourcePayload(appKey, appPath);
88
+ } catch {
89
+ return { tier: 'unknown', tierLabel: 'unknown', configPath: '' };
90
+ }
91
+ }
92
+
41
93
  /** Truncate deployment key for display */
42
94
  const DEPLOYMENT_KEY_TRUNCATE_LEN = 12;
43
95
 
@@ -651,16 +703,34 @@ async function loadOfflineShowSummary(appKey) {
651
703
  }
652
704
  }
653
705
 
706
+ async function emitShowOfflineManifestLine(appKey, json) {
707
+ if (json) return;
708
+ try {
709
+ const { detectAppType } = require('../utils/paths');
710
+ const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
711
+ const { appPath } = await detectAppType(appKey);
712
+ emitManifestMetadataLineIfTTY(logger, { appKey, appPath, envOnly: false, json: false });
713
+ } catch {
714
+ /* ignore */
715
+ }
716
+ }
717
+
654
718
  async function runOffline(appKey, json, permissionsOnly, verifyCert = false) {
655
719
  const summary = await loadOfflineShowSummary(appKey);
720
+ await emitShowOfflineManifestLine(appKey, json);
721
+ const userCfg = await getConfig();
722
+ attachRunDefaultsFromUserConfig(summary, appKey, userCfg);
723
+ await attachDeclarativeUrlsToShowApplication(summary, appKey);
656
724
 
657
725
  if (json) {
726
+ const manifestSource = await tryGetLocalManifestSourceForJson(appKey);
658
727
  if (permissionsOnly) {
659
728
  const out = {
660
729
  source: summary.source,
661
730
  path: summary.path,
662
731
  appKey: summary.appKey,
663
- permissions: summary.permissions || []
732
+ permissions: summary.permissions || [],
733
+ manifestSource
664
734
  };
665
735
  logger.log(JSON.stringify(out, null, 2));
666
736
  return;
@@ -669,6 +739,9 @@ async function runOffline(appKey, json, permissionsOnly, verifyCert = false) {
669
739
  source: summary.source,
670
740
  path: summary.path,
671
741
  appKey: summary.appKey,
742
+ runReloadDefault: Boolean(summary.runReloadDefault),
743
+ runProxyDefault: Boolean(summary.runProxyDefault),
744
+ manifestSource,
672
745
  application: {
673
746
  ...summary.application,
674
747
  roles: summary.roles,
@@ -717,13 +790,14 @@ async function fetchExternalSystemForOnline(controllerUrl, appKey, authConfig) {
717
790
  }
718
791
  }
719
792
 
720
- function outputOnlineJson(summary, permissionsOnly) {
793
+ function outputOnlineJson(summary, permissionsOnly, manifestSource) {
721
794
  if (permissionsOnly) {
722
795
  const out = {
723
796
  source: summary.source,
724
797
  controllerUrl: summary.controllerUrl,
725
798
  appKey: summary.appKey,
726
- permissions: summary.permissions || []
799
+ permissions: summary.permissions || [],
800
+ manifestSource
727
801
  };
728
802
  logger.log(JSON.stringify(out, null, 2));
729
803
  return;
@@ -733,6 +807,7 @@ function outputOnlineJson(summary, permissionsOnly) {
733
807
  source: summary.source,
734
808
  controllerUrl: summary.controllerUrl,
735
809
  appKey: summary.appKey,
810
+ manifestSource,
736
811
  application: {
737
812
  key: app.key,
738
813
  displayName: app.displayName,
@@ -751,6 +826,10 @@ function outputOnlineJson(summary, permissionsOnly) {
751
826
  }
752
827
  };
753
828
  if (app.version !== undefined && app.version !== null) out.application.version = app.version;
829
+ if (summary.runReloadDefault) {
830
+ out.runReloadDefault = true;
831
+ }
832
+ out.runProxyDefault = Boolean(summary.runProxyDefault);
754
833
  if (summary.externalSystem !== undefined && summary.externalSystem !== null) {
755
834
  out.externalSystem = summary.externalSystem && summary.externalSystem.error
756
835
  ? { error: summary.externalSystem.error }
@@ -787,8 +866,11 @@ async function runOnline(appKey, json, permissionsOnly, verifyCert = false) {
787
866
  token: authConfig.token,
788
867
  controllerUrl: authResult.actualControllerUrl
789
868
  });
869
+ attachRunDefaultsFromUserConfig(summary, appKey, await getConfig());
870
+ await attachDeclarativeUrlsToShowApplication(summary, appKey);
790
871
  if (json) {
791
- outputOnlineJson(summary, permissionsOnly);
872
+ const manifestSource = await tryGetLocalManifestSourceForJson(appKey);
873
+ outputOnlineJson(summary, permissionsOnly, manifestSource);
792
874
  return;
793
875
  }
794
876
  displayShow(summary, { permissionsOnly: !!permissionsOnly });
@@ -196,12 +196,13 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
196
196
 
197
197
  async function postBuildTasks(appName, buildConfig) {
198
198
  try {
199
- const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
200
- logger.log(formatSuccessLine(`Generated .env file: ${envPath}`));
201
- // Note: processEnvVariables is already called by generateEnvFile to generate local .env
202
- // at the envOutputPath, so we don't need to manually copy the docker .env file
199
+ // Validate that env.template + secrets resolve cleanly, but never write <appPath>/.env or
200
+ // envOutputPath. Build args still flow through resolveAndGetEnvMap (in-memory). Run
201
+ // `aifabrix resolve <app>` to materialize an on-disk .env.
202
+ await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker', false, { noWrite: true });
203
+ logger.log(formatSuccessLine('Env resolution validated (in-memory only; run "aifabrix resolve ' + appName + '" for on-disk .env)'));
203
204
  } catch (error) {
204
- logger.log(formatWarningLine(`Could not generate .env file: ${error.message}`));
205
+ logger.log(formatWarningLine(`Could not resolve env: ${error.message}`));
205
206
  }
206
207
  }
207
208
 
@@ -411,6 +412,9 @@ async function resolveDockerBuildArgsForApp(appName) {
411
412
  */
412
413
  async function runStandardDockerBuild(appName, options) {
413
414
  const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
415
+ const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
416
+ const { appPath } = await detectAppType(appName);
417
+ emitManifestMetadataLineIfTTY(logger, { appKey: appName, appPath, envOnly: false, json: false });
414
418
  const { devDir, effectiveImageName, imageName, appConfig } = await prepareDevDirectory(appName, buildConfig, options);
415
419
  const contextPath = prepareBuildContext(buildConfig, devDir);
416
420
  const dockerfilePath = await handleDockerfileGeneration(appName, {
@@ -23,6 +23,9 @@ const { handleLogin } = require('../commands/login');
23
23
  const healthCheck = require('../utils/health-check');
24
24
  const { prepareUrlsLocalRegistryForUpPlatform } = require('../commands/up-common');
25
25
  const { assertDevInfraUp } = require('../commands/dev-infra-gate');
26
+ const {
27
+ emitSystemBuilderAppManifestLineIfTTY
28
+ } = require('../utils/manifest-source-emit');
26
29
 
27
30
  const SEPARATOR = '────────────────────────────────────────';
28
31
 
@@ -67,22 +70,23 @@ async function hasErrorLogs(appName, opts = {}) {
67
70
 
68
71
  async function validateAppErrorLogs(appNames) {
69
72
  const names = Array.isArray(appNames) ? appNames : [];
70
- const results = await Promise.all(
71
- names.map(async(name) => {
72
- try {
73
- const hasErrors = await hasErrorLogs(name, { tailLines: 300 });
74
- return { name, hasErrors };
75
- } catch {
76
- return { name, hasErrors: false };
73
+ const bad = [];
74
+ for (const name of names) {
75
+ try {
76
+ const hasErrors = await hasErrorLogs(name, { tailLines: 300 });
77
+ if (hasErrors) {
78
+ bad.push(name);
77
79
  }
78
- })
79
- );
80
- const bad = results.filter(r => r && r.hasErrors);
81
- if (bad.length === 0) return;
82
-
80
+ } catch {
81
+ // ignore per-app log failures
82
+ }
83
+ }
84
+ if (bad.length === 0) {
85
+ return;
86
+ }
83
87
  logger.log(chalk.yellow('⚠ Some services reported error logs'));
84
- for (const r of bad) {
85
- logger.log(chalk.gray(` Run: aifabrix logs ${r.name} -l error`));
88
+ for (const name of bad) {
89
+ logger.log(chalk.gray(` Run: aifabrix logs ${name} -l error`));
86
90
  }
87
91
  logger.log('');
88
92
  }
@@ -158,7 +162,7 @@ async function computeAppBaseUrl(appName) {
158
162
  return joinUrlPath(publicBase, mount);
159
163
  }
160
164
 
161
- // No Traefik front-door routing: publicBase already includes correct scheme/host/port (localhost or remote-server).
165
+ // No Traefik front-door routing: publicBase is localhost + published port unless Traefik/proxy opts into remote.
162
166
  return String(publicBase).replace(/\/+$/, '');
163
167
  }
164
168
 
@@ -226,7 +230,9 @@ async function getInfraHostAndPorts() {
226
230
  const developerId = await config.getDeveloperId();
227
231
  const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
228
232
  const ports = devConfig.getDevPorts(idNum);
229
- const remoteServer = await config.getRemoteServer();
233
+ const cfg = await config.getConfig();
234
+ const traefikOn = Boolean(cfg && cfg.traefik === true);
235
+ const remoteServer = traefikOn ? await config.getRemoteServer() : null;
230
236
  const host = resolveInfraHost(remoteServer);
231
237
  return { idNum, ports, host };
232
238
  }
@@ -308,6 +314,7 @@ async function runGuidedUpDataplane(options, handleUpDataplane) {
308
314
  await runGuidedAuthStep(handleLogin);
309
315
 
310
316
  logger.log('');
317
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
311
318
  const dpSpin = startSpinner('Starting Dataplane...');
312
319
  await withMutedLogger(() =>
313
320
  handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
@@ -346,6 +353,7 @@ function logGuidedPlatformSetupHeader() {
346
353
  logger.log('');
347
354
  logger.log(title('AI Fabrix Platform Setup'));
348
355
  logger.log(SEPARATOR);
356
+ logger.log(chalk.gray('This may take a few minutes...'));
349
357
  logger.log('');
350
358
  }
351
359
 
@@ -364,32 +372,39 @@ async function runGuidedAuthStep(handleLogin) {
364
372
  if (spin) spin.stop();
365
373
  }
366
374
 
367
- async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, handleLogin, forceCleanSummary = null) {
368
- logGuidedPlatformSetupHeader();
369
- if (forceCleanSummary && forceCleanSummary.forceSummary) {
370
- logUpPlatformForceCleanSummary(forceCleanSummary.forceSummary, forceCleanSummary.cleanedApps);
371
- }
372
-
373
- await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
374
-
375
+ async function runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso) {
376
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'keycloak');
375
377
  const kcSpin = startSpinner('Starting Keycloak...');
376
378
  await withMutedLogger(() => handleUpMiso({ ...options, platformInstall: true }));
377
379
  stopSpinnerSuccess(kcSpin, 'Keycloak ready');
378
380
  logger.log('');
379
-
381
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
380
382
  const mcSpin = startSpinner('Starting Miso Controller...');
381
383
  await waitForAppReady('miso-controller', { timeoutSeconds: 120 });
382
384
  stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
383
385
  logger.log('');
386
+ }
384
387
 
385
- await runGuidedAuthStep(handleLogin);
386
-
388
+ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane) {
387
389
  logger.log('');
390
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
388
391
  const dpSpin = startSpinner('Starting Dataplane...');
389
392
  await withMutedLogger(() =>
390
393
  handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
391
394
  );
392
395
  stopSpinnerSuccess(dpSpin, 'Dataplane ready');
396
+ }
397
+
398
+ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, handleLogin, forceCleanSummary = null) {
399
+ logGuidedPlatformSetupHeader();
400
+ if (forceCleanSummary && forceCleanSummary.forceSummary) {
401
+ logUpPlatformForceCleanSummary(forceCleanSummary.forceSummary, forceCleanSummary.cleanedApps);
402
+ }
403
+
404
+ await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
405
+ await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
406
+ await runGuidedAuthStep(handleLogin);
407
+ await runGuidedDataplaneInstallPhase(options, handleUpDataplane);
393
408
  await validateAppErrorLogs(['miso-controller', 'dataplane']);
394
409
  await logPlatformReadyFooter();
395
410
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared completion hook for infra/platform CLI commands (installation.log).
3
+ *
4
+ * @fileoverview installation.log CLI helper
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const config = require('../core/config');
12
+ const installationLog = require('../utils/installation-log');
13
+
14
+ /**
15
+ * @param {Object} params
16
+ * @param {string} params.command
17
+ * @param {Object} params.options
18
+ * @param {Date} params.startedAt
19
+ * @param {'success'|'failure'} params.outcome
20
+ * @param {Error} [params.error]
21
+ * @param {string[]} [params.platformAppList]
22
+ * @param {Object} [params.cleanup]
23
+ * @param {boolean} [params.upPlatformForce]
24
+ * @param {boolean} [params.omitInfraSection]
25
+ * @returns {Promise<void>}
26
+ */
27
+ async function recordInfraInstallationCommand(params) {
28
+ const {
29
+ command,
30
+ options,
31
+ startedAt,
32
+ outcome,
33
+ error,
34
+ platformAppList,
35
+ cleanup,
36
+ upPlatformForce,
37
+ omitInfraSection
38
+ } = params;
39
+
40
+ const completedAt = new Date();
41
+ let cfg = {};
42
+ try {
43
+ cfg = await config.getConfig();
44
+ } catch {
45
+ cfg = {};
46
+ }
47
+
48
+ try {
49
+ await installationLog.appendInstallationRecord({
50
+ command,
51
+ outcome,
52
+ startedAt,
53
+ completedAt,
54
+ options,
55
+ infra: !omitInfraSection && cfg ? { cfg, options } : undefined,
56
+ platformAppList,
57
+ cleanup,
58
+ upPlatformForce: upPlatformForce === true,
59
+ configExtra: {
60
+ controllerUrl: await installationLog.resolveControllerUrlForLog(),
61
+ adminEmail: await installationLog.resolveAdminEmailPresence()
62
+ },
63
+ error,
64
+ errorCode: error && error.code ? String(error.code) : undefined
65
+ });
66
+ } catch {
67
+ // never block CLI on log failure
68
+ }
69
+ }
70
+
71
+ module.exports = {
72
+ recordInfraInstallationCommand
73
+ };
@@ -207,7 +207,8 @@ Examples:
207
207
  $ aifabrix run myapp --env tst
208
208
  $ aifabrix run myapp --tag v1.0.0
209
209
  $ aifabrix run myapp --base
210
- $ aifabrix run myapp --reload`;
210
+ $ aifabrix run myapp --reload
211
+ $ aifabrix run myapp --no-proxy # same as --proxy false: localhost for Docker declarative public URLs; saves applications.<app>.proxy: false`;
211
212
  program.command('run <app>')
212
213
  .description('Run app locally or on remote Docker host')
213
214
  .option('-p, --port <port>', 'Override local port')
@@ -216,6 +217,15 @@ Examples:
216
217
  .option('-e, --env <env>', 'Environment: dev (default), tst, or pro', 'dev')
217
218
  .option('--base', 'Use manifest base image only (skip local developer-scoped tag preference)')
218
219
  .option('--reload', 'In dev: mount workspace into container (Mutagen only if docker-endpoint is a remote host)')
220
+ .option(
221
+ '--proxy',
222
+ 'Use Traefik/front-door public URL hints when infra has Traefik and application.yaml enables frontDoorRouting (default: on)',
223
+ true
224
+ )
225
+ .option(
226
+ '--no-proxy',
227
+ 'Docker declarative public url://* use localhost + published port (saves applications.<app>.proxy: false); overrides --proxy'
228
+ )
219
229
  .addHelpText('after', runHelp)
220
230
  .action(async(appName, options) => {
221
231
  try {
@@ -23,12 +23,97 @@ Examples:
23
23
  const AUTH_HELP_AFTER = `
24
24
  Without options: show auth status (same as: aifabrix auth status).
25
25
  With --set-controller or --set-environment: write defaults to config.yaml.
26
+ Omit the URL on --set-controller to pick from controllers already stored in config (device logins + default).
27
+ For details on omitting the URL: aifabrix auth set-controller -h
26
28
  Aliases:
27
- aifabrix auth set-controller <url>
29
+ aifabrix auth set-controller [url]
28
30
  aifabrix auth set-environment <env>
29
31
  Subcommand: auth status [--validate] for CI/scripts.
30
32
  `;
31
33
 
34
+ const AUTH_SET_CONTROLLER_HELP_AFTER = `
35
+ Argument:
36
+ [url] Optional. When omitted, the CLI uses controllers already known in your config file
37
+ (the default controller value plus each device-token key). Requires an interactive TTY.
38
+
39
+ Behavior:
40
+ - No saved controllers: error; run aifabrix login first, or pass a URL.
41
+ - One saved controller: if it is already the default, prints confirmation; otherwise sets it.
42
+ - Several saved controllers: interactive list to choose the default.
43
+ - Non-interactive shell: pass the URL explicitly (piped/CI shells cannot pick without a URL).
44
+
45
+ Examples:
46
+ $ aifabrix auth set-controller
47
+ Pick default controller from config (TTY), or set the only saved one.
48
+
49
+ $ aifabrix auth set-controller http://localhost:3600
50
+ Set default controller to that URL (validated; same rules as aifabrix auth --set-controller).
51
+
52
+ $ aifabrix auth --set-controller
53
+ Same as omitting the URL on this subcommand (parent command flag).
54
+ `;
55
+
56
+ function createAuthStatusAction() {
57
+ return async(options) => {
58
+ try {
59
+ await handleAuthStatus(options);
60
+ } catch (error) {
61
+ handleCommandError(error, 'auth status');
62
+ process.exit(1);
63
+ }
64
+ };
65
+ }
66
+
67
+ function createAuthParentAction() {
68
+ return async(options) => {
69
+ try {
70
+ const setController = options.setController || options['set-controller'];
71
+ const setEnvironment = options.setEnvironment || options['set-environment'];
72
+ if (setController || setEnvironment) {
73
+ await handleAuthConfig({
74
+ setController,
75
+ setEnvironment
76
+ });
77
+ return;
78
+ }
79
+ await handleAuthStatus(options);
80
+ } catch (error) {
81
+ handleCommandError(error, 'auth');
82
+ process.exit(1);
83
+ }
84
+ };
85
+ }
86
+
87
+ function registerAuthSetControllerSubcommand(auth) {
88
+ const setControllerCmd = auth.command('set-controller [url]').description(
89
+ 'Set config.controller to a URL, or omit the URL to pick from controllers already stored in config'
90
+ );
91
+ if (typeof setControllerCmd.summary === 'function') {
92
+ setControllerCmd.summary('Set or pick default controller URL in config');
93
+ }
94
+ setControllerCmd.addHelpText('after', AUTH_SET_CONTROLLER_HELP_AFTER).action(async(url) => {
95
+ try {
96
+ await handleAuthConfig({ setController: url || true });
97
+ } catch (error) {
98
+ handleCommandError(error, 'auth set-controller');
99
+ process.exit(1);
100
+ }
101
+ });
102
+ }
103
+
104
+ function registerAuthSetEnvironmentSubcommand(auth) {
105
+ auth.command('set-environment <env>')
106
+ .description('Set default environment in config (alias for auth --set-environment)')
107
+ .action(async(env) => {
108
+ try {
109
+ await handleAuthConfig({ setEnvironment: env });
110
+ } catch (error) {
111
+ handleCommandError(error, 'auth set-environment');
112
+ process.exit(1);
113
+ }
114
+ });
115
+ }
116
+
32
117
  function setupLoginCommand(program) {
33
118
  program.command('login')
34
119
  .description('Sign in to Miso Controller (device or credentials flow)')
@@ -68,63 +153,23 @@ function setupLogoutCommand(program) {
68
153
  }
69
154
 
70
155
  function setupAuthSubcommands(program) {
71
- const authStatusHandler = async(options) => {
72
- try {
73
- await handleAuthStatus(options);
74
- } catch (error) {
75
- handleCommandError(error, 'auth status');
76
- process.exit(1);
77
- }
78
- };
79
156
  const auth = program.command('auth')
80
157
  .description('Show auth status or set default controller/environment')
81
158
  .addHelpText('after', AUTH_HELP_AFTER)
82
- .option('--set-controller <url>', 'Set default controller URL in config')
159
+ .option(
160
+ '--set-controller [url]',
161
+ 'Set default controller URL in config; omit url to choose from controllers registered in config'
162
+ )
83
163
  .option('--set-environment <env>', 'Set default environment in config')
84
- .action(async(options) => {
85
- try {
86
- const setController = options.setController || options['set-controller'];
87
- const setEnvironment = options.setEnvironment || options['set-environment'];
88
- if (setController || setEnvironment) {
89
- await handleAuthConfig({
90
- setController,
91
- setEnvironment
92
- });
93
- return;
94
- }
95
- await handleAuthStatus(options);
96
- } catch (error) {
97
- handleCommandError(error, 'auth');
98
- process.exit(1);
99
- }
100
- });
164
+ .action(createAuthParentAction());
101
165
 
102
- auth.command('set-controller <url>')
103
- .description('Set default controller URL in config (alias for auth --set-controller)')
104
- .action(async(url) => {
105
- try {
106
- await handleAuthConfig({ setController: url });
107
- } catch (error) {
108
- handleCommandError(error, 'auth set-controller');
109
- process.exit(1);
110
- }
111
- });
112
-
113
- auth.command('set-environment <env>')
114
- .description('Set default environment in config (alias for auth --set-environment)')
115
- .action(async(env) => {
116
- try {
117
- await handleAuthConfig({ setEnvironment: env });
118
- } catch (error) {
119
- handleCommandError(error, 'auth set-environment');
120
- process.exit(1);
121
- }
122
- });
166
+ registerAuthSetControllerSubcommand(auth);
167
+ registerAuthSetEnvironmentSubcommand(auth);
123
168
 
124
169
  auth.command('status')
125
170
  .description('Show tokens/session for current controller and environment')
126
171
  .option('--validate', 'Exit with code 1 when not authenticated (for scripted use, e.g. manual test setup)')
127
- .action(authStatusHandler);
172
+ .action(createAuthStatusAction());
128
173
  }
129
174
 
130
175
  /**