@aifabrix/builder 2.41.0 → 2.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  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 +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  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 +45 -14
  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/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -17,12 +17,20 @@ const { handleCommandError } = require('../utils/cli-utils');
17
17
  * @param {Object} options - Raw CLI options
18
18
  * @returns {Object} Normalized options
19
19
  */
20
+ const VALID_ENTITY_TYPES = ['recordStorage', 'documentStorage', 'vectorStore', 'messageService', 'none'];
21
+
20
22
  function normalizeExternalOptions(options) {
21
23
  const normalized = { ...options };
22
24
  if (options.displayName) normalized.systemDisplayName = options.displayName;
23
25
  if (options.description) normalized.systemDescription = options.description;
24
26
  if (options.systemType) normalized.systemType = options.systemType;
25
27
  if (options.authType) normalized.authType = options.authType;
28
+ if (options.entityType) {
29
+ if (!VALID_ENTITY_TYPES.includes(options.entityType)) {
30
+ throw new Error(`Invalid --entity-type. Must be one of: ${VALID_ENTITY_TYPES.join(', ')}`);
31
+ }
32
+ normalized.entityType = options.entityType;
33
+ }
26
34
  if (options.datasources !== undefined) {
27
35
  const parsedCount = parseInt(options.datasources, 10);
28
36
  if (Number.isNaN(parsedCount) || parsedCount < 1 || parsedCount > 10) {
@@ -48,6 +56,7 @@ function validateNonInteractiveExternalOptions(normalizedOptions) {
48
56
  if (!normalizedOptions.systemDescription) missing.push('--description');
49
57
  if (!normalizedOptions.systemType) missing.push('--system-type');
50
58
  if (!normalizedOptions.authType) missing.push('--auth-type');
59
+ if (!normalizedOptions.entityType) missing.push('--entity-type');
51
60
  if (!normalizedOptions.datasourceCount) missing.push('--datasources');
52
61
  if (missing.length > 0) {
53
62
  throw new Error(`Missing required options for non-interactive external create: ${missing.join(', ')}`);
@@ -75,14 +84,14 @@ async function handleCreateCommand(appName, options) {
75
84
  const wizardOptions = { app: appName, ...options };
76
85
  const normalizedOptions = normalizeExternalOptions(options);
77
86
 
78
- const isExternalType = options.type === 'external';
87
+ const isExternalType = options.type === 'external' || !options.type;
79
88
  const isNonInteractive = process.stdin && process.stdin.isTTY === false;
80
89
 
81
90
  if (isExternalType && !options.wizard && isNonInteractive) {
82
91
  validateNonInteractiveExternalOptions(normalizedOptions);
83
92
  }
84
93
 
85
- const shouldUseWizard = options.wizard && (options.type === 'external' || (!options.type && validTypes.includes('external')));
94
+ const shouldUseWizard = options.wizard && (options.type === 'external' || !options.type);
86
95
  if (shouldUseWizard) {
87
96
  const { handleWizard } = require('../commands/wizard');
88
97
  await handleWizard(wizardOptions);
@@ -101,7 +110,7 @@ function setupCreateCommand(program) {
101
110
  .option('-a, --authentication', 'Requires authentication/RBAC')
102
111
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
103
112
  .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
104
- .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
113
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'external')
105
114
  .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
106
115
  .option('-g, --github', 'Generate GitHub Actions workflows')
107
116
  .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
@@ -110,7 +119,8 @@ function setupCreateCommand(program) {
110
119
  .option('--display-name <name>', 'External system display name')
111
120
  .option('--description <desc>', 'External system description')
112
121
  .option('--system-type <type>', 'External system type (openapi, mcp, custom)')
113
- .option('--auth-type <type>', 'External system auth type (oauth2, apikey, basic)')
122
+ .option('--auth-type <type>', 'External system auth type (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none)')
123
+ .option('--entity-type <type>', 'Entity type for datasources (recordStorage, documentStorage, vectorStore, messageService, none)')
114
124
  .option('--datasources <count>', 'Number of datasources to create')
115
125
  .action(async(appName, options) => {
116
126
  try {
@@ -130,6 +140,7 @@ Examples:
130
140
  $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
131
141
  $ aifabrix wizard -a my-integration Same as above (app name set)
132
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
133
144
 
134
145
  Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
135
146
  To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
@@ -140,6 +151,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`;
140
151
  .option('-a, --app <app>', 'Application name (synonym for positional appName)')
141
152
  .option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
142
153
  .option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
154
+ .option('--debug', 'Enable debug output and save debug manifests on validation failure')
143
155
  .addHelpText('after', wizardHelp)
144
156
  .action(async(positionalAppName, options) => {
145
157
  try {
@@ -280,6 +292,29 @@ function setupShellTestStopCommands(program) {
280
292
  });
281
293
  }
282
294
 
295
+ async function runTestE2ECommand(appName, options) {
296
+ const pathsUtil = require('../utils/paths');
297
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
298
+ if (appType && appType.baseDir === 'integration') {
299
+ const { runTestE2EForExternalSystem } = require('../commands/test-e2e-external');
300
+ const { success, results } = await runTestE2EForExternalSystem(appName, {
301
+ env: options.env,
302
+ debug: options.debug,
303
+ verbose: options.verbose,
304
+ async: options.async !== false
305
+ });
306
+ results.forEach(r => {
307
+ const icon = r.success ? chalk.green('✓') : chalk.red('✗');
308
+ const msg = r.error ? `${r.key}: ${r.error}` : r.key;
309
+ logger.log(` ${icon} ${msg}`);
310
+ });
311
+ if (!success) process.exit(1);
312
+ return;
313
+ }
314
+ const { runAppTestE2e } = require('../commands/app-test');
315
+ await runAppTestE2e(appName, { env: options.env });
316
+ }
317
+
283
318
  function setupInstallTestE2eLintCommands(program) {
284
319
  program.command('install <app>')
285
320
  .description('Install dependencies in container (builder apps only)')
@@ -301,18 +336,14 @@ function setupInstallTestE2eLintCommands(program) {
301
336
  });
302
337
 
303
338
  program.command('test-e2e <app>')
304
- .description('Run e2e tests in container (builder apps only)')
305
- .option('--env <env>', 'dev (running container) or tst (ephemeral with .env)', 'dev')
339
+ .description('Run e2e tests (builder: in container; external system: all datasources via dataplane)')
340
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro (builder: dev/tst for container)')
341
+ .option('-v, --verbose', 'Show detailed step output and poll progress')
342
+ .option('--debug', 'Include debug output and write log to integration/<app>/logs/')
343
+ .option('--no-async', 'Use sync mode (no polling); single POST per datasource')
306
344
  .action(async(appName, options) => {
307
345
  try {
308
- const pathsUtil = require('../utils/paths');
309
- const appType = await pathsUtil.detectAppType(appName).catch(() => null);
310
- if (appType && appType.baseDir === 'integration') {
311
- logger.log(chalk.gray('test-e2e is for builder applications only. Use aifabrix shell <app> then make test:e2e or pnpm test:e2e.'));
312
- return;
313
- }
314
- const { runAppTestE2e } = require('../commands/app-test');
315
- await runAppTestE2e(appName, { env: options.env });
346
+ await runTestE2ECommand(appName, options);
316
347
  } catch (error) {
317
348
  handleCommandError(error, 'test-e2e');
318
349
  process.exit(1);
@@ -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
+ };