@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
@@ -19,34 +19,44 @@ const {
19
19
  validateEnvironment,
20
20
  checkUserLoggedIn
21
21
  } = require('../utils/auth-config-validator');
22
+ const { getControllerUrlFromLoggedInUser } = require('../utils/controller-url');
22
23
  const logger = require('../utils/logger');
23
24
 
24
25
  /**
25
26
  * Handle set-controller command
27
+ * Allows setting the default controller when no credentials are stored, or when already logged in to that controller.
28
+ * If credentials exist for a different controller, throws with a clear message.
29
+ *
26
30
  * @async
27
31
  * @function handleSetController
28
32
  * @param {string} url - Controller URL to set
29
33
  * @returns {Promise<void>}
30
- * @throws {Error} If validation fails
34
+ * @throws {Error} If validation fails or credentials exist for another controller
31
35
  */
32
36
  async function handleSetController(url) {
33
37
  try {
34
- // Validate URL format
35
38
  validateControllerUrl(url);
39
+ const normalizedUrl = url.trim().replace(/\/+$/, '');
36
40
 
37
- // Check if user is logged in to that controller
38
- const isLoggedIn = await checkUserLoggedIn(url);
39
- if (!isLoggedIn) {
40
- throw new Error(
41
- `You are not logged in to controller ${url}.\n` +
42
- 'Please run "aifabrix login" first to authenticate with this controller.'
43
- );
41
+ const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
42
+ if (!loggedInControllerUrl) {
43
+ // No stored credentials: allow setting controller so "aifabrix login" opens the right place
44
+ await setControllerUrl(url);
45
+ logger.log(chalk.green(`✓ Controller URL set to: ${url}`));
46
+ return;
44
47
  }
45
48
 
46
- // Save controller URL
47
- await setControllerUrl(url);
49
+ const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
50
+ if (normalizedLoggedIn === normalizedUrl) {
51
+ await setControllerUrl(url);
52
+ logger.log(chalk.green(`✓ Controller URL set to: ${url}`));
53
+ return;
54
+ }
48
55
 
49
- logger.log(chalk.green(`✓ Controller URL set to: ${url}`));
56
+ throw new Error(
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.`
59
+ );
50
60
  } catch (error) {
51
61
  logger.error(chalk.red(`✗ Failed to set controller URL: ${error.message}`));
52
62
  throw error;
@@ -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 };
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Credential list command – list credentials from Dataplane
3
3
  * GET /api/v1/credential. Used by `aifabrix credential list`.
4
- * The Controller does not expose this endpoint; credentials are listed from the Dataplane.
5
4
  *
6
5
  * @fileoverview Credential list command implementation
7
6
  * @author AI Fabrix Team
@@ -10,6 +9,7 @@
10
9
 
11
10
  const chalk = require('chalk');
12
11
  const logger = require('../utils/logger');
12
+ const { formatCredentialWithStatus } = require('../utils/credential-display');
13
13
  const { resolveControllerUrl } = require('../utils/controller-url');
14
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
15
15
  const { normalizeControllerUrl, resolveEnvironment } = require('../core/config');
@@ -37,12 +37,14 @@ async function getCredentialListAuth(controllerUrl) {
37
37
  }
38
38
 
39
39
  /**
40
- * Extract credentials array from API response
41
- * @param {Object} response - API response
40
+ * Extract credentials array from API response.
41
+ * Handles: { data: { meta, data: [...] } } (paginated), { data: [...] }, { credentials: [...] }, { items: [...] }.
42
+ * @param {Object} response - API response (e.g. { success, data } from API client, or raw body)
42
43
  * @returns {Array}
43
44
  */
44
45
  function extractCredentials(response) {
45
- const data = response?.data ?? response;
46
+ const body = response?.data ?? response;
47
+ const data = body?.data ?? body;
46
48
  const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
47
49
  return Array.isArray(items) ? items : [];
48
50
  }
@@ -59,9 +61,10 @@ function displayCredentialList(list, baseUrl) {
59
61
  return;
60
62
  }
61
63
  list.forEach((c) => {
62
- const key = c.key ?? c.id ?? c.credentialKey ?? '-';
63
- const name = c.displayName ?? c.name ?? key;
64
- logger.log(` ${chalk.cyan(key)} - ${name}`);
64
+ const { key, name, statusFormatted, statusLabel } = formatCredentialWithStatus(c);
65
+ const prefix = statusFormatted ? `${statusFormatted} ` : ' ';
66
+ const line = `${prefix}${chalk.cyan(key)} - ${name}${statusLabel}`;
67
+ logger.log(line);
65
68
  });
66
69
  logger.log('');
67
70
  }
@@ -69,10 +72,10 @@ function displayCredentialList(list, baseUrl) {
69
72
  /**
70
73
  * Ensure controller URL and auth; exit on failure. Returns { controllerUrl, authConfig } when valid.
71
74
  * @async
72
- * @param {Object} options - CLI options with optional controller
75
+ * @param {Object} options - CLI options
73
76
  * @returns {Promise<{controllerUrl: string, authConfig: Object}>}
74
77
  */
75
- async function ensureControllerAndAuth(options) {
78
+ async function ensureControllerAndAuth(options = {}) {
76
79
  const controllerUrl = options.controller || (await resolveControllerUrl());
77
80
  if (!controllerUrl) {
78
81
  logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
@@ -91,19 +94,14 @@ async function ensureControllerAndAuth(options) {
91
94
  }
92
95
 
93
96
  /**
94
- * Resolve Dataplane URL for credential list (override or discover from controller + environment)
97
+ * Resolve Dataplane URL for credential list (discover from controller + environment)
95
98
  * @async
96
99
  * @param {string} controllerUrl - Controller base URL
97
100
  * @param {Object} authConfig - Auth config with token
98
- * @param {Object} options - CLI options
99
- * @param {string} [options.dataplane] - Optional Dataplane URL override
100
101
  * @returns {Promise<string>} Dataplane base URL
101
102
  * @throws {Error} When resolution fails (caller should exit)
102
103
  */
103
- async function resolveCredentialListDataplaneUrl(controllerUrl, authConfig, options) {
104
- if (options.dataplane) {
105
- return options.dataplane.replace(/\/$/, '');
106
- }
104
+ async function resolveCredentialListDataplaneUrl(controllerUrl, authConfig) {
107
105
  const environment = await resolveEnvironment();
108
106
  return await resolveDataplaneUrl(controllerUrl, environment, authConfig);
109
107
  }
@@ -127,12 +125,11 @@ async function fetchAndDisplayCredentials(dataplaneUrl, authConfig, listOptions)
127
125
  * @param {Object} options - CLI options
128
126
  * @returns {Promise<string>} Dataplane URL (never returns on failure; process.exit(1))
129
127
  */
130
- async function resolveDataplaneUrlOrExit(controllerUrl, authConfig, options) {
128
+ async function resolveDataplaneUrlOrExit(controllerUrl, authConfig) {
131
129
  try {
132
- return await resolveCredentialListDataplaneUrl(controllerUrl, authConfig, options);
130
+ return await resolveCredentialListDataplaneUrl(controllerUrl, authConfig);
133
131
  } catch (err) {
134
132
  logger.error(chalk.red(`❌ Could not resolve Dataplane URL: ${err.message}`));
135
- logger.error(chalk.gray('Use --dataplane <url> to specify the Dataplane URL directly.'));
136
133
  process.exit(1);
137
134
  }
138
135
  }
@@ -141,15 +138,13 @@ async function resolveDataplaneUrlOrExit(controllerUrl, authConfig, options) {
141
138
  * Run credential list command: call GET /api/v1/credential on Dataplane and display results
142
139
  * @async
143
140
  * @param {Object} options - CLI options
144
- * @param {string} [options.controller] - Controller URL override
145
- * @param {string} [options.dataplane] - Dataplane URL override (default: resolved from controller + environment)
146
141
  * @param {boolean} [options.activeOnly] - List only active credentials
147
142
  * @param {number} [options.pageSize] - Items per page
148
143
  * @returns {Promise<void>}
149
144
  */
150
145
  async function runCredentialList(options = {}) {
151
146
  const { controllerUrl, authConfig } = await ensureControllerAndAuth(options);
152
- const dataplaneUrl = await resolveDataplaneUrlOrExit(controllerUrl, authConfig, options);
147
+ const dataplaneUrl = await resolveDataplaneUrlOrExit(controllerUrl, authConfig);
153
148
  const listOptions = {
154
149
  pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
155
150
  activeOnly: options.activeOnly
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Credential push command – pushes KV_* from .env to dataplane.
3
+ * Used by `aifabrix credential push <system-key>`.
4
+ *
5
+ * @fileoverview Credential push command – push credential secrets to dataplane
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const path = require('path');
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const { resolveControllerUrl } = require('../utils/controller-url');
14
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
15
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
16
+ const { getIntegrationPath } = require('../utils/paths');
17
+ const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
18
+ const { generateControllerManifest } = require('../generator/external-controller-manifest');
19
+
20
+ /**
21
+ * Validates system-key format.
22
+ * @param {string} systemKey - System key
23
+ * @throws {Error} If invalid
24
+ */
25
+ function validateSystemKeyFormat(systemKey) {
26
+ if (!systemKey || typeof systemKey !== 'string') {
27
+ throw new Error('System key is required and must be a string');
28
+ }
29
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
30
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Builds upload payload for credential push (same shape as upload).
36
+ * @param {string} systemKey - System key
37
+ * @returns {Promise<Object>} { version, application, dataSources }
38
+ */
39
+ async function buildPayload(systemKey) {
40
+ const manifest = await generateControllerManifest(systemKey, { type: 'external' });
41
+ return {
42
+ version: manifest.version || '1.0.0',
43
+ application: manifest.system,
44
+ dataSources: manifest.dataSources || []
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Runs credential push: push KV_* from .env to dataplane.
50
+ * @async
51
+ * @param {string} systemKey - External system key
52
+ * @returns {Promise<{ pushed: number }>} Count of secrets pushed
53
+ * @throws {Error} If auth or push fails
54
+ */
55
+ function logPushResult(pushResult) {
56
+ if (pushResult.pushed > 0) {
57
+ const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
58
+ logger.log(chalk.green(`✓ Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`));
59
+ } else if (pushResult.skipped) {
60
+ logger.log(chalk.yellow('No credential secrets to push (empty .env or no KV_* vars with values).'));
61
+ } else {
62
+ logger.log(chalk.yellow('Secret push skipped'));
63
+ }
64
+ if (pushResult.warning) logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
65
+ }
66
+
67
+ async function runCredentialPush(systemKey) {
68
+ validateSystemKeyFormat(systemKey);
69
+ const appPath = getIntegrationPath(systemKey);
70
+ const envFilePath = path.join(appPath, '.env');
71
+
72
+ const { resolveEnvironment } = require('../core/config');
73
+ const environment = await resolveEnvironment();
74
+ const controllerUrl = await resolveControllerUrl();
75
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
76
+
77
+ if (!authConfig.token && !authConfig.clientId) {
78
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
79
+ }
80
+
81
+ requireBearerForDataplanePipeline(authConfig);
82
+ logger.log(chalk.blue('Resolving dataplane URL...'));
83
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
84
+
85
+ const payload = await buildPayload(systemKey);
86
+ const pushResult = await pushCredentialSecrets(dataplaneUrl, authConfig, {
87
+ envFilePath,
88
+ appName: systemKey,
89
+ payload
90
+ });
91
+
92
+ logPushResult(pushResult);
93
+ return { pushed: pushResult.pushed || 0 };
94
+ }
95
+
96
+ module.exports = { runCredentialPush, validateSystemKeyFormat, buildPayload };
@@ -2,7 +2,7 @@
2
2
  * AI Fabrix Builder - Datasource Commands
3
3
  *
4
4
  * Handles datasource validation, listing, comparison, and deployment
5
- * Commands: datasource validate, datasource list, datasource diff, datasource deploy
5
+ * Commands: datasource validate, datasource list, datasource diff, datasource upload
6
6
  *
7
7
  * @fileoverview Datasource management commands for AI Fabrix Builder
8
8
  * @author AI Fabrix Team
@@ -15,6 +15,9 @@ const { validateDatasourceFile } = require('../datasource/validate');
15
15
  const { listDatasources } = require('../datasource/list');
16
16
  const { compareDatasources } = require('../datasource/diff');
17
17
  const { deployDatasource } = require('../datasource/deploy');
18
+ const { runDatasourceTestIntegration } = require('../datasource/test-integration');
19
+ const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+ const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
18
21
 
19
22
  function setupDatasourceValidateCommand(datasource) {
20
23
  datasource.command('validate <file>')
@@ -62,14 +65,80 @@ function setupDatasourceDiffCommand(datasource) {
62
65
  });
63
66
  }
64
67
 
65
- function setupDatasourceDeployCommand(datasource) {
66
- datasource.command('deploy <myapp> <file>')
67
- .description('Deploy datasource to dataplane')
68
+ function setupDatasourceUploadCommand(datasource) {
69
+ datasource.command('upload <myapp> <file>')
70
+ .description('Upload datasource to dataplane')
68
71
  .action(async(myapp, file, options) => {
69
72
  try {
70
73
  await deployDatasource(myapp, file, options);
71
74
  } catch (error) {
72
- logger.error(chalk.red('❌ Deployment failed:'), error.message);
75
+ logger.error(chalk.red('❌ Upload failed:'), error.message);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ }
80
+
81
+ function setupDatasourceTestIntegrationCommand(datasource) {
82
+ datasource.command('test-integration <datasourceKey>')
83
+ .description('Run integration (config) test for one datasource via dataplane pipeline')
84
+ .option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
85
+ .option('-p, --payload <file>', 'Path to custom test payload file')
86
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro')
87
+ .option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
88
+ .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
89
+ .action(async(datasourceKey, options) => {
90
+ try {
91
+ const result = await runDatasourceTestIntegration(datasourceKey, {
92
+ app: options.app,
93
+ payload: options.payload,
94
+ environment: options.env,
95
+ debug: options.debug,
96
+ timeout: options.timeout
97
+ });
98
+ displayIntegrationTestResults({
99
+ systemKey: result.systemKey || 'unknown',
100
+ datasourceResults: [result],
101
+ success: result.success
102
+ }, options.verbose);
103
+ if (!result.success) process.exit(1);
104
+ } catch (error) {
105
+ logger.error(chalk.red('❌ Integration test failed:'), error.message);
106
+ process.exit(1);
107
+ }
108
+ });
109
+ }
110
+
111
+ function setupDatasourceTestE2ECommand(datasource) {
112
+ datasource.command('test-e2e <datasourceKey>')
113
+ .description('Run E2E test for one datasource (config, credential, sync, data, CIP) via dataplane')
114
+ .option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
115
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro')
116
+ .option('-v, --verbose', 'Show detailed step output and poll progress')
117
+ .option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
118
+ .option('--test-crud', 'Enable CRUD lifecycle test (body testCrud: true)')
119
+ .option('--record-id <id>', 'Record ID for test (body recordId)')
120
+ .option('--no-cleanup', 'Disable cleanup after test (body cleanup: false)')
121
+ .option('--primary-key-value <value|@path>', 'Primary key value or path to JSON file (e.g. @pk.json) for body primaryKeyValue')
122
+ .option('--no-async', 'Use sync mode (no polling); single POST, no asyncRun')
123
+ .action(async(datasourceKey, options) => {
124
+ try {
125
+ const data = await runDatasourceTestE2E(datasourceKey, {
126
+ app: options.app,
127
+ environment: options.env,
128
+ debug: options.debug,
129
+ verbose: options.verbose,
130
+ async: options.async !== false,
131
+ testCrud: options.testCrud,
132
+ recordId: options.recordId,
133
+ cleanup: options.cleanup,
134
+ primaryKeyValue: options.primaryKeyValue
135
+ });
136
+ displayE2EResults(data, options.verbose);
137
+ const steps = data.steps || data.completedActions || [];
138
+ const failed = data.success === false || steps.some(s => s.success === false || s.error);
139
+ if (failed) process.exit(1);
140
+ } catch (error) {
141
+ logger.error(chalk.red('❌ E2E test failed:'), error.message);
73
142
  process.exit(1);
74
143
  }
75
144
  });
@@ -84,7 +153,9 @@ function setupDatasourceCommands(program) {
84
153
  setupDatasourceValidateCommand(datasource);
85
154
  setupDatasourceListCommand(datasource);
86
155
  setupDatasourceDiffCommand(datasource);
87
- setupDatasourceDeployCommand(datasource);
156
+ setupDatasourceUploadCommand(datasource);
157
+ setupDatasourceTestIntegrationCommand(datasource);
158
+ setupDatasourceTestE2ECommand(datasource);
88
159
  }
89
160
 
90
161
  module.exports = { setupDatasourceCommands };
@@ -14,6 +14,44 @@ const { generateCSR, getCertDir, readClientCertPem, readClientKeyPem, getCertVal
14
14
  const { getOrCreatePublicKeyContent } = require('../utils/ssh-key-helper');
15
15
  const devApi = require('../api/dev.api');
16
16
  const logger = require('../utils/logger');
17
+ const {
18
+ isSslUntrustedError,
19
+ fetchInstallCa,
20
+ installCaPlatform,
21
+ promptInstallCa
22
+ } = require('../utils/dev-ca-install');
23
+
24
+ /**
25
+ * Ensure the Builder Server is trusted: run health check; on SSL untrusted error,
26
+ * optionally fetch and install CA, then retry.
27
+ * @param {string} baseUrl - Builder Server base URL
28
+ * @param {Object} options - Commander options (yes, y, no-install-ca)
29
+ * @returns {Promise<void>}
30
+ */
31
+ async function ensureServerTrusted(baseUrl, options) {
32
+ const skipInstall = options['no-install-ca'];
33
+ const autoInstall = options.yes || options.y;
34
+ try {
35
+ await devApi.getHealth(baseUrl);
36
+ } catch (err) {
37
+ if (!isSslUntrustedError(err)) throw err;
38
+ const manualUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
39
+ if (skipInstall) {
40
+ throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
41
+ }
42
+ if (!autoInstall) {
43
+ const install = await promptInstallCa();
44
+ if (!install) {
45
+ throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
46
+ }
47
+ }
48
+ logger.log(chalk.gray(' Downloading and installing CA...'));
49
+ const caPem = await fetchInstallCa(baseUrl);
50
+ await installCaPlatform(caPem, baseUrl);
51
+ logger.log(chalk.gray(' CA installed. Retrying...'));
52
+ await devApi.getHealth(baseUrl);
53
+ }
54
+ }
17
55
 
18
56
  /**
19
57
  * Validate init options and return normalized baseUrl and devId.
@@ -213,7 +251,7 @@ async function runDevInit(options) {
213
251
  logger.log(chalk.blue('\n🔐 Onboarding with Builder Server...\n'));
214
252
 
215
253
  try {
216
- await devApi.getHealth(baseUrl);
254
+ await ensureServerTrusted(baseUrl, options);
217
255
  } catch (err) {
218
256
  throw new Error(`Cannot reach Builder Server at ${baseUrl}. Check URL and network. ${err.message}`);
219
257
  }