@aifabrix/builder 2.42.0 → 2.43.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 (133) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/api/wizard.api.js +2 -1
  38. package/lib/app/index.js +2 -2
  39. package/lib/app/prompts.js +2 -2
  40. package/lib/app/readme.js +3 -1
  41. package/lib/app/register.js +3 -1
  42. package/lib/app/rotate-secret.js +3 -0
  43. package/lib/cli/setup-app.js +5 -5
  44. package/lib/cli/setup-auth.js +19 -11
  45. package/lib/cli/setup-dev.js +62 -32
  46. package/lib/cli/setup-environment.js +6 -21
  47. package/lib/cli/setup-infra.js +13 -0
  48. package/lib/cli/setup-secrets.js +45 -6
  49. package/lib/cli/setup-service-user.js +146 -20
  50. package/lib/cli/setup-utility.js +12 -0
  51. package/lib/commands/auth-config.js +25 -19
  52. package/lib/commands/datasource.js +46 -1
  53. package/lib/commands/dev-init.js +1 -1
  54. package/lib/commands/repair-env-template.js +14 -8
  55. package/lib/commands/repair-rbac.js +25 -19
  56. package/lib/commands/repair.js +108 -31
  57. package/lib/commands/secrets-remove.js +1 -1
  58. package/lib/commands/secrets-set.js +6 -0
  59. package/lib/commands/secrets-validate.js +17 -4
  60. package/lib/commands/service-user.js +231 -2
  61. package/lib/commands/up-common.js +25 -0
  62. package/lib/commands/up-dataplane.js +91 -7
  63. package/lib/commands/wizard-core-helpers.js +5 -2
  64. package/lib/commands/wizard-core.js +2 -1
  65. package/lib/commands/wizard-headless.js +6 -1
  66. package/lib/commands/wizard.js +13 -6
  67. package/lib/core/admin-secrets.js +2 -0
  68. package/lib/core/config.js +7 -5
  69. package/lib/core/ensure-encryption-key.js +1 -3
  70. package/lib/core/secrets.js +32 -9
  71. package/lib/core/templates.js +1 -1
  72. package/lib/datasource/abac-validator.js +157 -0
  73. package/lib/datasource/field-reference-validator.js +74 -36
  74. package/lib/datasource/log-viewer.js +221 -0
  75. package/lib/datasource/resolve-app.js +109 -0
  76. package/lib/datasource/test-e2e.js +11 -20
  77. package/lib/datasource/test-integration.js +42 -22
  78. package/lib/datasource/validate.js +5 -2
  79. package/lib/external-system/download-helpers.js +3 -1
  80. package/lib/external-system/generator.js +12 -8
  81. package/lib/external-system/test-system-level.js +1 -1
  82. package/lib/generator/external-controller-manifest.js +3 -3
  83. package/lib/generator/external-schema-utils.js +3 -1
  84. package/lib/generator/external.js +7 -7
  85. package/lib/generator/helpers.js +13 -9
  86. package/lib/generator/index.js +4 -4
  87. package/lib/generator/split.js +45 -10
  88. package/lib/generator/wizard-prompts-secondary.js +39 -7
  89. package/lib/generator/wizard-readme.js +4 -1
  90. package/lib/generator/wizard.js +68 -53
  91. package/lib/infrastructure/helpers.js +50 -35
  92. package/lib/infrastructure/index.js +39 -23
  93. package/lib/schema/env-config.yaml +19 -2
  94. package/lib/schema/external-datasource.schema.json +11 -1
  95. package/lib/schema/wizard-config.schema.json +7 -1
  96. package/lib/utils/app-config-resolver.js +23 -1
  97. package/lib/utils/config-paths.js +48 -4
  98. package/lib/utils/credential-secrets-env.js +16 -1
  99. package/lib/utils/env-map.js +7 -3
  100. package/lib/utils/error-formatter.js +37 -0
  101. package/lib/utils/external-env-template.js +180 -0
  102. package/lib/utils/external-readme.js +33 -1
  103. package/lib/utils/external-system-display.js +43 -0
  104. package/lib/utils/external-system-validators.js +2 -2
  105. package/lib/utils/help-builder.js +3 -5
  106. package/lib/utils/local-secrets.js +26 -3
  107. package/lib/utils/paths.js +2 -1
  108. package/lib/utils/secrets-generator.js +2 -2
  109. package/lib/utils/secrets-utils.js +4 -0
  110. package/lib/utils/secure-file-permissions.js +91 -0
  111. package/lib/utils/token-manager.js +36 -3
  112. package/lib/utils/yaml-preserve.js +59 -1
  113. package/lib/validation/env-template-auth.js +50 -2
  114. package/lib/validation/external-manifest-validator.js +8 -0
  115. package/lib/validation/validate.js +8 -0
  116. package/lib/validation/validator.js +10 -13
  117. package/package.json +6 -2
  118. package/templates/applications/dataplane/env.template +5 -1
  119. package/templates/applications/miso-controller/application.yaml +1 -1
  120. package/templates/applications/miso-controller/env.template +13 -2
  121. package/templates/external-system/README.md.hbs +18 -5
  122. package/templates/external-system/env.template.hbs +22 -0
  123. package/integration/hubspot/README.md +0 -100
  124. package/integration/hubspot/env.template +0 -4
  125. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  126. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  127. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  128. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  129. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  130. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  131. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  132. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  133. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
