@aifabrix/builder 2.41.0 → 2.42.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 (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -11,8 +11,37 @@ const chalk = require('chalk');
11
11
  const logger = require('../utils/logger');
12
12
  const { handleCommandError } = require('../utils/cli-utils');
13
13
  const { runCredentialList } = require('../commands/credential-list');
14
+ const { runCredentialEnv } = require('../commands/credential-env');
15
+ const { runCredentialPush } = require('../commands/credential-push');
14
16
  const { runDeploymentList } = require('../commands/deployment-list');
15
17
 
18
+ function setupCredentialEnvAndPush(credential) {
19
+ credential
20
+ .command('env <system-key>')
21
+ .description('Prompt for KV_* credential values and write integration/<system-key>/.env')
22
+ .action(async(systemKey, _options) => {
23
+ try {
24
+ await runCredentialEnv(systemKey);
25
+ } catch (error) {
26
+ logger.error(chalk.red(`Error: ${error.message}`));
27
+ handleCommandError(error, 'credential env');
28
+ process.exit(1);
29
+ }
30
+ });
31
+ credential
32
+ .command('push <system-key>')
33
+ .description('Push credential secrets from .env to Dataplane (KV_* vars)')
34
+ .action(async(systemKey, _options) => {
35
+ try {
36
+ await runCredentialPush(systemKey);
37
+ } catch (error) {
38
+ logger.error(chalk.red(`Error: ${error.message}`));
39
+ handleCommandError(error, 'credential push');
40
+ process.exit(1);
41
+ }
42
+ });
43
+ }
44
+
16
45
  /**
17
46
  * Sets up credential and deployment list commands
18
47
  * @param {Command} program - Commander program instance
@@ -21,19 +50,15 @@ function setupCredentialDeploymentCommands(program) {
21
50
  const credential = program
22
51
  .command('credential')
23
52
  .description('Manage credentials');
24
-
53
+ setupCredentialEnvAndPush(credential);
25
54
  credential
26
55
  .command('list')
27
- .description('List credentials from Dataplane (GET /api/v1/credential). Controller does not expose this endpoint.')
28
- .option('--controller <url>', 'Controller URL (default: from config)')
29
- .option('--dataplane <url>', 'Dataplane URL (default: resolved from controller + environment)')
56
+ .description('Get credentials from Dataplane')
30
57
  .option('--active-only', 'List only active credentials')
31
58
  .option('--page-size <n>', 'Items per page', '50')
32
59
  .action(async(options) => {
33
60
  try {
34
61
  const opts = {
35
- controller: options.controller,
36
- dataplane: options.dataplane,
37
62
  activeOnly: options.activeOnly,
38
63
  pageSize: parseInt(options.pageSize, 10) || 50
39
64
  };
@@ -33,6 +33,7 @@ async function displayDevConfig(devId) {
33
33
  const controller = await config.getControllerUrl();
34
34
 
35
35
  const optionalConfigVars = [
36
+ { key: 'format', value: (await config.getFormat()) ?? '(not set)' },
36
37
  { key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
37
38
  { key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
38
39
  { key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() },
@@ -59,6 +60,18 @@ async function displayDevConfig(devId) {
59
60
  logger.log('');
60
61
  }
61
62
 
63
+ /**
64
+ * Handle dev set-format command
65
+ * @param {string} format - Format value (json | yaml)
66
+ * @returns {Promise<void>}
67
+ */
68
+ async function handleSetFormat(format) {
69
+ await config.setFormat(format);
70
+ logger.log(chalk.green(`✓ Format set to ${format.toLowerCase()}`));
71
+ const devId = await config.getDeveloperId();
72
+ await displayDevConfig(devId);
73
+ }
74
+
62
75
  /**
63
76
  * Register dev config and set-id commands.
64
77
  * @param {Command} dev - dev subcommand group
@@ -90,6 +103,18 @@ function setupDevConfigCommands(dev) {
90
103
  }
91
104
  });
92
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');
114
+ process.exit(1);
115
+ }
116
+ });
117
+
93
118
  dev
94
119
  .command('set-id <id>')
95
120
  .description('Set developer ID (convenience alias for "dev config --set-id")')
@@ -121,6 +146,8 @@ function setupDevInitCommand(dev) {
121
146
  .requiredOption('--developer-id <id>', 'Developer ID (same as dev add; e.g. 01)')
122
147
  .requiredOption('--server <url>', 'Builder Server base URL (e.g. https://dev.aifabrix.dev)')
123
148
  .requiredOption('--pin <pin>', 'One-time PIN from your admin')
149
+ .option('-y, --yes', 'Auto-install development CA without prompt when certificate is untrusted')
150
+ .option('--no-install-ca', 'Do not offer CA install; fail with manual instructions on untrusted certificate')
124
151
  .action(async(options) => {
125
152
  try {
126
153
  await runDevInit(options);
@@ -25,30 +25,38 @@ function setupEnvironmentCommands(program) {
25
25
 
26
26
  const environment = program
27
27
  .command('environment')
28
- .description('Manage environments');
28
+ .description('Deploy and manage Miso Controller environments (dev, tst, pro, miso)');
29
+
30
+ const deployExamples = `
31
+ Examples:
32
+ $ aifabrix environment deploy dev
33
+ $ aifabrix environment deploy tst --preset m
34
+ $ aifabrix environment deploy dev --config ./env-config.json --no-poll`;
29
35
 
30
36
  environment
31
37
  .command('deploy <env>')
32
- .description('Deploy/setup environment in Miso Controller')
38
+ .description('Deploy environment infrastructure in Miso Controller (run before deploy <app>)')
33
39
  .option('--config <file>', 'Environment configuration file (optional if --preset is used)')
34
40
  .option('--preset <size>', 'Environment size preset: s, m, l, xl (default: s)', 's')
35
41
  .option('--skip-validation', 'Skip environment validation')
36
42
  .option('--poll', 'Poll for deployment status', true)
37
43
  .option('--no-poll', 'Do not poll for status')
44
+ .addHelpText('after', deployExamples)
38
45
  .action(deployEnvHandler);
39
46
 
40
47
  const env = program
41
48
  .command('env')
42
- .description('Environment management (alias for environment)');
49
+ .description('Deploy and manage Miso Controller environments (alias for environment)');
43
50
 
44
51
  env
45
52
  .command('deploy <env>')
46
- .description('Deploy/setup environment in Miso Controller')
53
+ .description('Deploy environment infrastructure in Miso Controller (run before deploy <app>)')
47
54
  .option('--config <file>', 'Environment configuration file (optional if --preset is used)')
48
55
  .option('--preset <size>', 'Environment size preset: s, m, l, xl (default: s)', 's')
49
56
  .option('--skip-validation', 'Skip environment validation')
50
57
  .option('--poll', 'Poll for deployment status', true)
51
58
  .option('--no-poll', 'Do not poll for status')
59
+ .addHelpText('after', deployExamples)
52
60
  .action(deployEnvHandler);
53
61
  }
54
62
 
@@ -1,9 +1,17 @@
1
1
  /**
2
- * CLI external system command setup (download, delete, test, test-integration).
2
+ * CLI external system command setup (download, upload, delete, test-integration).
3
+ *
4
+ * Registers these commands on the Commander program:
5
+ * - download <system-key> – Download external system from dataplane to integration/<system-key>/
6
+ * - upload <system-key> – Upload to dataplane (validate → publish; no controller deploy)
7
+ * - delete <system-key> – Delete external system and associated datasources from dataplane
8
+ * - test-integration <app> – Run integration tests (builder: in container; external: via dataplane pipeline)
3
9
  *
4
10
  * @fileoverview External system command definitions for AI Fabrix Builder CLI
5
11
  * @author AI Fabrix Team
6
12
  * @version 2.0.0
13
+ * @see docs/commands/external-integration.md - User-facing command reference
14
+ * @see docs/external-systems.md - External systems guide and workflow
7
15
  */
8
16
 
9
17
  const { handleCommandError } = require('../utils/cli-utils');
@@ -11,11 +19,18 @@ const { handleCommandError } = require('../utils/cli-utils');
11
19
  function setupDownloadCommand(program) {
12
20
  program.command('download <system-key>')
13
21
  .description('Download external system from dataplane to local development structure')
22
+ .option('--format <format>', 'Output format: json | yaml (default: yaml or config format)')
14
23
  .option('--dry-run', 'Show what would be downloaded without actually downloading')
24
+ .option('--force', 'Overwrite existing README.md without prompting')
15
25
  .action(async(systemKey, options) => {
16
26
  try {
27
+ const config = require('../core/config');
28
+ const effectiveFormat = (options.format || (await config.getFormat()) || 'yaml').trim().toLowerCase();
29
+ if (effectiveFormat !== 'json' && effectiveFormat !== 'yaml') {
30
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
31
+ }
17
32
  const download = require('../external-system/download');
18
- await download.downloadExternalSystem(systemKey, options);
33
+ await download.downloadExternalSystem(systemKey, { ...options, format: effectiveFormat });
19
34
  } catch (error) {
20
35
  handleCommandError(error, 'download');
21
36
  process.exit(1);
@@ -27,7 +42,6 @@ function setupUploadCommand(program) {
27
42
  program.command('upload <system-key>')
28
43
  .description('Upload external system to dataplane (upload → validate → publish; no controller deploy)')
29
44
  .option('--dry-run', 'Validate and build payload only; no API calls')
30
- .option('--dataplane <url>', 'Dataplane URL (default: discovered from controller)')
31
45
  .action(async(systemKey, options) => {
32
46
  try {
33
47
  const upload = require('../commands/upload');
@@ -97,7 +111,7 @@ async function tryBuilderTestIntegration(appName, options) {
97
111
  */
98
112
  async function runExternalSystemTestIntegration(appName, options) {
99
113
  const test = require('../external-system/test');
100
- const opts = { ...options, environment: options.env || options.environment };
114
+ const opts = { ...options, environment: options.env || options.environment, debug: options.debug };
101
115
  const results = await test.testExternalSystemIntegration(appName, opts);
102
116
  test.displayIntegrationTestResults(results, options.verbose);
103
117
  if (!results.success) process.exit(1);
@@ -131,6 +145,7 @@ function setupExternalSystemTestCommands(program) {
131
145
  .option('-p, --payload <file>', 'Path to custom test payload file')
132
146
  .option('-e, --env <env>', 'Environment: dev, tst, or pro (default: from aifabrix auth config)')
133
147
  .option('-v, --verbose', 'Show detailed test output')
148
+ .option('--debug', 'Include debug output and write log to integration/<app>/logs/')
134
149
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
135
150
  .action(async(appName, options) => {
136
151
  try {
@@ -19,8 +19,34 @@ const { handleUpMiso } = require('../commands/up-miso');
19
19
  const { handleUpDataplane } = require('../commands/up-dataplane');
20
20
 
21
21
  /**
22
- * Runs the up-infra command: resolves developer ID, traefik, and starts infra.
23
- * @param {Object} options - Commander options (developer, traefik)
22
+ * Persists optional service flag to config when explicitly set.
23
+ * @param {Object} cfg - Config object (mutated)
24
+ * @param {string} key - Config key (traefik, pgadmin, redisCommander)
25
+ * @param {boolean} value - Value to set
26
+ * @param {string} label - Label for log message
27
+ */
28
+ async function persistOptionalServiceFlag(cfg, key, value, label) {
29
+ cfg[key] = value;
30
+ await config.saveConfig(cfg);
31
+ logger.log(chalk.green(`✓ ${label} ${value ? 'enabled' : 'disabled'} and saved to config`));
32
+ }
33
+
34
+ /**
35
+ * Resolves effective boolean from option vs config.
36
+ * @param {*} optValue - options.traefik | options.pgAdmin | options.redisAdmin
37
+ * @param {*} cfgValue - cfg.traefik | cfg.pgadmin | cfg.redisCommander
38
+ * @param {boolean} defaultWhenUndef - Default when config value is undefined
39
+ * @returns {boolean}
40
+ */
41
+ function resolveFlag(optValue, cfgValue, defaultWhenUndef = true) {
42
+ if (optValue === true) return true;
43
+ if (optValue === false) return false;
44
+ return cfgValue !== false && (cfgValue === true || defaultWhenUndef);
45
+ }
46
+
47
+ /**
48
+ * Runs the up-infra command: resolves developer ID, traefik, pgAdmin, redisAdmin, and starts infra.
49
+ * @param {Object} options - Commander options (developer, traefik, pgAdmin, redisAdmin)
24
50
  * @returns {Promise<void>}
25
51
  */
26
52
  async function runUpInfraCommand(options) {
@@ -37,24 +63,33 @@ async function runUpInfraCommand(options) {
37
63
  logger.log(chalk.green(`✓ Developer ID set to ${id}`));
38
64
  }
39
65
  const cfg = await config.getConfig();
40
- if (options.traefik === true) {
41
- cfg.traefik = true;
42
- await config.saveConfig(cfg);
43
- logger.log(chalk.green('✓ Traefik enabled and saved to config'));
44
- } else if (options.traefik === false) {
45
- cfg.traefik = false;
46
- await config.saveConfig(cfg);
47
- logger.log(chalk.green('✓ Traefik disabled and saved to config'));
66
+ const flagSpecs = [
67
+ { opt: options.traefik, key: 'traefik', label: 'Traefik' },
68
+ { opt: options.pgAdmin, key: 'pgadmin', label: 'pgAdmin' },
69
+ { opt: options.redisAdmin, key: 'redisCommander', label: 'Redis Commander' }
70
+ ];
71
+ for (const { opt, key, label } of flagSpecs) {
72
+ if (opt === true || opt === false) {
73
+ await persistOptionalServiceFlag(cfg, key, opt, label);
74
+ }
48
75
  }
49
- const useTraefik = options.traefik === true ? true : (options.traefik === false ? false : !!(cfg.traefik));
50
- await infra.startInfra(developerId, { traefik: useTraefik, adminPwd: options.adminPwd });
76
+ await infra.startInfra(developerId, {
77
+ traefik: resolveFlag(options.traefik, cfg.traefik, false),
78
+ pgadmin: resolveFlag(options.pgAdmin, cfg.pgadmin, true),
79
+ redisCommander: resolveFlag(options.redisAdmin, cfg.redisCommander, true),
80
+ adminPwd: options.adminPwd
81
+ });
51
82
  }
52
83
 
53
84
  function setupUpInfraCommand(program) {
54
85
  program.command('up-infra')
55
- .description('Start local infrastructure: Postgres, Redis, optional Traefik')
86
+ .description('Start local infrastructure: Postgres, Redis, optional pgAdmin, Redis Commander, Traefik')
56
87
  .option('-d, --developer <id>', 'Set developer ID and start infrastructure')
57
88
  .option('--adminPwd <password>', 'Override default admin password for new install (Postgres, pgAdmin, Redis Commander)')
89
+ .option('--pgAdmin', 'Include pgAdmin web UI and save to config')
90
+ .option('--no-pgAdmin', 'Exclude pgAdmin and save to config')
91
+ .option('--redisAdmin', 'Include Redis Commander web UI and save to config')
92
+ .option('--no-redisAdmin', 'Exclude Redis Commander and save to config')
58
93
  .option('--traefik', 'Include Traefik reverse proxy and save to config')
59
94
  .option('--no-traefik', 'Exclude Traefik and save to config')
60
95
  .action(async(options) => {
@@ -175,7 +210,12 @@ function setupDoctorCommand(program) {
175
210
  }
176
211
  if (result.docker === 'ok') {
177
212
  try {
178
- const health = await infra.checkInfraHealth();
213
+ const cfg = await config.getConfig();
214
+ const health = await infra.checkInfraHealth(null, {
215
+ pgadmin: cfg.pgadmin !== false,
216
+ redisCommander: cfg.redisCommander !== false,
217
+ traefik: !!cfg.traefik
218
+ });
179
219
  logger.log('\n🏥 Infrastructure Health:');
180
220
  Object.entries(health).forEach(([service, status]) => {
181
221
  const icon = status === 'healthy' ? '✅' : status === 'unknown' ? '❓' : '❌';
@@ -77,9 +77,13 @@ function logSplitJsonResult(result) {
77
77
  result.datasourceFiles.forEach(filePath => logger.log(` • datasource: ${filePath}`));
78
78
  }
79
79
  if (result.rbac) {
80
- logger.log(` • rbac.yml: ${result.rbac}`);
80
+ logger.log(` • rbac.yaml: ${result.rbac}`);
81
+ }
82
+ if (result.readmeSkipped) {
83
+ logger.log(` • README.md: (kept existing) ${result.readmeSkipped}`);
84
+ } else if (result.readme) {
85
+ logger.log(` • README.md: ${result.readme}`);
81
86
  }
82
- logger.log(` • README.md: ${result.readme}`);
83
87
  }
84
88
 
85
89
  function setupResolveCommand(program) {
@@ -141,7 +145,7 @@ function setupJsonCommand(program) {
141
145
  });
142
146
  }
143
147
 
144
- function setupSplitJsonConvertShowCommands(program) {
148
+ function setupSplitJsonCommand(program) {
145
149
  program.command('split-json <app>')
146
150
  .description('Split deployment JSON into component files (env.template, application.yaml, rbac.yml, README.md)')
147
151
  .option('-o, --output <dir>', 'Output directory for component files (defaults to same directory as JSON)')
@@ -153,15 +157,66 @@ function setupSplitJsonConvertShowCommands(program) {
153
157
  process.exit(1);
154
158
  }
155
159
  });
160
+ }
156
161
 
162
+ function setupRepairCommand(program) {
163
+ program.command('repair <app>')
164
+ .description('Repair external integration config: fix drift (file lists, app key, datasource alignment, rbac, manifest)')
165
+ .option('--auth <method>', 'Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template')
166
+ .option('--dry-run', 'Report changes only; do not write')
167
+ .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
168
+ .option('--expose', 'Set exposed.attributes on each datasource to all fieldMappings.attributes keys')
169
+ .option('--sync', 'Add default sync section to datasources that lack it')
170
+ .option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
171
+ .action(async(appName, options) => {
172
+ try {
173
+ const { repairExternalIntegration } = require('../commands/repair');
174
+ const { detectAppType } = require('../utils/paths');
175
+ const { appPath } = await detectAppType(appName);
176
+ logOfflinePathWhenType(appPath);
177
+ const result = await repairExternalIntegration(appName, {
178
+ auth: options.auth,
179
+ dryRun: options.dryRun,
180
+ rbac: options.rbac,
181
+ expose: options.expose,
182
+ sync: options.sync,
183
+ test: options.test
184
+ });
185
+ if (options.dryRun && result.updated && result.changes.length > 0) {
186
+ logger.log(chalk.yellow('\nWould apply:'));
187
+ result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
188
+ } else if (result.updated) {
189
+ logger.log(chalk.green('\n✓ Repaired external integration config.'));
190
+ } else {
191
+ logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
192
+ }
193
+ } catch (error) {
194
+ handleCommandError(error, 'repair');
195
+ process.exit(1);
196
+ }
197
+ });
198
+ }
199
+
200
+ function setupConvertCommand(program) {
157
201
  program.command('convert <app>')
158
202
  .description('Convert integration/external system and datasource config files between JSON and YAML')
159
- .option('--format <format>', 'Target format: json | yaml (required)')
203
+ .option('--format <format>', 'Target format: json | yaml (required unless config format is set)')
160
204
  .option('-f, --force', 'Skip confirmation prompt')
161
205
  .action(async(appName, options) => {
162
206
  try {
207
+ const config = require('../core/config');
208
+ const effectiveFormat = options.format || (await config.getFormat());
209
+ if (!effectiveFormat) {
210
+ throw new Error(
211
+ 'Option --format is required and must be \'json\' or \'yaml\' (or set default with aifabrix dev set-format)'
212
+ );
213
+ }
214
+ const normalized = effectiveFormat.trim().toLowerCase();
215
+ if (normalized !== 'json' && normalized !== 'yaml') {
216
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
217
+ }
163
218
  const { runConvert } = require('../commands/convert');
164
- const { converted, deleted } = await runConvert(appName, { format: options.format, force: options.force });
219
+ const { converted, deleted } = await runConvert(appName, { format: normalized, force: options.force });
165
220
  logger.log(chalk.green('\n✓ Convert complete.'));
166
221
  converted.forEach(p => logger.log(` • ${p}`));
167
222
  if (deleted.length > 0) {
@@ -173,7 +228,9 @@ function setupSplitJsonConvertShowCommands(program) {
173
228
  process.exit(1);
174
229
  }
175
230
  });
231
+ }
176
232
 
233
+ function setupShowCommand(program) {
177
234
  program.command('show <appKey>')
178
235
  .description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
179
236
  .option('--online', 'Fetch application data from the controller')
@@ -189,25 +246,60 @@ function setupSplitJsonConvertShowCommands(program) {
189
246
  });
190
247
  }
191
248
 
249
+ function setupSplitJsonConvertShowCommands(program) {
250
+ setupSplitJsonCommand(program);
251
+ setupRepairCommand(program);
252
+ setupConvertCommand(program);
253
+ setupShowCommand(program);
254
+ }
255
+
256
+ async function runValidateCommand(appOrFile, options) {
257
+ const validate = require('../validation/validate');
258
+ const opts = options.opts ? options.opts() : options;
259
+ const integration = opts.integration === true;
260
+ const builder = opts.builder === true;
261
+ const outFormat = (opts.format || 'default').toLowerCase();
262
+
263
+ if (integration || builder) {
264
+ const batchResult = integration && builder
265
+ ? await validate.validateAll(opts)
266
+ : integration
267
+ ? await validate.validateAllIntegrations(opts)
268
+ : await validate.validateAllBuilderApps(opts);
269
+ if (outFormat === 'json') {
270
+ logger.log(JSON.stringify(batchResult, null, 2));
271
+ } else {
272
+ validate.displayBatchValidationResults(batchResult);
273
+ }
274
+ if (!batchResult.valid) process.exit(1);
275
+ return;
276
+ }
277
+
278
+ if (!appOrFile || typeof appOrFile !== 'string') {
279
+ logger.log(chalk.red('App name or file path is required, or use --integration / --builder'));
280
+ process.exit(1);
281
+ }
282
+
283
+ const result = await validate.validateAppOrFile(appOrFile, opts);
284
+ if (outFormat === 'json') {
285
+ logger.log(JSON.stringify(result, null, 2));
286
+ } else {
287
+ validate.displayValidationResults(result);
288
+ }
289
+ if (!result.valid) process.exit(1);
290
+ }
291
+
192
292
  function setupValidateDiffCommands(program) {
193
- program.command('validate <appOrFile>')
194
- .description('Validate application or external integration file')
293
+ program.command('validate [appOrFile]')
294
+ .description('Validate application or external integration file; use --integration or --builder to validate all apps')
195
295
  .option('--format <format>', 'Output format: json | default (human-readable)')
196
- .action(async(appOrFile, options) => {
197
- try {
198
- const validate = require('../validation/validate');
199
- const result = await validate.validateAppOrFile(appOrFile, options);
200
- const outFormat = (options.format || 'default').toLowerCase();
201
- if (outFormat === 'json') {
202
- logger.log(JSON.stringify(result, null, 2));
203
- } else {
204
- validate.displayValidationResults(result);
205
- }
206
- if (!result.valid) process.exit(1);
207
- } catch (error) {
296
+ .option('--integration', 'Validate all applications under integration/')
297
+ .option('--builder', 'Validate all applications under builder/')
298
+ .action((appOrFile, options) => {
299
+ runValidateCommand(appOrFile, options).catch((error) => {
208
300
  handleCommandError(error, 'validate');
209
301
  process.exit(1);
210
- }
302
+ });
211
303
  });
212
304
 
213
305
  program.command('diff <file1> <file2>')
@@ -238,4 +330,8 @@ function setupUtilityCommands(program) {
238
330
  setupValidateDiffCommands(program);
239
331
  }
240
332
 
241
- module.exports = { setupUtilityCommands };
333
+ module.exports = {
334
+ setupUtilityCommands,
335
+ resolveSplitJsonApp,
336
+ handleSplitJsonCommand
337
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Credential env command – prompts for KV_* values and writes .env.
3
+ * Used by `aifabrix credential env <system-key>`.
4
+ *
5
+ * @fileoverview Credential env command – interactive credential capture to .env
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const { getIntegrationPath } = require('../utils/paths');
15
+ const { kvEnvKeyToPath } = require('../utils/credential-secrets-env');
16
+ const { parseEnvToMap } = require('../utils/credential-secrets-env');
17
+
18
+ const KV_PREFIX = 'KV_';
19
+
20
+ /**
21
+ * Secret var suffixes (use password prompt).
22
+ * @type {Set<string>}
23
+ */
24
+ const SECRET_SUFFIXES = new Set([
25
+ 'CLIENTID', 'CLIENTSECRET', 'APIKEY', 'USERNAME', 'PASSWORD', 'PARAMVALUE',
26
+ 'SIGNINGSECRET', 'BEARERTOKEN'
27
+ ]);
28
+
29
+ /**
30
+ * Validates system-key format.
31
+ * @param {string} systemKey - System key
32
+ * @throws {Error} If invalid
33
+ */
34
+ function validateSystemKeyFormat(systemKey) {
35
+ if (!systemKey || typeof systemKey !== 'string') {
36
+ throw new Error('System key is required and must be a string');
37
+ }
38
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
39
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Extracts KV_* variable names from env.template content.
45
+ * @param {string} content - env.template content
46
+ * @returns {Array<{ key: string, isSecret: boolean }>} KV_* vars to prompt
47
+ */
48
+ function extractKvVarsFromTemplate(content) {
49
+ if (!content || typeof content !== 'string') return [];
50
+ const vars = [];
51
+ const seen = new Set();
52
+ const lines = content.split(/\r?\n/);
53
+ for (const line of lines) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed || trimmed.startsWith('#')) continue;
56
+ const eq = trimmed.indexOf('=');
57
+ if (eq <= 0) continue;
58
+ const key = trimmed.substring(0, eq).trim();
59
+ if (!key.toUpperCase().startsWith(KV_PREFIX)) continue;
60
+ if (kvEnvKeyToPath(key) && !seen.has(key)) {
61
+ seen.add(key);
62
+ const suffix = key.slice(KV_PREFIX.length).split('_').pop() || '';
63
+ vars.push({ key, isSecret: SECRET_SUFFIXES.has(suffix.toUpperCase()) });
64
+ }
65
+ }
66
+ return vars;
67
+ }
68
+
69
+ /**
70
+ * Prompts for KV_* values using inquirer.
71
+ * @async
72
+ * @param {Array<{ key: string, isSecret: boolean }>} vars - Variables to prompt
73
+ * @param {Object} existingMap - Existing .env key-value map (for default values)
74
+ * @returns {Promise<Object.<string, string>>} Key-value map from prompts
75
+ */
76
+ async function promptForKvValues(vars, existingMap) {
77
+ if (vars.length === 0) return {};
78
+ const inquirer = require('inquirer');
79
+ const questions = vars.map(({ key, isSecret }) => ({
80
+ type: isSecret ? 'password' : 'input',
81
+ name: key,
82
+ message: key,
83
+ default: existingMap[key] || undefined
84
+ }));
85
+ return await inquirer.prompt(questions);
86
+ }
87
+
88
+ /**
89
+ * Builds .env content: template lines with KV_* values replaced/merged from prompts.
90
+ * Preserves comments, non-KV lines, and structure; updates KV_* with prompted values.
91
+ * @param {string} templateContent - env.template content
92
+ * @param {Object.<string, string>} promptValues - Values from prompts
93
+ * @returns {string} Final .env content
94
+ */
95
+ function buildEnvContent(templateContent, promptValues) {
96
+ if (!templateContent || typeof templateContent !== 'string') return '';
97
+ const lines = templateContent.split(/\r?\n/);
98
+ const result = [];
99
+ for (const line of lines) {
100
+ const trimmed = line.trim();
101
+ if (!trimmed || trimmed.startsWith('#')) {
102
+ result.push(line);
103
+ continue;
104
+ }
105
+ const eq = trimmed.indexOf('=');
106
+ if (eq <= 0) {
107
+ result.push(line);
108
+ continue;
109
+ }
110
+ const key = trimmed.substring(0, eq).trim();
111
+ if (key.toUpperCase().startsWith(KV_PREFIX) && key in promptValues) {
112
+ result.push(`${key}=${promptValues[key]}`);
113
+ } else {
114
+ result.push(line);
115
+ }
116
+ }
117
+ return result.join('\n');
118
+ }
119
+
120
+ /**
121
+ * Runs credential env command: prompt for KV_* values and write .env.
122
+ * @async
123
+ * @param {string} systemKey - External system key (integration/<system-key>/)
124
+ * @returns {Promise<string>} Path to written .env file
125
+ * @throws {Error} If env.template missing or write fails
126
+ */
127
+ function loadExistingEnvMap(envPath) {
128
+ if (!fs.existsSync(envPath)) return {};
129
+ return parseEnvToMap(fs.readFileSync(envPath, 'utf8'));
130
+ }
131
+
132
+ async function runCredentialEnv(systemKey) {
133
+ validateSystemKeyFormat(systemKey);
134
+ const appPath = getIntegrationPath(systemKey);
135
+ const envTemplatePath = path.join(appPath, 'env.template');
136
+ const envPath = path.join(appPath, '.env');
137
+
138
+ if (!fs.existsSync(envTemplatePath)) {
139
+ throw new Error(`env.template not found at ${envTemplatePath}. Create the integration first (e.g. aifabrix wizard or download).`);
140
+ }
141
+
142
+ const templateContent = fs.readFileSync(envTemplatePath, 'utf8');
143
+ const vars = extractKvVarsFromTemplate(templateContent);
144
+
145
+ if (vars.length === 0) {
146
+ logger.log(chalk.yellow('No KV_* variables in env.template. Nothing to prompt.'));
147
+ return envPath;
148
+ }
149
+
150
+ const existingMap = loadExistingEnvMap(envPath);
151
+ logger.log(chalk.blue(`\nEnter credential values for ${systemKey} (integration/${systemKey}/):`));
152
+ const promptValues = await promptForKvValues(vars, existingMap);
153
+ const content = buildEnvContent(templateContent, promptValues);
154
+
155
+ const dir = path.dirname(envPath);
156
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
157
+ fs.writeFileSync(envPath, content, { mode: 0o600 });
158
+ logger.log(chalk.green(`✓ Wrote ${envPath}`));
159
+ return envPath;
160
+ }
161
+
162
+ module.exports = { runCredentialEnv, validateSystemKeyFormat, extractKvVarsFromTemplate, buildEnvContent };