@aifabrix/builder 2.42.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.
package/README.md CHANGED
@@ -82,7 +82,7 @@ Create and deploy an external system (e.g. HubSpot): wizard or manual setup, the
82
82
 
83
83
  **Example: HubSpot**
84
84
 
85
- - Create: `aifabrix create hubspot-test --type external` (or `aifabrix wizard` for guided setup).
85
+ - Create: `aifabrix create hubspot` (external is the default; or `aifabrix wizard` for guided setup). For a web app use `aifabrix create my-app --type webapp`.
86
86
  - Configure auth and datasources under `integration/hubspot-test/`.
87
87
  - Validate: `aifabrix validate hubspot-test`
88
88
  - Deploy: `aifabrix deploy hubspot-test`
@@ -26,9 +26,11 @@ Optional: `rbac.yaml` – Roles and permissions merged into the system when pres
26
26
  ### 1. Create External System
27
27
 
28
28
  ```bash
29
- aifabrix create hubspot --type external
29
+ aifabrix create hubspot
30
30
  ```
31
31
 
32
+ (External is the default type. Use `aifabrix create <name> --type webapp` for a builder app.)
33
+
32
34
  Or use the interactive wizard:
33
35
 
34
36
  ```bash
@@ -219,8 +219,9 @@ async function getPlatformConfig(dataplaneUrl, authConfig, platformKey, config)
219
219
  * @param {string} config.detectedType - Detected API type (required, e.g., 'record-based')
220
220
  * @param {string} config.intent - User intent (required, any descriptive text)
221
221
  * @param {string} config.mode - Wizard mode (required, 'create-system' | 'add-datasource')
222
- * @param {string} [config.systemIdOrKey] - Existing system ID/key (required for add-datasource)
222
+ * @param {string} [config.systemIdOrKey] - Existing system ID/key (required for add-datasource; must be application/system key, not entity/datasource key)
223
223
  * @param {string} [config.credentialIdOrKey] - Credential ID or key
224
+ * @param {string|null} [config.systemDisplayName] - System-level display name for credential (e.g. 'Hubspot Demo'); when OpenAPI title is entity-specific, pass system name here
224
225
  * @param {string} [config.fieldOnboardingLevel] - Field onboarding level ('full' | 'standard' | 'minimal')
225
226
  * @param {boolean} [config.enableOpenAPIGeneration] - Enable OpenAPI operation generation
226
227
  * @param {Object} [config.userPreferences] - User preferences
package/lib/app/index.js CHANGED
@@ -68,7 +68,7 @@ function validateAppNameAndSetup(appName, options) {
68
68
  throw new Error('Application name is required');
69
69
  }
70
70
 
71
- const initialType = options.type || 'webapp';
71
+ const initialType = options.type || 'external';
72
72
  const baseDir = getBaseDirForAppType(initialType);
73
73
  const appPath = getAppPath(appName, initialType);
74
74
 
@@ -179,7 +179,7 @@ async function logApplicationCreation(appName, config, options) {
179
179
  async function createApp(appName, options = {}) {
180
180
  try {
181
181
  const { appPath } = validateAppNameAndSetup(appName, options);
182
- await validateAppCreation(appName, options, appPath, getBaseDirForAppType(options.type || 'webapp'));
182
+ await validateAppCreation(appName, options, appPath, getBaseDirForAppType(options.type || 'external'));
183
183
 
184
184
  const mergedOptions = await handleTemplateSetup(options);
185
185
  const config = await promptForOptions(appName, mergedOptions);
@@ -426,8 +426,8 @@ function mergePromptAnswers(appName, options, answers) {
426
426
  * @returns {Promise<Object>} Complete configuration
427
427
  */
428
428
  async function promptForOptions(appName, options) {
429
- // Get app type from options (default to webapp)
430
- const appType = options.type || 'webapp';
429
+ // Get app type from options (default to external)
430
+ const appType = options.type || 'external';
431
431
 
432
432
  // Build questions based on app type
433
433
  let questions = [];
package/lib/app/readme.js CHANGED
@@ -150,6 +150,7 @@ function generateReadmeMd(appName, config) {
150
150
  const datasources = Array.isArray(config.datasources) && config.datasources.length > 0
151
151
  ? config.datasources
152
152
  : buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount, fileExt);
153
+ const authType = config.authentication?.type || config.authentication?.method || config.authType;
153
154
  return generateExternalReadmeContent({
154
155
  appName,
155
156
  systemKey,
@@ -157,7 +158,8 @@ function generateReadmeMd(appName, config) {
157
158
  displayName: config.systemDisplayName,
158
159
  description: config.systemDescription,
159
160
  fileExt: config.fileExt,
160
- datasources
161
+ datasources,
162
+ authType
161
163
  });
162
164
  }
163
165
  const context = buildReadmeContext(appName, config);
@@ -84,14 +84,14 @@ async function handleCreateCommand(appName, options) {
84
84
  const wizardOptions = { app: appName, ...options };
85
85
  const normalizedOptions = normalizeExternalOptions(options);
86
86
 
87
- const isExternalType = options.type === 'external';
87
+ const isExternalType = options.type === 'external' || !options.type;
88
88
  const isNonInteractive = process.stdin && process.stdin.isTTY === false;
89
89
 
90
90
  if (isExternalType && !options.wizard && isNonInteractive) {
91
91
  validateNonInteractiveExternalOptions(normalizedOptions);
92
92
  }
93
93
 
94
- const shouldUseWizard = options.wizard && (options.type === 'external' || (!options.type && validTypes.includes('external')));
94
+ const shouldUseWizard = options.wizard && (options.type === 'external' || !options.type);
95
95
  if (shouldUseWizard) {
96
96
  const { handleWizard } = require('../commands/wizard');
97
97
  await handleWizard(wizardOptions);
@@ -110,7 +110,7 @@ function setupCreateCommand(program) {
110
110
  .option('-a, --authentication', 'Requires authentication/RBAC')
111
111
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
112
112
  .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
113
- .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
113
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'external')
114
114
  .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
115
115
  .option('-g, --github', 'Generate GitHub Actions workflows')
116
116
  .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
@@ -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;
@@ -351,6 +351,7 @@ function persistChangesAndRegenerate(configPath, variables, appName, appPath, ch
351
351
 
352
352
  /**
353
353
  * Apply --auth: replace system file authentication with canonical block for the given method.
354
+ * Preserves existing authentication.variables (e.g. baseUrl, tokenUrl) from the current system file.
354
355
  * @param {Object} ctx - Context with systemParsed, systemKey, auth, dryRun, changes
355
356
  * @returns {boolean} True if auth was replaced
356
357
  */
@@ -362,8 +363,18 @@ function applyAuthMethod(ctx) {
362
363
  `Invalid --auth "${ctx.auth}". Allowed methods: ${ALLOWED_AUTH.join(', ')}`
363
364
  );
364
365
  }
366
+ const existingAuth = ctx.systemParsed.authentication || ctx.systemParsed.auth || {};
365
367
  const { buildAuthenticationFromMethod } = require('../external-system/generator');
366
- ctx.systemParsed.authentication = buildAuthenticationFromMethod(ctx.systemKey, method);
368
+ const newAuth = buildAuthenticationFromMethod(ctx.systemKey, method);
369
+ const existingVars = existingAuth.variables && typeof existingAuth.variables === 'object' ? existingAuth.variables : {};
370
+ const mergedVariables = { ...newAuth.variables, ...existingVars };
371
+ ctx.systemParsed.authentication = {
372
+ ...newAuth,
373
+ variables: Object.keys(mergedVariables).length ? mergedVariables : newAuth.variables
374
+ };
375
+ if (existingAuth.displayName !== undefined) {
376
+ ctx.systemParsed.authentication.displayName = existingAuth.displayName;
377
+ }
367
378
  ctx.changes.push(`Set authentication method to ${method}`);
368
379
  return true;
369
380
  }
@@ -60,6 +60,12 @@ async function handleSecretsSet(key, value, options) {
60
60
  throw new Error('Secret key is required and must be a string');
61
61
  }
62
62
 
63
+ if (key.startsWith('kv://')) {
64
+ throw new Error(
65
+ 'Secret key must not start with kv://. Use the key path without the prefix (e.g. my-app/clientSecret or hubspot/apiKey).'
66
+ );
67
+ }
68
+
63
69
  if (value === undefined || value === null || value === '') {
64
70
  throw new Error('Secret value is required');
65
71
  }
@@ -11,6 +11,7 @@
11
11
  * @version 2.0.0
12
12
  */
13
13
 
14
+ const readline = require('readline');
14
15
  const chalk = require('chalk');
15
16
  const pathsUtil = require('../utils/paths');
16
17
  const { loadConfigFile } = require('../utils/config-format');
@@ -18,13 +19,87 @@ const logger = require('../utils/logger');
18
19
  const config = require('../core/config');
19
20
  const { checkAuthentication } = require('../utils/app-register-auth');
20
21
  const { resolveControllerUrl } = require('../utils/controller-url');
21
- const { resolveEnvironment } = require('../core/config');
22
+ const { resolveEnvironment, setControllerUrl } = require('../core/config');
22
23
  const { registerApplication } = require('../app/register');
23
24
  const { rotateSecret } = require('../app/rotate-secret');
24
25
  const { checkApplicationExists } = require('../utils/app-existence');
26
+ const { checkHealthEndpoint } = require('../utils/health-check');
27
+ const { validateControllerUrl } = require('../utils/auth-config-validator');
25
28
  const app = require('../app');
26
29
  const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
27
30
 
31
+ const CONTROLLER_HEALTH_PATH = '/health';
32
+
33
+ /**
34
+ * Check if controller is reachable (health endpoint).
35
+ * @param {string} baseUrl - Controller base URL (no trailing slash)
36
+ * @returns {Promise<boolean>} True if healthy
37
+ */
38
+ async function isControllerHealthy(baseUrl) {
39
+ const healthUrl = `${baseUrl.replace(/\/+$/, '')}${CONTROLLER_HEALTH_PATH}`;
40
+ try {
41
+ return await checkHealthEndpoint(healthUrl, false);
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Prompt user for controller URL when current controller is not available.
49
+ * @param {string} currentUrl - Current controller URL that failed health check
50
+ * @returns {Promise<string|null>} New URL or null if user aborted (empty input)
51
+ */
52
+ function promptForControllerUrl(currentUrl) {
53
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
+ return new Promise((resolve) => {
55
+ rl.question(
56
+ chalk.yellow(`Controller at ${currentUrl} is not available. Enter controller URL (or press Enter to abort): `),
57
+ (answer) => {
58
+ rl.close();
59
+ const trimmed = (answer || '').trim();
60
+ resolve(trimmed === '' ? null : trimmed);
61
+ }
62
+ );
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Resolve controller URL and ensure it is healthy; if not, prompt once for new URL.
68
+ * @returns {Promise<string>} Controller URL to use
69
+ * @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
70
+ */
71
+ async function resolveControllerUrlWithHealthCheck() {
72
+ let controllerUrl = await resolveControllerUrl();
73
+ controllerUrl = controllerUrl.replace(/\/+$/, '');
74
+
75
+ let healthy = await isControllerHealthy(controllerUrl);
76
+ if (healthy) {
77
+ return controllerUrl;
78
+ }
79
+
80
+ logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
81
+ const newUrl = await promptForControllerUrl(controllerUrl);
82
+ if (!newUrl) {
83
+ throw new Error('Controller URL is required. Run "aifabrix up-dataplane" again and enter a valid controller URL, or set it with: aifabrix auth config --set-controller <url>');
84
+ }
85
+
86
+ try {
87
+ validateControllerUrl(newUrl);
88
+ } catch (err) {
89
+ throw new Error(`Invalid controller URL: ${err.message}`);
90
+ }
91
+
92
+ await setControllerUrl(newUrl);
93
+ const normalizedNew = newUrl.trim().replace(/\/+$/, '');
94
+ healthy = await isControllerHealthy(normalizedNew);
95
+ if (!healthy) {
96
+ throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
97
+ }
98
+
99
+ logger.log(chalk.green(`✓ Using controller: ${normalizedNew}`));
100
+ return normalizedNew;
101
+ }
102
+
28
103
  /**
29
104
  * Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
30
105
  * @async
@@ -45,6 +120,17 @@ async function registerOrRotateDataplane(options, controllerUrl, environmentKey,
45
120
  }
46
121
  }
47
122
 
123
+ /**
124
+ * Deploy dataplane app (send manifest to controller).
125
+ * @param {Object} options - Commander options (registry, registryMode, image)
126
+ * @returns {Promise<void>}
127
+ */
128
+ async function deployDataplaneToController(options) {
129
+ const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
130
+ const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
131
+ await app.deployApp('dataplane', deployOpts);
132
+ }
133
+
48
134
  /**
49
135
  * Build full image ref from registry and dataplane config (registry/name:tag)
50
136
  * @param {string} registry - Registry URL
@@ -85,7 +171,8 @@ async function handleUpDataplane(options = {}) {
85
171
  }
86
172
  logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
87
173
 
88
- const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
174
+ const controllerUrl = await resolveControllerUrlWithHealthCheck();
175
+ const environmentKey = await resolveEnvironment();
89
176
  const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
90
177
 
91
178
  const cfg = await config.getConfig();
@@ -103,10 +190,7 @@ async function handleUpDataplane(options = {}) {
103
190
 
104
191
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
105
192
 
106
- const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
107
- const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
108
-
109
- await app.deployApp('dataplane', deployOpts);
193
+ await deployDataplaneToController(options);
110
194
  logger.log('');
111
195
  await app.runApp('dataplane', { skipEnvOutputPath: true });
112
196
 
@@ -3,6 +3,7 @@
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
+ /* eslint-disable max-lines -- 502 lines; wizard payload and helpers */
6
7
 
7
8
  const chalk = require('chalk');
8
9
  const ora = require('ora');
@@ -181,11 +182,12 @@ function buildConfigPreferences(configPrefs) {
181
182
  * @param {string} params.mode - Selected mode
182
183
  * @param {Object} params.prefs - Configuration preferences
183
184
  * @param {string} [params.credentialIdOrKey] - Credential ID or key
184
- * @param {string} [params.systemIdOrKey] - System ID or key
185
+ * @param {string} [params.systemIdOrKey] - System ID or key (application/system key, not datasource/entity key)
185
186
  * @param {string} [params.entityName] - Entity name for multi-entity OpenAPI (from discover-entities)
187
+ * @param {string|null} [params.systemDisplayName] - System-level display name for credential (e.g. 'Hubspot Demo')
186
188
  * @returns {Object} Configuration payload
187
189
  */
188
- function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName }) {
190
+ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName, systemDisplayName }) {
189
191
  const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
190
192
  const payload = {
191
193
  openapiSpec,
@@ -199,6 +201,7 @@ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credential
199
201
  if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
200
202
  if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
201
203
  if (entityName) payload.entityName = entityName;
204
+ if (systemDisplayName) payload.systemDisplayName = systemDisplayName;
202
205
  if (prefs.debug) payload.debug = true;
203
206
  return payload;
204
207
  }
@@ -298,7 +298,8 @@ async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
298
298
  prefs,
299
299
  credentialIdOrKey: options.credentialIdOrKey,
300
300
  systemIdOrKey: options.systemIdOrKey,
301
- entityName: options.entityName
301
+ entityName: options.entityName,
302
+ systemDisplayName: options.systemDisplayName
302
303
  });
303
304
  return await generateConfig(dataplaneUrl, authConfig, configPayload);
304
305
  }
@@ -24,6 +24,7 @@ const {
24
24
  } = require('./wizard-core');
25
25
  const { discoverEntities } = require('../api/wizard.api');
26
26
  const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
27
+ const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
27
28
 
28
29
  /**
29
30
  * Validate entityName for headless config (throws if invalid)
@@ -80,6 +81,9 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
80
81
 
81
82
  await validateHeadlessEntityName(source?.entityName, openapiSpec, sourceType, dataplaneUrl, authConfig);
82
83
 
84
+ const systemDisplayName = wizardConfig.systemDisplayName ||
85
+ (mode === 'create-system' ? humanizeAppKey(appName) : undefined);
86
+
83
87
  // Step 5: Generate Configuration
84
88
  const { systemConfig, datasourceConfigs, systemKey } = await handleConfigurationGeneration(dataplaneUrl, authConfig, {
85
89
  mode,
@@ -93,7 +97,8 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
93
97
  datasourceKeys: source?.datasourceKeys,
94
98
  configurationValues: source?.configurationValues,
95
99
  entityName: source?.entityName,
96
- appName
100
+ appName,
101
+ systemDisplayName
97
102
  });
98
103
 
99
104
  // Step 6: Validate Configuration
@@ -48,6 +48,7 @@ const {
48
48
  showWizardConfigSummary,
49
49
  ensureIntegrationDir
50
50
  } = require('./wizard-helpers');
51
+ const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
51
52
 
52
53
  /**
53
54
  * Create wizard session with given mode and optional systemIdOrKey (no prompts)
@@ -150,7 +151,8 @@ async function handleInteractiveConfigGeneration(options) {
150
151
  sourceType: options.sourceType,
151
152
  platformKey: options.sourceType === 'known-platform' ? options.sourceData : undefined,
152
153
  entityName: options.entityName,
153
- appName: options.appName
154
+ appName: options.appName,
155
+ systemDisplayName: options.systemDisplayName
154
156
  });
155
157
 
156
158
  return {
@@ -190,7 +192,7 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, sessionId, sy
190
192
  logger.warn('Preview unavailable, showing full configuration.');
191
193
  }
192
194
 
193
- const reviewResult = await promptForConfigReview({ preview, systemConfig, datasourceConfigs });
195
+ const reviewResult = await promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey: opts.appKey });
194
196
 
195
197
  if (reviewResult.action === 'cancel') {
196
198
  logger.log(chalk.yellow('Wizard cancelled.'));
@@ -251,7 +253,8 @@ async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOp
251
253
  sourceData,
252
254
  entityName: entityName || undefined,
253
255
  appName: appKey,
254
- debug
256
+ debug,
257
+ systemDisplayName: flowOpts.systemDisplayName
255
258
  });
256
259
  const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
257
260
  state.preferences = savedPrefs || {};
@@ -314,7 +317,7 @@ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sess
314
317
  * @returns {Promise<void>} Resolves when wizard flow completes
315
318
  */
316
319
  async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
317
- const { mode, systemIdOrKey, configPath, debug } = flowOpts;
320
+ const { mode, systemIdOrKey, configPath, debug, systemDisplayName } = flowOpts;
318
321
 
319
322
  if (debug) {
320
323
  logger.log(chalk.gray(`[DEBUG] Wizard debug mode enabled for app: ${appKey}`));
@@ -329,7 +332,8 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
329
332
  systemIdOrKey,
330
333
  platforms,
331
334
  configPath,
332
- debug: flowOpts.debug
335
+ debug: flowOpts.debug,
336
+ systemDisplayName
333
337
  });
334
338
  if (!state) return;
335
339
 
@@ -474,12 +478,15 @@ async function handleWizardInteractive(options) {
474
478
  if (!resolved) return;
475
479
  const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
476
480
  const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
481
+ const systemDisplayName = options.systemDisplayName || options.displayName ||
482
+ (mode === 'create-system' ? humanizeAppKey(appKey) : undefined);
477
483
  try {
478
484
  await executeWizardFlow(appKey, dataplaneUrl, authConfig, {
479
485
  mode,
480
486
  systemIdOrKey,
481
487
  configPath,
482
- debug: options.debug
488
+ debug: options.debug,
489
+ systemDisplayName
483
490
  });
484
491
  logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
485
492
  } catch (error) {
@@ -77,13 +77,15 @@ function generateReadme(systemKey, application, dataSources) {
77
77
  };
78
78
  });
79
79
 
80
+ const authType = application.authentication?.type || application.authentication?.method;
80
81
  return generateExternalReadmeContent({
81
82
  appName: systemKey,
82
83
  systemKey,
83
84
  systemType: application.type,
84
85
  displayName: application.displayName,
85
86
  description: application.description,
86
- datasources
87
+ datasources,
88
+ authType
87
89
  });
88
90
  }
89
91
 
@@ -196,13 +196,15 @@ async function writeSplitExternalSchemaFiles({ outputDir, systemKey, application
196
196
  await writeYamlFile(rbacPath, rbac, { indent: 2, lineWidth: -1 });
197
197
  }
198
198
 
199
+ const authType = application.authentication?.type || application.authentication?.method;
199
200
  const readmeContent = generateExternalReadmeContent({
200
201
  appName: systemKey,
201
202
  systemKey,
202
203
  systemType: application.type,
203
204
  displayName: application.displayName,
204
205
  description: application.description,
205
- datasources: dataSources
206
+ datasources: dataSources,
207
+ authType
206
208
  });
207
209
  const readmePath = path.join(outputDir, 'README.md');
208
210
  await fs.promises.writeFile(readmePath, readmeContent, 'utf8');
@@ -107,6 +107,16 @@ async function promptForEntitySelection(entities = []) {
107
107
  /** @param {*} o - Value to stringify */
108
108
  const _s = (o) => (o === null || o === undefined || o === '' ? '—' : String(o));
109
109
 
110
+ /**
111
+ * Humanize app key for display name (must stay in sync with prepareWizardContext in lib/generator/wizard.js).
112
+ * @param {string} appKey - Application key (e.g. hubspot-demo)
113
+ * @returns {string} Display name (e.g. Hubspot Demo)
114
+ */
115
+ function humanizeAppKey(appKey) {
116
+ if (!appKey || typeof appKey !== 'string') return appKey || '';
117
+ return appKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
118
+ }
119
+
110
120
  /** @param {Object} sys - systemSummary */
111
121
  function _formatSystem(sys) {
112
122
  return [
@@ -149,8 +159,11 @@ function _formatFieldMappings(fm) {
149
159
  /**
150
160
  * Derive a preview summary from systemConfig and datasourceConfigs when the dataplane
151
161
  * preview API does not return summaries. Ensures a compact summary is always shown.
162
+ * When appKey is provided, system key/displayName and datasource keys are overridden
163
+ * to match what prepareWizardContext will write (so the preview matches saved files).
152
164
  * @param {Object} systemConfig - System configuration
153
165
  * @param {Object[]} datasourceConfigs - Array of datasource configurations
166
+ * @param {string} [appKey] - Optional app key; when set, overrides system key/displayName and rewrites datasource keys
154
167
  * @returns {Object} Preview object compatible with formatPreviewSummary
155
168
  */
156
169
  function _buildDatasourceSummary(ds) {
@@ -171,8 +184,9 @@ function _buildDatasourceSummary(ds) {
171
184
  }
172
185
 
173
186
  function _buildSystemSummary(sys) {
174
- const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ?? null;
175
- const authType = sys.authentication?.type ?? sys.authenticationType ?? null;
187
+ const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ??
188
+ sys.authentication?.variables?.baseUrl ?? null;
189
+ const authType = sys.authentication?.type ?? sys.authentication?.method ?? sys.authenticationType ?? null;
176
190
  const endpointCount = sys.openapi?.endpoints?.length ??
177
191
  (sys.openapi?.operations ? Object.keys(sys.openapi.operations || {}).length : null);
178
192
  return {
@@ -191,7 +205,7 @@ function _buildFieldMappingsSummary(ds0) {
191
205
  return mappedFields.length > 0 ? { mappingCount: mappedFields.length, mappedFields, unmappedFields: [] } : null;
192
206
  }
193
207
 
194
- function derivePreviewFromConfig(systemConfig, datasourceConfigs) {
208
+ function derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey) {
195
209
  const sys = systemConfig || {};
196
210
  const dsList = Array.isArray(datasourceConfigs) ? datasourceConfigs : (datasourceConfigs ? [datasourceConfigs] : []);
197
211
 
@@ -199,7 +213,23 @@ function derivePreviewFromConfig(systemConfig, datasourceConfigs) {
199
213
  systemSummary: _buildSystemSummary(sys),
200
214
  fieldMappingsSummary: _buildFieldMappingsSummary(dsList[0] || {})
201
215
  };
202
- const datasourceSummaries = dsList.map(_buildDatasourceSummary);
216
+ let datasourceSummaries = dsList.map(_buildDatasourceSummary);
217
+
218
+ if (appKey && typeof appKey === 'string') {
219
+ result.systemSummary.key = appKey;
220
+ result.systemSummary.displayName = humanizeAppKey(appKey);
221
+ const originalSystemKey = sys.key || appKey;
222
+ const originalPrefix = `${originalSystemKey}-`;
223
+ datasourceSummaries = datasourceSummaries.map((summary, i) => {
224
+ const ds = dsList[i] || {};
225
+ const dsKey = ds.key || '';
226
+ const newKey = dsKey && dsKey.startsWith(originalPrefix)
227
+ ? `${appKey}-${dsKey.substring(originalPrefix.length)}`
228
+ : `${appKey}-${ds.entityType || ds.entityKey || (dsKey && dsKey.split('-').pop()) || 'default'}`;
229
+ return { ...summary, key: newKey };
230
+ });
231
+ }
232
+
203
233
  if (datasourceSummaries.length === 1) {
204
234
  result.datasourceSummary = datasourceSummaries[0];
205
235
  } else if (datasourceSummaries.length > 1) {
@@ -247,11 +277,12 @@ function formatPreviewSummary(preview) {
247
277
  * @param {Object|null} [opts.preview] - Preview data from getPreview (null = fallback to YAML)
248
278
  * @param {Object} opts.systemConfig - System configuration
249
279
  * @param {Object[]} opts.datasourceConfigs - Array of datasource configurations
280
+ * @param {string} [opts.appKey] - App key; when set and using fallback preview, overrides system key/displayName and datasource keys
250
281
  * @returns {Promise<Object>} Object with action ('accept'|'cancel') and optionally edited configs
251
282
  */
252
- async function promptForConfigReview({ preview, systemConfig, datasourceConfigs }) {
283
+ async function promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey }) {
253
284
  const hasSummary = preview && (preview.systemSummary || preview.datasourceSummary || (preview.datasourceSummaries && preview.datasourceSummaries.length > 0));
254
- const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs);
285
+ const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey);
255
286
  const canShowSummary = summaryToShow.systemSummary || summaryToShow.datasourceSummary ||
256
287
  (summaryToShow.datasourceSummaries && summaryToShow.datasourceSummaries.length > 0);
257
288
 
@@ -290,5 +321,6 @@ module.exports = {
290
321
  promptForEntitySelection,
291
322
  promptForConfigReview,
292
323
  derivePreviewFromConfig,
293
- formatPreviewSummary
324
+ formatPreviewSummary,
325
+ humanizeAppKey
294
326
  };
@@ -68,6 +68,8 @@ async function generateReadme(options) {
68
68
  };
69
69
  });
70
70
 
71
+ const auth = systemConfig.authentication || systemConfig.auth || {};
72
+ const authType = auth.type || auth.method || auth.authType;
71
73
  const readmeContent = generateExternalReadmeContent({
72
74
  appName,
73
75
  systemKey,
@@ -75,7 +77,8 @@ async function generateReadme(options) {
75
77
  displayName: systemConfig.displayName,
76
78
  description: systemConfig.description,
77
79
  fileExt: ext,
78
- datasources
80
+ datasources,
81
+ authType
79
82
  });
80
83
 
81
84
  await fs.writeFile(readmePath, readmeContent, 'utf8');
@@ -15,7 +15,7 @@ const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
16
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
17
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
- const { systemKeyToKvPrefix } = require('../utils/credential-secrets-env');
18
+ const { systemKeyToKvPrefix, securityKeyToVar, isValidKvPath } = require('../utils/credential-secrets-env');
19
19
  const { generateReadme } = require('./wizard-readme');
20
20
 
21
21
  /**
@@ -297,72 +297,84 @@ async function generateOrUpdateVariablesYaml(params) {
297
297
  }
298
298
  }
299
299
 
300
- /**
301
- * Adds API key authentication lines with KV_* convention
302
- * @param {Array<string>} lines - Lines array to append to
303
- * @param {string} prefix - KV prefix (e.g. HUBSPOT)
304
- */
305
- function addApiKeyAuthLines(lines, prefix) {
306
- lines.push('# API Key Authentication');
307
- lines.push(`KV_${prefix}_APIKEY=`);
308
- lines.push('');
300
+ /** Path-style kv value: kv://systemKey/key (camelCase key). */
301
+ function toPathStyleKv(systemKey, key) {
302
+ return `kv://${systemKey}/${key}`;
309
303
  }
