@aifabrix/builder 2.42.1 → 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 (117) 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/app/register.js +3 -1
  38. package/lib/app/rotate-secret.js +3 -0
  39. package/lib/cli/setup-app.js +2 -2
  40. package/lib/cli/setup-auth.js +19 -11
  41. package/lib/cli/setup-dev.js +62 -32
  42. package/lib/cli/setup-environment.js +6 -21
  43. package/lib/cli/setup-infra.js +13 -0
  44. package/lib/cli/setup-secrets.js +45 -6
  45. package/lib/cli/setup-service-user.js +146 -20
  46. package/lib/cli/setup-utility.js +12 -0
  47. package/lib/commands/auth-config.js +4 -8
  48. package/lib/commands/datasource.js +46 -1
  49. package/lib/commands/dev-init.js +1 -1
  50. package/lib/commands/repair-env-template.js +14 -8
  51. package/lib/commands/repair-rbac.js +25 -19
  52. package/lib/commands/repair.js +96 -30
  53. package/lib/commands/secrets-remove.js +1 -1
  54. package/lib/commands/secrets-validate.js +17 -4
  55. package/lib/commands/service-user.js +231 -2
  56. package/lib/commands/up-common.js +25 -0
  57. package/lib/commands/up-dataplane.js +2 -2
  58. package/lib/core/admin-secrets.js +2 -0
  59. package/lib/core/config.js +7 -5
  60. package/lib/core/ensure-encryption-key.js +1 -3
  61. package/lib/core/secrets.js +32 -9
  62. package/lib/core/templates.js +1 -1
  63. package/lib/datasource/abac-validator.js +157 -0
  64. package/lib/datasource/field-reference-validator.js +74 -36
  65. package/lib/datasource/log-viewer.js +221 -0
  66. package/lib/datasource/resolve-app.js +109 -0
  67. package/lib/datasource/test-e2e.js +11 -20
  68. package/lib/datasource/test-integration.js +42 -22
  69. package/lib/datasource/validate.js +5 -2
  70. package/lib/external-system/generator.js +12 -8
  71. package/lib/external-system/test-system-level.js +1 -1
  72. package/lib/generator/external-controller-manifest.js +3 -3
  73. package/lib/generator/external.js +7 -7
  74. package/lib/generator/helpers.js +13 -9
  75. package/lib/generator/index.js +4 -4
  76. package/lib/generator/split.js +45 -10
  77. package/lib/generator/wizard.js +9 -6
  78. package/lib/infrastructure/helpers.js +50 -35
  79. package/lib/infrastructure/index.js +39 -23
  80. package/lib/schema/env-config.yaml +19 -2
  81. package/lib/schema/external-datasource.schema.json +11 -1
  82. package/lib/utils/app-config-resolver.js +23 -1
  83. package/lib/utils/config-paths.js +48 -4
  84. package/lib/utils/credential-secrets-env.js +16 -1
  85. package/lib/utils/env-map.js +7 -3
  86. package/lib/utils/error-formatter.js +37 -0
  87. package/lib/utils/external-env-template.js +180 -0
  88. package/lib/utils/external-system-display.js +43 -0
  89. package/lib/utils/external-system-validators.js +2 -2
  90. package/lib/utils/help-builder.js +3 -5
  91. package/lib/utils/local-secrets.js +26 -3
  92. package/lib/utils/paths.js +2 -1
  93. package/lib/utils/secrets-generator.js +2 -2
  94. package/lib/utils/secrets-utils.js +4 -0
  95. package/lib/utils/secure-file-permissions.js +91 -0
  96. package/lib/utils/token-manager.js +36 -3
  97. package/lib/utils/yaml-preserve.js +59 -1
  98. package/lib/validation/env-template-auth.js +50 -2
  99. package/lib/validation/external-manifest-validator.js +8 -0
  100. package/lib/validation/validate.js +8 -0
  101. package/lib/validation/validator.js +10 -13
  102. package/package.json +5 -1
  103. package/templates/applications/dataplane/env.template +5 -1
  104. package/templates/applications/miso-controller/application.yaml +1 -1
  105. package/templates/applications/miso-controller/env.template +13 -2
  106. package/templates/external-system/env.template.hbs +22 -0
  107. package/integration/hubspot/README.md +0 -102
  108. package/integration/hubspot/env.template +0 -4
  109. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  110. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  111. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  112. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  113. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  114. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  115. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  116. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  117. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -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 };