package/lib/app/readme.js CHANGED
@@ -150,6 +150,7 @@ function generateReadmeMd(appName, config) {
150
150
  const datasources = Array.isArray(config.datasources) && config.datasources.length > 0
151
151
  ? config.datasources
152
152
  : buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount, fileExt);
153
+ const authType = config.authentication?.type || config.authentication?.method || config.authType;
153
154
  return generateExternalReadmeContent({
154
155
  appName,
155
156
  systemKey,
@@ -157,7 +158,8 @@ function generateReadmeMd(appName, config) {
157
158
  displayName: config.systemDisplayName,
158
159
  description: config.systemDescription,
159
160
  fileExt: config.fileExt,
160
- datasources
161
+ datasources,
162
+ authType
161
163
  });
162
164
  }
163
165
  const context = buildReadmeContext(appName, config);
@@ -150,7 +150,9 @@ async function registerApplication(appKey, options = {}) {
150
150
  logger.log(chalk.blue('📋 Registering application...\n'));
151
151
 
152
152
  const { resolveControllerUrl } = require('../utils/controller-url');
153
- const { resolveEnvironment } = require('../core/config');
153
+ const config = require('../core/config');
154
+ await config.ensureSecretsEncryptionKey();
155
+ const { resolveEnvironment } = config;
154
156
 
155
157
  // Load application config
156
158
  const { variables, created } = await loadVariablesYaml(appKey);
@@ -339,6 +339,9 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
339
339
  async function rotateSecret(appKey, _options = {}) {
340
340
  logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
341
341
 
342
+ const { ensureSecretsEncryptionKey } = require('../core/config');
343
+ await ensureSecretsEncryptionKey();
344
+
342
345
  const { controllerUrl, environment } = await resolveControllerAndEnvironment();
343
346
  const config = await getConfig();
344
347
  const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
@@ -84,14 +84,14 @@ async function handleCreateCommand(appName, options) {
84
84
  const wizardOptions = { app: appName, ...options };
85
85
  const normalizedOptions = normalizeExternalOptions(options);
86
86
 
87
- const isExternalType = options.type === 'external';
87
+ const isExternalType = options.type === 'external' || !options.type;
88
88
  const isNonInteractive = process.stdin && process.stdin.isTTY === false;
89
89
 
90
90
  if (isExternalType && !options.wizard && isNonInteractive) {
91
91
  validateNonInteractiveExternalOptions(normalizedOptions);
92
92
  }
93
93
 
94
- const shouldUseWizard = options.wizard && (options.type === 'external' || (!options.type && validTypes.includes('external')));
94
+ const shouldUseWizard = options.wizard && (options.type === 'external' || !options.type);
95
95
  if (shouldUseWizard) {
96
96
  const { handleWizard } = require('../commands/wizard');
97
97
  await handleWizard(wizardOptions);
@@ -110,7 +110,7 @@ function setupCreateCommand(program) {
110
110
  .option('-a, --authentication', 'Requires authentication/RBAC')
111
111
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
112
112
  .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
113
- .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
113
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'external')
114
114
  .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
115
115
  .option('-g, --github', 'Generate GitHub Actions workflows')
116
116
  .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
@@ -140,12 +140,12 @@ Examples:
140
140
  $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
141
141
  $ aifabrix wizard -a my-integration Same as above (app name set)
142
142
  $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
143
- $ aifabrix wizard hubspot-test-v2 --debug Enable debug output and save debug manifests on validation failure
143
+ $ aifabrix wizard hubspot-test --debug Enable debug output and save debug manifests on validation failure
144
144
 
145
145
  Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
146
146
  To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
147
147
  Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
148
- See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`;
148
+ See integration/hubspot-test/wizard-hubspot-e2e.yaml for an example.`;
149
149
  program.command('wizard [appName]')
150
150
  .description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
151
151
  .option('-a, --app <app>', 'Application name (synonym for positional appName)')
@@ -60,23 +60,31 @@ function setupAuthSubcommands(program) {
60
60
  process.exit(1);
61
61
  }
62
62
  };
63
- const auth = program.command('auth').description('Authentication commands');
64
- auth.command('status')
65
- .description('Display authentication status for current controller and environment')
66
- .option('--validate', 'Exit with code 1 when not authenticated (for scripted use, e.g. manual test setup)')
67
- .action(authStatusHandler);
68
- auth.command('config')
69
- .description('Configure authentication settings (controller, environment)')
70
- .option('--set-controller <url>', 'Set default controller URL')
71
- .option('--set-environment <env>', 'Set default environment')
63
+ const auth = program.command('auth')
64
+ .description('Authentication status and config (controller, environment)')
65
+ .option('--set-controller <url>', 'Set default controller URL in config')
66
+ .option('--set-environment <env>', 'Set default environment in config')
72
67
  .action(async(options) => {
73
68
  try {
74
- await handleAuthConfig(options);
69
+ const setController = options.setController || options['set-controller'];
70
+ const setEnvironment = options.setEnvironment || options['set-environment'];
71
+ if (setController || setEnvironment) {
72
+ await handleAuthConfig({
73
+ setController,
74
+ setEnvironment
75
+ });
76
+ return;
77
+ }
78
+ await handleAuthStatus(options);
75
79
  } catch (error) {
76
- handleCommandError(error, 'auth config');
80
+ handleCommandError(error, 'auth');
77
81
  process.exit(1);
78
82
  }
79
83
  });
84
+ auth.command('status')
85
+ .description('Display authentication status for current controller and environment')
86
+ .option('--validate', 'Exit with code 1 when not authenticated (for scripted use, e.g. manual test setup)')
87
+ .action(authStatusHandler);
80
88
  }
81
89
 
82
90
  /**
@@ -73,51 +73,26 @@ async function handleSetFormat(format) {
73
73
  }
74
74
 
75
75
  /**
76
- * Register dev config and set-id commands.
76
+ * Register dev show and set-id commands.
77
77
  * @param {Command} dev - dev subcommand group
78
78
  */
79
- function setupDevConfigCommands(dev) {
79
+ function setupDevShowAndSetId(dev) {
80
80
  dev
81
- .command('config')
82
- .description('Show or set developer configuration')
83
- .option('--set-id <id>', 'Set developer ID')
84
- .action(async(options) => {
81
+ .command('show')
82
+ .description('Show developer configuration (ports and config vars)')
83
+ .action(async() => {
85
84
  try {
86
- const setIdValue = options.setId || options['set-id'];
87
- if (setIdValue) {
88
- const digitsOnly = /^[0-9]+$/.test(setIdValue);
89
- if (!digitsOnly) {
90
- throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
91
- }
92
- await config.setDeveloperId(setIdValue);
93
- process.env.AIFABRIX_DEVELOPERID = setIdValue;
94
- logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
95
- await displayDevConfig(setIdValue);
96
- return;
97
- }
98
85
  const devId = await config.getDeveloperId();
99
86
  await displayDevConfig(devId);
100
87
  } catch (error) {
101
- handleCommandError(error, 'dev config');
102
- process.exit(1);
103
- }
104
- });
105
-
106
- dev
107
- .command('set-format <format>')
108
- .description('Set default output format for download/convert (json | yaml); used when --format not passed')
109
- .action(async(format) => {
110
- try {
111
- await handleSetFormat(format);
112
- } catch (error) {
113
- handleCommandError(error, 'dev set-format');
88
+ handleCommandError(error, 'dev show');
114
89
  process.exit(1);
115
90
  }
116
91
  });
117
92
 
118
93
  dev
119
94
  .command('set-id <id>')
120
- .description('Set developer ID (convenience alias for "dev config --set-id")')
95
+ .description('Set developer ID (0 = default infra, > 0 = developer-specific)')
121
96
  .action(async(id) => {
122
97
  try {
123
98
  const digitsOnly = /^[0-9]+$/.test(id);
@@ -135,6 +110,61 @@ function setupDevConfigCommands(dev) {
135
110
  });
136
111
  }
137
112
 
113
+ /**
114
+ * Register dev set-env-config, set-home and set-format commands.
115
+ * @param {Command} dev - dev subcommand group
116
+ */
117
+ function setupDevPathAndFormatCommands(dev) {
118
+ dev
119
+ .command('set-env-config <filePath>')
120
+ .description('Set aifabrix-env-config path in config.yaml (pass empty to clear; path is not checked for existence)')
121
+ .action(async(filePath) => {
122
+ try {
123
+ const trimmed = (filePath || '').trim();
124
+ await config.setAifabrixEnvConfigPath(trimmed);
125
+ logger.log(trimmed === '' ? chalk.green('✓ Env config path cleared') : chalk.green(`✓ Env config path set to ${trimmed}`));
126
+ } catch (error) {
127
+ handleCommandError(error, 'dev set-env-config');
128
+ process.exit(1);
129
+ }
130
+ });
131
+
132
+ dev
133
+ .command('set-home <path>')
134
+ .description('Set aifabrix-home path in config.yaml (pass empty string to clear)')
135
+ .action(async(homePath) => {
136
+ try {
137
+ const trimmed = (homePath || '').trim();
138
+ await config.setAifabrixHomeOverride(trimmed);
139
+ logger.log(trimmed === '' ? chalk.green('✓ Home path cleared') : chalk.green(`✓ Home path set to ${trimmed}`));
140
+ } catch (error) {
141
+ handleCommandError(error, 'dev set-home');
142
+ process.exit(1);
143
+ }
144
+ });
145
+
146
+ dev
147
+ .command('set-format <format>')
148
+ .description('Set default output format for download/convert (json | yaml); used when --format not passed')
149
+ .action(async(format) => {
150
+ try {
151
+ await handleSetFormat(format);
152
+ } catch (error) {
153
+ handleCommandError(error, 'dev set-format');
154
+ process.exit(1);
155
+ }
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Register dev show, set-id, set-env-config, set-home and set-format commands.
161
+ * @param {Command} dev - dev subcommand group
162
+ */
163
+ function setupDevConfigCommands(dev) {
164
+ setupDevShowAndSetId(dev);
165
+ setupDevPathAndFormatCommands(dev);
166
+ }
167
+
138
168
  /**
139
169
  * Register dev init and refresh commands.
140
170
  * @param {Command} dev - dev subcommand group
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI environment deployment command setup (environment deploy, env deploy).
2
+ * CLI environment deployment command setup (env deploy).
3
3
  *
4
4
  * @fileoverview Environment command definitions for AI Fabrix Builder CLI
5
5
  * @author AI Fabrix Team
@@ -18,35 +18,20 @@ function setupEnvironmentCommands(program) {
18
18
  const environmentDeploy = require('../deployment/environment');
19
19
  await environmentDeploy.deployEnvironment(envKey, options);
20
20
  } catch (error) {
21
- handleCommandError(error, 'environment deploy');
21
+ handleCommandError(error, 'env deploy');
22
22
  process.exit(1);
23
23
  }
24
24
  };
25
25
 
26
- const environment = program
27
- .command('environment')
28
- .description('Deploy and manage Miso Controller environments (dev, tst, pro, miso)');
29
-
30
26
  const deployExamples = `
31
27
  Examples:
32
- $ aifabrix environment deploy dev
33
- $ aifabrix environment deploy tst --preset m
34
- $ aifabrix environment deploy dev --config ./env-config.json --no-poll`;
35
-
36
- environment
37
- .command('deploy <env>')
38
- .description('Deploy environment infrastructure in Miso Controller (run before deploy <app>)')
39
- .option('--config <file>', 'Environment configuration file (optional if --preset is used)')
40
- .option('--preset <size>', 'Environment size preset: s, m, l, xl (default: s)', 's')
41
- .option('--skip-validation', 'Skip environment validation')
42
- .option('--poll', 'Poll for deployment status', true)
43
- .option('--no-poll', 'Do not poll for status')
44
- .addHelpText('after', deployExamples)
45
- .action(deployEnvHandler);
28
+ $ aifabrix env deploy dev
29
+ $ aifabrix env deploy tst --preset m
30
+ $ aifabrix env deploy dev --config ./env-config.json --no-poll`;
46
31
 
47
32
  const env = program
48
33
  .command('env')
49
- .description('Deploy and manage Miso Controller environments (alias for environment)');
34
+ .description('Deploy and manage Miso Controller environments (dev, tst, pro, miso)');
50
35
 
51
36
  env
52
37
  .command('deploy <env>')
@@ -17,6 +17,7 @@ const { resolveControllerUrl } = require('../utils/controller-url');
17
17
  const { handleLogin } = require('../commands/login');
18
18
  const { handleUpMiso } = require('../commands/up-miso');
19
19
  const { handleUpDataplane } = require('../commands/up-dataplane');
20
+ const { cleanBuilderAppDirs } = require('../commands/up-common');
20
21
 
21
22
  /**
22
23
  * Persists optional service flag to config when explicitly set.
@@ -108,8 +109,12 @@ function setupUpPlatformCommand(program) {
108
109
  .option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
109
110
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
110
111
  .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
112
+ .option('-f, --force', 'Clean builder/keycloak, builder/miso-controller, builder/dataplane and re-fetch from templates')
111
113
  .action(async(options) => {
112
114
  try {
115
+ if (options.force) {
116
+ await cleanBuilderAppDirs(['keycloak', 'miso-controller', 'dataplane']);
117
+ }
113
118
  await handleUpMiso(options);
114
119
  await handleUpDataplane(options);
115
120
  } catch (error) {
@@ -137,8 +142,12 @@ function setupUpMisoCommand(program) {
137
142
  .option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
138
143
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
139
144
  .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
145
+ .option('-f, --force', 'Clean builder/keycloak and builder/miso-controller and re-fetch from templates')
140
146
  .action(async(options) => {
141
147
  try {
148
+ if (options.force) {
149
+ await cleanBuilderAppDirs(['keycloak', 'miso-controller']);
150
+ }
142
151
  await handleUpMiso(options);
143
152
  } catch (error) {
144
153
  handleCommandError(error, 'up-miso');
@@ -153,8 +162,12 @@ function setupUpDataplaneCommand(program) {
153
162
  .option('-r, --registry <url>', 'Override registry for dataplane image')
154
163
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
155
164
  .option('-i, --image <ref>', 'Override dataplane image reference (e.g. myreg/dataplane:latest)')
165
+ .option('-f, --force', 'Clean builder/dataplane and re-fetch from templates')
156
166
  .action(async(options) => {
157
167
  try {
168
+ if (options.force) {
169
+ await cleanBuilderAppDirs(['dataplane']);
170
+ }
158
171
  await handleUpDataplane(options);
159
172
  } catch (error) {
160
173
  if (isAuthenticationError(error)) {
@@ -6,12 +6,15 @@
6
6
  * @version 2.0.0
7
7
  */
8
8
 
9
+ const chalk = require('chalk');
9
10
  const { handleCommandError } = require('../utils/cli-utils');
10
11
  const { handleSecretsSet } = require('../commands/secrets-set');
11
12
  const { handleSecretsList } = require('../commands/secrets-list');
12
13
  const { handleSecretsRemove } = require('../commands/secrets-remove');
13
14
  const { handleSecretsValidate } = require('../commands/secrets-validate');
14
15
  const { handleSecure } = require('../commands/secure');
16
+ const config = require('../core/config');
17
+ const logger = require('../utils/logger');
15
18
 
16
19
  function setupSecretValidateCommand(secretCmd) {
17
20
  secretCmd
@@ -20,6 +23,7 @@ function setupSecretValidateCommand(secretCmd) {
20
23
  .option('--naming', 'Check key names against *KeyVault convention')
21
24
  .action(async(pathArg, options) => {
22
25
  try {
26
+ await config.ensureSecretsEncryptionKey();
23
27
  const result = await handleSecretsValidate(pathArg, options);
24
28
  if (!result.valid) process.exit(1);
25
29
  } catch (error) {
@@ -29,6 +33,25 @@ function setupSecretValidateCommand(secretCmd) {
29
33
  });
30
34
  }
31
35
 
36
+ /**
37
+ * Registers the secure command on the program.
38
+ * @param {Command} program - Commander program instance
39
+ */
40
+ function setupSecureCommand(program) {
41
+ program.command('secure')
42
+ .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
43
+ .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
44
+ .action(async(options) => {
45
+ try {
46
+ await config.ensureSecretsEncryptionKey();
47
+ await handleSecure(options);
48
+ } catch (error) {
49
+ handleCommandError(error, 'secure');
50
+ process.exit(1);
51
+ }
52
+ });
53
+ }
54
+
32
55
  /**
33
56
  * Sets up secrets and security commands
34
57
  * @param {Command} program - Commander program instance
@@ -44,6 +67,7 @@ function setupSecretsCommands(program) {
44
67
  .option('--shared', 'List shared secrets (from config aifabrix-secrets or remote API)')
45
68
  .action(async(options) => {
46
69
  try {
70
+ await config.ensureSecretsEncryptionKey();
47
71
  await handleSecretsList(options);
48
72
  } catch (error) {
49
73
  handleCommandError(error, 'secret list');
@@ -57,6 +81,7 @@ function setupSecretsCommands(program) {
57
81
  .option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
58
82
  .action(async(key, value, options) => {
59
83
  try {
84
+ await config.ensureSecretsEncryptionKey();
60
85
  await handleSecretsSet(key, value, options);
61
86
  } catch (error) {
62
87
  handleCommandError(error, 'secret set');
@@ -70,6 +95,7 @@ function setupSecretsCommands(program) {
70
95
  .option('--shared', 'Remove from shared secrets (file or remote API)')
71
96
  .action(async(key, options) => {
72
97
  try {
98
+ await config.ensureSecretsEncryptionKey();
73
99
  await handleSecretsRemove(key, options);
74
100
  } catch (error) {
75
101
  handleCommandError(error, 'secret remove');
@@ -77,16 +103,29 @@ function setupSecretsCommands(program) {
77
103
  }
78
104
  });
79
105
 
106
+ setupSecretSetSecretsFileCommand(secretCmd);
80
107
  setupSecretValidateCommand(secretCmd);
108
+ setupSecureCommand(program);
109
+ }
81
110
 
82
- program.command('secure')
83
- .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
84
- .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
85
- .action(async(options) => {
111
+ /**
112
+ * Register secret set-secrets-file command.
113
+ * @param {Command} secretCmd - secret command group
114
+ */
115
+ function setupSecretSetSecretsFileCommand(secretCmd) {
116
+ secretCmd
117
+ .command('set-secrets-file <path>')
118
+ .description('Set aifabrix-secrets path in config (local file or https URL; pass empty to clear; path/URL is not checked for existence)')
119
+ .action(async(secretsPath) => {
86
120
  try {
87
- await handleSecure(options);
121
+ const trimmed = (secretsPath || '').trim();
122
+ if (trimmed !== '' && trimmed.startsWith('http://')) {
123
+ throw new Error('Only https URLs are allowed for remote secrets');
124
+ }
125
+ await config.setSecretsPath(trimmed);
126
+ logger.log(trimmed === '' ? chalk.green('✓ Secrets file path cleared') : chalk.green(`✓ Secrets file path set to ${trimmed}`));
88
127
  } catch (error) {
89
- handleCommandError(error, 'secure');
128
+ handleCommandError(error, 'secret set-secrets-file');
90
129
  process.exit(1);
91
130
  }
92
131
  });
@@ -10,28 +10,37 @@
10
10
  const chalk = require('chalk');
11
11
  const logger = require('../utils/logger');
12
12
  const { handleCommandError } = require('../utils/cli-utils');
13
- const { runServiceUserCreate } = require('../commands/service-user');
13
+ const {
14
+ runServiceUserCreate,
15
+ runServiceUserList,
16
+ runServiceUserRotateSecret,
17
+ runServiceUserDelete,
18
+ runServiceUserUpdateGroups,
19
+ runServiceUserUpdateRedirectUris
20
+ } = require('../commands/service-user');
14
21
 
15
- /**
16
- * Sets up service-user commands
17
- * @param {Command} program - Commander program instance
18
- */
19
- function setupServiceUserCommands(program) {
20
- const serviceUser = program
21
- .command('service-user')
22
- .description('Create and manage service users (API clients) for integrations and CI')
23
- .addHelpText('after', `
22
+ const HELP_AFTER = `
24
23
  Service users are dedicated accounts for integrations, CI pipelines, or API clients.
25
- The controller returns a one-time clientSecret on create—save it immediately; it cannot be retrieved again.
24
+ Use: list (service-user:read), create (service-user:create), rotate-secret, update-groups, update-redirect-uris (service-user:update), delete (service-user:delete).
25
+ The controller returns a one-time clientSecret on create and rotate-secret—save it immediately; it cannot be retrieved again.
26
26
 
27
- Example:
28
- $ aifabrix service-user create -u api-client-001 -e api@example.com \\
29
- --redirect-uris "https://app.example.com/callback" --group-names "AI-Fabrix-Developers"
27
+ Examples:
28
+ $ aifabrix service-user create -u postman -e postman@aifabrix.dev \\
29
+ --redirect-uris https://oauth.pstmn.io/v1/callback --group-names AI-Fabrix-Platform-Admins
30
+ $ aifabrix service-user list
31
+ $ aifabrix service-user rotate-secret --id <uuid>
32
+ $ aifabrix service-user delete --id <uuid>
33
+ $ aifabrix service-user update-groups --id <uuid> --group-names Group1,Group2
34
+ $ aifabrix service-user update-redirect-uris --id <uuid> --redirect-uris https://app.example.com/callback
30
35
 
31
- Required: permission service-user:create on the controller. Run "aifabrix login" first.`);
36
+ Run "aifabrix login" first.`;
37
+
38
+ function parseOptionalInt(val) {
39
+ return (val !== undefined && val !== null) ? parseInt(val, 10) : undefined;
40
+ }
32
41
 
33
- serviceUser
34
- .command('create')
42
+ function addCreateCommand(serviceUser) {
43
+ serviceUser.command('create')
35
44
  .description('Create a service user and receive a one-time clientSecret (save it now; it will not be shown again)')
36
45
  .option('--controller <url>', 'Controller base URL (default: from config)')
37
46
  .option('-u, --username <username>', 'Service user username (required)')
@@ -41,15 +50,14 @@ Required: permission service-user:create on the controller. Run "aifabrix login"
41
50
  .option('-d, --description <description>', 'Description for the service user')
42
51
  .action(async(options) => {
43
52
  try {
44
- const opts = {
53
+ await runServiceUserCreate({
45
54
  controller: options.controller,
46
55
  username: options.username,
47
56
  email: options.email,
48
57
  redirectUris: options.redirectUris,
49
58
  groupNames: options.groupNames,
50
59
  description: options.description
51
- };
52
- await runServiceUserCreate(opts);
60
+ });
53
61
  } catch (error) {
54
62
  logger.error(chalk.red(`Error: ${error.message}`));
55
63
  handleCommandError(error, 'service-user create');
@@ -58,4 +66,122 @@ Required: permission service-user:create on the controller. Run "aifabrix login"
58
66
  });
59
67
  }
60
68
 
69
+ function addListCommand(serviceUser) {
70
+ serviceUser.command('list')
71
+ .description('List service users (supports pagination and search)')
72
+ .option('--controller <url>', 'Controller base URL (default: from config)')
73
+ .option('--page <n>', 'Page number')
74
+ .option('--page-size <n>', 'Items per page')
75
+ .option('--search <term>', 'Search term')
76
+ .option('--sort <field>', 'Sort field/direction')
77
+ .option('--filter <expr>', 'Filter expression')
78
+ .action(async(options) => {
79
+ try {
80
+ await runServiceUserList({
81
+ controller: options.controller,
82
+ page: parseOptionalInt(options.page),
83
+ pageSize: parseOptionalInt(options.pageSize),
84
+ search: options.search,
85
+ sort: options.sort,
86
+ filter: options.filter
87
+ });
88
+ } catch (error) {
89
+ logger.error(chalk.red(`Error: ${error.message}`));
90
+ handleCommandError(error, 'service-user list');
91
+ process.exit(1);
92
+ }
93
+ });
94
+ }
95
+
96
+ function addRotateSecretCommand(serviceUser) {
97
+ serviceUser.command('rotate-secret')
98
+ .description('Rotate (regenerate) secret for a service user; new secret shown once only')
99
+ .option('--controller <url>', 'Controller base URL (default: from config)')
100
+ .option('--id <uuid>', 'Service user ID (required)')
101
+ .action(async(options) => {
102
+ try {
103
+ await runServiceUserRotateSecret({ controller: options.controller, id: options.id });
104
+ } catch (error) {
105
+ logger.error(chalk.red(`Error: ${error.message}`));
106
+ handleCommandError(error, 'service-user rotate-secret');
107
+ process.exit(1);
108
+ }
109
+ });
110
+ }
111
+
112
+ function addDeleteCommand(serviceUser) {
113
+ serviceUser.command('delete')
114
+ .description('Delete (deactivate) a service user')
115
+ .option('--controller <url>', 'Controller base URL (default: from config)')
116
+ .option('--id <uuid>', 'Service user ID (required)')
117
+ .action(async(options) => {
118
+ try {
119
+ await runServiceUserDelete({ controller: options.controller, id: options.id });
120
+ } catch (error) {
121
+ logger.error(chalk.red(`Error: ${error.message}`));
122
+ handleCommandError(error, 'service-user delete');
123
+ process.exit(1);
124
+ }
125
+ });
126
+ }
127
+
128
+ function addUpdateGroupsCommand(serviceUser) {
129
+ serviceUser.command('update-groups')
130
+ .description('Update group assignments for a service user')
131
+ .option('--controller <url>', 'Controller base URL (default: from config)')
132
+ .option('--id <uuid>', 'Service user ID (required)')
133
+ .option('--group-names <names>', 'Comma-separated group names (required)')
134
+ .action(async(options) => {
135
+ try {
136
+ await runServiceUserUpdateGroups({
137
+ controller: options.controller,
138
+ id: options.id,
139
+ groupNames: options.groupNames
140
+ });
141
+ } catch (error) {
142
+ logger.error(chalk.red(`Error: ${error.message}`));
143
+ handleCommandError(error, 'service-user update-groups');
144
+ process.exit(1);
145
+ }
146
+ });
147
+ }
148
+
149
+ function addUpdateRedirectUrisCommand(serviceUser) {
150
+ serviceUser.command('update-redirect-uris')
151
+ .description('Update redirect URIs for a service user (min 1)')
152
+ .option('--controller <url>', 'Controller base URL (default: from config)')
153
+ .option('--id <uuid>', 'Service user ID (required)')
154
+ .option('--redirect-uris <uris>', 'Comma-separated redirect URIs (required, min 1)')
155
+ .action(async(options) => {
156
+ try {
157
+ await runServiceUserUpdateRedirectUris({
158
+ controller: options.controller,
159
+ id: options.id,
160
+ redirectUris: options.redirectUris
161
+ });
162
+ } catch (error) {
163
+ logger.error(chalk.red(`Error: ${error.message}`));
164
+ handleCommandError(error, 'service-user update-redirect-uris');
165
+ process.exit(1);
166
+ }
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Sets up service-user commands
172
+ * @param {Command} program - Commander program instance
173
+ */
174
+ function setupServiceUserCommands(program) {
175
+ const serviceUser = program
176
+ .command('service-user')
177
+ .description('Create and manage service users (API clients) for integrations and CI')
178
+ .addHelpText('after', HELP_AFTER);
179
+ addCreateCommand(serviceUser);
180
+ addListCommand(serviceUser);
181
+ addRotateSecretCommand(serviceUser);
182
+ addDeleteCommand(serviceUser);
183
+ addUpdateGroupsCommand(serviceUser);
184
+ addUpdateRedirectUrisCommand(serviceUser);
185
+ }
186
+
61
187
  module.exports = { setupServiceUserCommands };