310
304
 
311
305
  /**
312
- * Adds OAuth2 authentication lines with KV_* convention
306
+ * Adds env.template lines from authentication.security (path-style kv:// values).
313
307
  * @param {Array<string>} lines - Lines array to append to
314
- * @param {Object} auth - Authentication configuration
315
- * @param {string} prefix - KV prefix
308
+ * @param {Object} security - authentication.security object
309
+ * @param {string} systemKey - System key for path namespace
310
+ * @param {string} prefix - KV prefix (e.g. HUBSPOT)
316
311
  */
317
- function addOAuth2AuthLines(lines, auth, prefix) {
318
- lines.push('# OAuth2 Authentication');
319
- lines.push(`KV_${prefix}_CLIENTID=`);
320
- lines.push(`KV_${prefix}_CLIENTSECRET=`);
321
- if (auth.scope) lines.push(`SCOPE=${auth.scope}`);
322
- lines.push('');
312
+ function addLinesFromSecurity(lines, security, systemKey, prefix) {
313
+ if (!security || typeof security !== 'object') return;
314
+ for (const key of Object.keys(security)) {
315
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
316
+ let value = security[key];
317
+ if (typeof value === 'string' && value.trim().startsWith('kv://') && isValidKvPath(value.trim())) {
318
+ value = value.trim();
319
+ } else {
320
+ value = toPathStyleKv(systemKey, key);
321
+ }
322
+ lines.push(`${envName}=${value}`);
323
+ }
324
+ if (Object.keys(security).length > 0) lines.push('');
323
325
  }
324
326
 
325
- /**
326
- * Adds bearer token authentication lines with KV_* convention
327
- * @param {Array<string>} lines - Lines array to append to
328
- * @param {string} prefix - KV prefix
329
- */
330
- function addBearerTokenAuthLines(lines, prefix) {
331
- lines.push('# Bearer Token Authentication');
332
- lines.push(`KV_${prefix}_BEARERTOKEN=`);
333
- lines.push('');
334
- }
327
+ /** Fallback security keys by auth method (camelCase) for path-style kv://. */
328
+ const FALLBACK_SECURITY_BY_AUTH = {
329
+ oauth2: ['clientId', 'clientSecret'],
330
+ oauth: ['clientId', 'clientSecret'],
331
+ aad: ['clientId', 'clientSecret'],
332
+ apikey: ['apiKey'],
333
+ apiKey: ['apiKey'],
334
+ basic: ['username', 'password'],
335
+ queryParam: ['paramValue'],
336
+ oidc: [],
337
+ hmac: ['signingSecret'],
338
+ bearer: ['bearerToken'],
339
+ token: ['bearerToken'],
340
+ none: []
341
+ };
335
342
 
336
343
  /**
337
- * Adds basic authentication lines with KV_* convention
344
+ * Adds auth lines by type with path-style kv:// values (fallback when security is absent).
338
345
  * @param {Array<string>} lines - Lines array to append to
346
+ * @param {string} authType - Authentication type
347
+ * @param {string} systemKey - System key
339
348
  * @param {string} prefix - KV prefix
340
349
  */
341
- function addBasicAuthLines(lines, prefix) {
342
- lines.push('# Basic Authentication');
343
- lines.push(`KV_${prefix}_USERNAME=`);
344
- lines.push(`KV_${prefix}_PASSWORD=`);
350
+ function addFallbackAuthLines(lines, authType, systemKey, prefix) {
351
+ const keys = FALLBACK_SECURITY_BY_AUTH[authType] || FALLBACK_SECURITY_BY_AUTH.apikey;
352
+ if (keys.length === 0) return;
353
+ const labels = { oauth2: 'OAuth2', aad: 'Azure AD', apikey: 'API Key', basic: 'Basic', queryParam: 'Query Param', hmac: 'HMAC', bearer: 'Bearer Token' };
354
+ const label = labels[authType] || authType;
355
+ lines.push(`# ${label} Authentication`);
356
+ for (const key of keys) {
357
+ lines.push(`KV_${prefix}_${securityKeyToVar(key)}=${toPathStyleKv(systemKey, key)}`);
358
+ }
345
359
  lines.push('');
346
360
  }
347
361
 
348
362
  /**
349
- * Adds authentication lines based on auth type. Uses KV_<APPKEY>_<VAR> convention.
363
+ * Adds authentication lines: from authentication.security when present, else by auth type with path-style kv://.
350
364
  * @param {Array<string>} lines - Lines array to append to
351
- * @param {Object} auth - Authentication configuration
352
- * @param {string} authType - Authentication type
353
- * @param {string} systemKey - System key (e.g. hubspot) for KV_ prefix
365
+ * @param {Object} auth - Authentication configuration (may have security, type/method)
366
+ * @param {string} authType - Normalized auth type
367
+ * @param {string} systemKey - System key for KV path namespace
354
368
  */
355
369
  function addAuthenticationLines(lines, auth, authType, systemKey) {
356
370
  const prefix = systemKeyToKvPrefix(systemKey);
357
371
  if (!prefix) return;
358
- if (authType === 'apikey' || authType === 'apiKey') {
359
- addApiKeyAuthLines(lines, prefix);
360
- } else if (authType === 'oauth2' || authType === 'oauth') {
361
- addOAuth2AuthLines(lines, auth || {}, prefix);
362
- } else if (authType === 'bearer' || authType === 'token') {
363
- addBearerTokenAuthLines(lines, prefix);
364
- } else if (authType === 'basic') {
365
- addBasicAuthLines(lines, prefix);
372
+ const security = auth?.security;
373
+ if (security && typeof security === 'object' && Object.keys(security).length > 0) {
374
+ lines.push('# Authentication (from security)');
375
+ addLinesFromSecurity(lines, security, systemKey, prefix);
376
+ } else {
377
+ addFallbackAuthLines(lines, authType, systemKey, prefix);
366
378
  }
367
379
  }
368
380
 
@@ -396,7 +408,7 @@ async function generateEnvTemplate(appPath, systemConfig, finalSystemKey) {
396
408
  const lines = ['# Environment variables for external system integration', '# Use KV_* for credential push (aifabrix credential push)', ''];
397
409
 
398
410
  const auth = systemConfig?.authentication || systemConfig?.auth || {};
399
- const authType = auth.type || auth.authType || 'apikey';
411
+ const authType = (auth.type || auth.method || auth.authType || 'apikey').toLowerCase();
400
412
 
401
413
  addAuthenticationLines(lines, auth, authType, systemKey);
402
414
  addBaseUrlLines(lines, systemConfig);
@@ -20,11 +20,17 @@
20
20
  },
21
21
  "systemIdOrKey": {
22
22
  "type": "string",
23
- "description": "Existing system ID or key (required when mode='add-datasource')",
23
+ "description": "Application/system key (e.g. hubspot-demo), not a datasource or entity key. Required when mode='add-datasource'. Must be the system key from the dataplane, not an entity key (e.g. 'companies').",
24
24
  "pattern": "^[a-z0-9-]+$",
25
25
  "minLength": 1,
26
26
  "maxLength": 50
27
27
  },
28
+ "systemDisplayName": {
29
+ "type": ["string", "null"],
30
+ "title": "Systemdisplayname",
31
+ "description": "System-level display name for the credential (e.g. 'Hubspot Demo'). When the OpenAPI title is entity-specific (e.g. 'Companies'), pass the system name here so authentication.displayName is system-level.",
32
+ "maxLength": 200
33
+ },
28
34
  "source": {
29
35
  "type": "object",
30
36
  "description": "Source configuration for the wizard",
@@ -91,6 +91,33 @@ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
91
91
  );
92
92
  }
93
93
 
94
+ /**
95
+ * Builds secret path entries for README "Secrets" section per auth type.
96
+ * Path is the key for `aifabrix secret set <key> <value>` (no kv:// prefix; key format systemKey/secretKey in camelCase).
97
+ * @param {string} systemKey - System key
98
+ * @param {string} [authType] - Authentication type (oauth2, aad, apikey, basic, queryParam, hmac, bearer, token, none)
99
+ * @returns {Array<{path: string, description: string}>} secretPaths for template (path = key for secret set, no kv://)
100
+ */
101
+ function buildSecretPaths(systemKey, authType) {
102
+ if (!systemKey || typeof systemKey !== 'string') return [];
103
+ const t = (authType && typeof authType === 'string') ? authType.toLowerCase() : 'apikey';
104
+ const map = {
105
+ oauth2: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
106
+ oauth: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
107
+ aad: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
108
+ apikey: [{ path: `${systemKey}/apiKey`, description: 'API Key' }],
109
+ apiKey: [{ path: `${systemKey}/apiKey`, description: 'API Key' }],
110
+ basic: [{ path: `${systemKey}/username`, description: 'Username' }, { path: `${systemKey}/password`, description: 'Password' }],
111
+ queryparam: [{ path: `${systemKey}/paramValue`, description: 'Query parameter value' }],
112
+ hmac: [{ path: `${systemKey}/signingSecret`, description: 'Signing secret' }],
113
+ bearer: [{ path: `${systemKey}/bearerToken`, description: 'Bearer token' }],
114
+ token: [{ path: `${systemKey}/bearerToken`, description: 'Bearer token' }],
115
+ oidc: [],
116
+ none: []
117
+ };
118
+ return map[t] || map.apikey;
119
+ }
120
+
94
121
  /**
95
122
  * Builds the external system README template context
96
123
  * @function buildExternalReadmeContext
@@ -102,6 +129,8 @@ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
102
129
  * @param {string} [params.description] - Description
103
130
  * @param {Array} [params.datasources] - Datasource objects
104
131
  * @param {string} [params.fileExt] - File extension for config files (e.g. '.json', '.yaml'); default '.json'
132
+ * @param {string} [params.authType] - Authentication type for Secrets section (oauth2, aad, apikey, basic, etc.)
133
+ * @param {Object} [params.authentication] - Full authentication object (authType used if authType not set)
105
134
  * @returns {Object} Template context
106
135
  */
107
136
  function buildExternalReadmeContext(params = {}) {
@@ -112,6 +141,8 @@ function buildExternalReadmeContext(params = {}) {
112
141
  const systemType = params.systemType || 'openapi';
113
142
  const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
114
143
  const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
144
+ const authType = params.authType || params.authentication?.type || params.authentication?.method || params.authentication?.authType;
145
+ const secretPaths = buildSecretPaths(systemKey, authType);
115
146
 
116
147
  return {
117
148
  appName,
@@ -122,7 +153,8 @@ function buildExternalReadmeContext(params = {}) {
122
153
  fileExt: fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`,
123
154
  datasourceCount: datasources.length,
124
155
  hasDatasources: datasources.length > 0,
125
- datasources
156
+ datasources,
157
+ secretPaths
126
158
  };
127
159
  }
128
160
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.42.0",
3
+ "version": "2.42.1",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "node tests/scripts/test-wrapper.js",
12
12
  "test:ci": "bash tests/scripts/ci-simulate.sh",
13
- "test:same-as-github": "cross-env CI=true npm run lint && cross-env CI=true node tests/scripts/test-wrapper.js",
13
+ "test:same-as-github": "npm run build:ci",
14
14
  "test:coverage": "cross-env RUN_COVERAGE=1 node tests/scripts/test-wrapper.js",
15
15
  "test:coverage:nyc": "nyc --reporter=text --reporter=lcov --reporter=html jest --config jest.config.coverage.js --runInBand",
16
16
  "test:watch": "jest --watch",
@@ -46,6 +46,18 @@ Edit files in `integration/{{appName}}/`:
46
46
  - **Field mappings**: `{{systemKey}}-datasource-*-datasource{{fileExt}}` (dimensions, attributes, operations)
47
47
  - **Credential and configuration**: `env.template` (security settings and configuration variables)
48
48
 
49
+ {{#if secretPaths}}{{#if secretPaths.length}}
50
+ ### Secrets
51
+
52
+ Secrets are resolved from `.aifabrix` or key vault. Set them with:
53
+
54
+ ```bash
55
+ {{#each secretPaths}}
56
+ aifabrix secret set {{path}} <your value> # {{description}}
57
+ {{/each}}
58
+ ```
59
+ {{/if}}{{/if}}
60
+
49
61
  ### 3. Validate Configuration
50
62
 
51
63
  ```bash
@@ -61,11 +73,12 @@ aifabrix repair {{appName}}
61
73
  ```
62
74
 
63
75
  Options:
64
- --dry-run Report changes only; do not write
65
- --rbac Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist
66
- --expose Set exposed.attributes on each datasource to all fieldMappings.attributes keys
67
- --sync Add default sync section to datasources that lack it
68
- --test Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes
76
+ --auth <method> Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template
77
+ --dry-run Report changes only; do not write
78
+ --rbac Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist
79
+ --expose Set exposed.attributes on each datasource to all fieldMappings.attributes keys
80
+ --sync Add default sync section to datasources that lack it
81
+ --test Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes
69
82
 
70
83
  ### 5. Upload to dataplane
71
84