@@ -163,6 +163,7 @@ function setupRepairCommand(program) {
163
163
  program.command('repair <app>')
164
164
  .description('Repair external integration config: fix drift (file lists, app key, datasource alignment, rbac, manifest)')
165
165
  .option('--auth <method>', 'Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template')
166
+ .option('--doc', 'Regenerate README.md from deployment manifest')
166
167
  .option('--dry-run', 'Report changes only; do not write')
167
168
  .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
168
169
  .option('--expose', 'Set exposed.attributes on each datasource to all fieldMappings.attributes keys')
@@ -174,9 +175,18 @@ function setupRepairCommand(program) {
174
175
  const { detectAppType } = require('../utils/paths');
175
176
  const { appPath } = await detectAppType(appName);
176
177
  logOfflinePathWhenType(appPath);
178
+ let format = 'yaml';
179
+ try {
180
+ const config = require('../core/config');
181
+ format = (await config.getFormat()) || format;
182
+ } catch (_) {
183
+ // use default yaml when config unavailable
184
+ }
177
185
  const result = await repairExternalIntegration(appName, {
178
186
  auth: options.auth,
187
+ doc: options.doc,
179
188
  dryRun: options.dryRun,
189
+ format,
180
190
  rbac: options.rbac,
181
191
  expose: options.expose,
182
192
  sync: options.sync,
@@ -187,6 +197,8 @@ function setupRepairCommand(program) {
187
197
  result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
188
198
  } else if (result.updated) {
189
199
  logger.log(chalk.green('\n✓ Repaired external integration config.'));
200
+ } else if (result.readmeRegenerated) {
201
+ logger.log(chalk.green('\n✓ Regenerated README.md from deployment manifest.'));
190
202
  } else {
191
203
  logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
192
204
  }
@@ -55,7 +55,7 @@ async function handleSetController(url) {
55
55
 
56
56
  throw new Error(
57
57
  `You have credentials for another controller (${loggedInControllerUrl}).\n` +
58
- `To use ${url} either run "aifabrix login" with that controller, or run "aifabrix logout" first to clear credentials, then set the new controller with --set-controller.`
58
+ 'To use a different controller either run "aifabrix login" with that controller, or run "aifabrix logout" first to clear credentials, then set the new controller with "aifabrix auth --set-controller <url>".'
59
59
  );
60
60
  } catch (error) {
61
61
  logger.error(chalk.red(`✗ Failed to set controller URL: ${error.message}`));
@@ -80,8 +80,7 @@ async function handleSetEnvironment(environment) {
80
80
  const controllerUrl = await getControllerUrl();
81
81
  if (!controllerUrl) {
82
82
  throw new Error(
83
- 'No controller URL found in config.\n' +
84
- 'Please run "aifabrix login" first to set the controller URL.'
83
+ 'No controller URL found in config. Run "aifabrix login" first, or set the controller with "aifabrix auth --set-controller <url>".'
85
84
  );
86
85
  }
87
86
 
@@ -89,8 +88,7 @@ async function handleSetEnvironment(environment) {
89
88
  const isLoggedIn = await checkUserLoggedIn(controllerUrl);
90
89
  if (!isLoggedIn) {
91
90
  throw new Error(
92
- `You are not logged in to controller ${controllerUrl}.\n` +
93
- 'Please run "aifabrix login" first to authenticate with this controller.'
91
+ `You are not logged in to controller ${controllerUrl}. Run "aifabrix login" first to authenticate.`
94
92
  );
95
93
  }
96
94
 
@@ -117,9 +115,7 @@ async function handleSetEnvironment(environment) {
117
115
  async function handleAuthConfig(options) {
118
116
  if (!options.setController && !options.setEnvironment) {
119
117
  throw new Error(
120
- 'No action specified. Use one of:\n' +
121
- ' --set-controller <url>\n' +
122
- ' --set-environment <env>'
118
+ 'No action specified. Use "aifabrix auth --set-controller <url>" or "aifabrix auth --set-environment <env>".'
123
119
  );
124
120
  }
125
121
  if (options.setController) {