@aifabrix/builder 2.44.4 → 2.44.5

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/jest.projects.js CHANGED
@@ -57,24 +57,28 @@ const defaultProject = {
57
57
  '/tests/lib/utils/cli-utils.test.js',
58
58
  '/tests/lib/utils/external-system-display.test.js',
59
59
  '/tests/lib/utils/dev-hosts-helper.test.js',
60
- '/tests/lib/utils/declarative-url-matrix-d-reload.test.js',
60
+ '/tests/lib/utils/register-aifabrix-shell-env.test.js',
61
61
  '/tests/lib/utils/datasource-validation-watch.test.js',
62
62
  '\\\\tests\\\\lib\\\\utils\\\\cli-utils.test.js',
63
63
  '\\\\tests\\\\lib\\\\utils\\\\external-system-display.test.js',
64
64
  '\\\\tests\\\\lib\\\\utils\\\\dev-hosts-helper.test.js',
65
- '\\\\tests\\\\lib\\\\utils\\\\declarative-url-matrix-d-reload.test.js',
65
+ '\\\\tests\\\\lib\\\\utils\\\\register-aifabrix-shell-env.test.js',
66
66
  '\\\\tests\\\\lib\\\\utils\\\\datasource-validation-watch.test.js',
67
67
  'lib/utils/dev-hosts-helper.test.js',
68
- 'lib/utils/declarative-url-matrix-d-reload.test.js',
68
+ 'lib/utils/register-aifabrix-shell-env.test.js',
69
69
  'lib/utils/datasource-validation-watch.test.js',
70
70
  'dev-hosts-helper\\.test\\.js',
71
- 'declarative-url-matrix-d-reload\\.test\\.js',
71
+ 'register-aifabrix-shell-env\\.test\\.js',
72
72
  '/tests/lib/datasource/log-viewer.test.js',
73
73
  '\\\\tests\\\\lib\\\\datasource\\\\log-viewer.test.js',
74
74
  'lib/datasource/log-viewer.test.js',
75
75
  '/tests/lib/datasource/log-viewer-structural.test.js',
76
+ '/tests/lib/datasource/log-viewer-run.test.js',
76
77
  '\\\\tests\\\\lib\\\\datasource\\\\log-viewer-structural.test.js',
78
+ '\\\\tests\\\\lib\\\\datasource\\\\log-viewer-run.test.js',
77
79
  'lib/datasource/log-viewer-structural.test.js',
80
+ 'lib/datasource/log-viewer-run.test.js',
81
+ 'log-viewer-run\\.test\\.js',
78
82
  '/tests/lib/commands/parameters-validate.test.js',
79
83
  '\\\\tests\\\\lib\\\\commands\\\\parameters-validate.test.js',
80
84
  'lib/commands/parameters-validate.test.js',
@@ -153,10 +157,6 @@ const defaultProject = {
153
157
  '\\\\tests\\\\lib\\\\utils\\\\paths-app-listing.test.js',
154
158
  'lib/utils/paths-app-listing.test.js',
155
159
  'paths-app-listing\\.test\\.js',
156
- '/tests/lib/utils/url-declarative-truth-table-124.test.js',
157
- '\\\\tests\\\\lib\\\\utils\\\\url-declarative-truth-table-124.test.js',
158
- 'lib/utils/url-declarative-truth-table-124.test.js',
159
- 'url-declarative-truth-table-124\\.test\\.js',
160
160
  '/tests/lib/generator/generator-external-rbac.test.js',
161
161
  '\\\\tests\\\\lib\\\\generator\\\\generator-external-rbac.test.js',
162
162
  'lib/generator/generator-external-rbac.test.js',
@@ -200,6 +200,10 @@ const defaultProject = {
200
200
  '/tests/lib/app/app.test.js',
201
201
  '\\\\tests\\\\lib\\\\app\\\\app.test.js',
202
202
  'lib/app/app.test.js',
203
+ '/tests/lib/templates/application-frontdoor-paths.contract.test.js',
204
+ '\\\\tests\\\\lib\\\\templates\\\\application-frontdoor-paths.contract.test.js',
205
+ 'lib/templates/application-frontdoor-paths.contract.test.js',
206
+ 'application-frontdoor-paths\\.contract\\.test\\.js',
203
207
  '/tests/lib/core/admin-secrets.test.js',
204
208
  '\\\\tests\\\\lib\\\\core\\\\admin-secrets.test.js',
205
209
  'lib/core/admin-secrets.test.js'
@@ -221,9 +225,6 @@ const isolatedProjects = [
221
225
  makeIsolatedProject('cli-utils', ['**/tests/lib/utils/cli-utils.test.js']),
222
226
  makeIsolatedProject('external-system-display', ['**/tests/lib/utils/external-system-display.test.js']),
223
227
  makeIsolatedProject('dev-hosts-helper', ['**/tests/lib/utils/dev-hosts-helper.test.js']),
224
- makeIsolatedProject('declarative-url-matrix-d-reload', [
225
- '**/tests/lib/utils/declarative-url-matrix-d-reload.test.js'
226
- ]),
227
228
  makeIsolatedProject('parameters-validate', ['**/tests/lib/commands/parameters-validate.test.js']),
228
229
  makeIsolatedProject('paths-app-listing', ['**/tests/lib/utils/paths-app-listing.test.js']),
229
230
  makeIsolatedProject('datasource-validation-watch', [
@@ -231,7 +232,11 @@ const isolatedProjects = [
231
232
  ]),
232
233
  makeIsolatedProject('log-viewer', [
233
234
  '**/tests/lib/datasource/log-viewer.test.js',
234
- '**/tests/lib/datasource/log-viewer-structural.test.js'
235
+ '**/tests/lib/datasource/log-viewer-structural.test.js',
236
+ '**/tests/lib/datasource/log-viewer-run.test.js'
237
+ ]),
238
+ makeIsolatedProject('register-aifabrix-shell-env', [
239
+ '**/tests/lib/utils/register-aifabrix-shell-env.test.js'
235
240
  ]),
236
241
  makeIsolatedProject('datasource-test-run-schema-sync', [
237
242
  '**/tests/lib/utils/datasource-test-run-schema-sync.test.js'
@@ -239,6 +244,9 @@ const isolatedProjects = [
239
244
  makeIsolatedProject('infra-platform-contract', [
240
245
  '**/tests/lib/parameters/infra-platform-contract.test.js'
241
246
  ]),
247
+ makeIsolatedProject('application-frontdoor-paths-contract', [
248
+ '**/tests/lib/templates/application-frontdoor-paths.contract.test.js'
249
+ ]),
242
250
  makeIsolatedProject('database-secret-values', [
243
251
  '**/tests/lib/parameters/database-secret-values.test.js'
244
252
  ]),
@@ -283,9 +291,6 @@ const isolatedProjects = [
283
291
  makeIsolatedProject('helpers-ensure-admin-secrets', [
284
292
  '**/tests/lib/infrastructure/helpers-ensure-admin-secrets.test.js'
285
293
  ]),
286
- makeIsolatedProject('url-declarative-truth-table-124', [
287
- '**/tests/lib/utils/url-declarative-truth-table-124.test.js'
288
- ]),
289
294
  makeIsolatedProject('secrets-generator', ['**/tests/lib/utils/secrets-generator.test.js']),
290
295
  makeIsolatedProject('app-uncovered-lines', ['**/tests/lib/app/app-uncovered-lines.test.js']),
291
296
  makeIsolatedProject('ensure-dev-certs-for-remote-docker', [
@@ -260,12 +260,13 @@
260
260
  * @property {string} [source.serverUrl] - MCP server URL (for mcp-server)
261
261
  * @property {string} [source.token] - MCP token (for mcp-server, supports ${ENV_VAR})
262
262
  * @property {string} [source.platform] - Known platform (for known-platform)
263
+ * @property {string} [source.entityName] - Entity from discover-entities (openapi-file / openapi-url); skips interactive Step 4.5 when valid
263
264
  * @property {Object} [credential] - Credential configuration
264
265
  * @property {string} credential.action - Action ('create' | 'select' | 'skip')
265
266
  * @property {string} [credential.credentialIdOrKey] - Credential ID/key (for select)
266
267
  * @property {Object} [credential.config] - Credential config (for create)
267
268
  * @property {Object} [preferences] - Generation preferences
268
- * @property {string} [preferences.intent] - User intent (any descriptive text)
269
+ * @property {string} [preferences.intent] - User intent (any descriptive text, max 1000 chars)
269
270
  * @property {string} [preferences.fieldOnboardingLevel] - Field level ('full' | 'standard' | 'minimal')
270
271
  * @property {boolean} [preferences.enableOpenAPIGeneration] - Enable OpenAPI generation
271
272
  * @property {boolean} [preferences.enableMCP] - Enable MCP
@@ -56,7 +56,7 @@ Examples:
56
56
  Notes:
57
57
  - To run E2E for one datasource, use:
58
58
  aifabrix datasource test-e2e <datasourceKey>
59
- - Optional --sync publishes local files to the dataplane first (external integration only).
59
+ - External integration: local system and datasource files are published to the dataplane before E2E (same as upload). Use --no-sync to exercise only config already deployed. The old --sync flag is a no-op (kept for scripts).
60
60
  `;
61
61
 
62
62
  module.exports = {
@@ -91,7 +91,7 @@ async function runExternalIntegrationE2EAndCertSync(appName, options) {
91
91
  debug: options.debug,
92
92
  verbose: options.verbose,
93
93
  async: options.async !== false,
94
- sync: options.sync === true
94
+ noSync: options.noSync === true
95
95
  });
96
96
  const { displayIntegrationTestResults } = require('../utils/external-system-display');
97
97
  const datasourceResults = results.map(r => ({
@@ -121,10 +121,10 @@ async function runExternalIntegrationE2EAndCertSync(appName, options) {
121
121
  async function runTestE2ECommand(appName, options) {
122
122
  const pathsUtil = require('../utils/paths');
123
123
  const appType = await pathsUtil.detectAppType(appName).catch(() => null);
124
- if (options.sync === true && appType && appType.baseDir === 'builder') {
124
+ if (options.noSync === true && appType && appType.baseDir === 'builder') {
125
125
  throw new Error(
126
- 'Option --sync applies only to external integration E2E (integration/<systemKey>/). ' +
127
- 'Remove --sync for builder app E2E, or use aifabrix upload from the integration folder first.'
126
+ 'Option --no-sync applies only to external integration E2E (integration/<systemKey>/). ' +
127
+ 'Remove --no-sync for builder app E2E.'
128
128
  );
129
129
  }
130
130
  if (appType && appType.baseDir === 'integration') {
@@ -166,7 +166,11 @@ function setupTestE2eCommand(program) {
166
166
  .option('-d, --debug', 'Include debug output and write log to integration/<systemKey>/logs/')
167
167
  .option(
168
168
  '--sync',
169
- 'Publish local system and datasource files to the dataplane before running E2E (same as aifabrix upload <systemKey>; external integration only)'
169
+ '(Deprecated; no-op.) Local integration files are published before E2E by default; use --no-sync to skip.'
170
+ )
171
+ .option(
172
+ '--no-sync',
173
+ 'Skip publishing local integration files; E2E uses the system config already on the dataplane (external integration only)'
170
174
  )
171
175
  .option('--warnings-as-errors', 'Treat aggregate warn as failure (exit 1)')
172
176
  .option('--require-cert', 'Require certification passed on every datasource (exit 2 if not)')
@@ -18,7 +18,7 @@ const { resolveControllerUrl } = require('../utils/controller-url');
18
18
  const { handleLogin } = require('../commands/login');
19
19
  const { handleUpMiso } = require('../commands/up-miso');
20
20
  const { handleUpDataplane } = require('../commands/up-dataplane');
21
- const { cleanBuilderAppDirs } = require('../commands/up-common');
21
+ const { applyUpPlatformForceConfig, cleanBuilderAppDirs } = require('../commands/up-common');
22
22
  const {
23
23
  loadInfraStatusSummary,
24
24
  formatInfraStatusTitleLine,
@@ -184,10 +184,14 @@ function setupUpPlatformCommand(program) {
184
184
  .option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
185
185
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
186
186
  .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
187
- .option('-f, --force', 'Clean builder/keycloak, builder/miso-controller, builder/dataplane and re-fetch from templates')
187
+ .option(
188
+ '-f, --force',
189
+ 'Reset CLI auth (clear all device/client tokens, set environment to dev, set default controller URL from developer-id), clean builder/keycloak, builder/miso-controller, builder/dataplane, then re-fetch from templates'
190
+ )
188
191
  .action(async(options) => {
189
192
  try {
190
193
  if (options.force) {
194
+ await applyUpPlatformForceConfig();
191
195
  await cleanBuilderAppDirs(['keycloak', 'miso-controller', 'dataplane']);
192
196
  }
193
197
  await handleUpMiso(options);
@@ -30,6 +30,24 @@ Examples:
30
30
  $ aifabrix validate --builder
31
31
  `;
32
32
 
33
+ const REPAIR_HELP_AFTER = `
34
+ Examples:
35
+ $ aifabrix repair hubspot-demo
36
+ Align manifest, system/datasource files, RBAC extract, env.template, and deploy JSON under integration/<systemKey>/.
37
+
38
+ $ aifabrix repair hubspot-demo --dry-run
39
+ Show what would change without writing files.
40
+
41
+ $ aifabrix repair hubspot-demo --auth oauth2
42
+ Set authentication method; updates the system file and env.template (oauth2, aad, apikey, basic, …).
43
+
44
+ $ aifabrix repair hubspot-demo --rbac --expose --sync --test
45
+ Optional datasource fixes: RBAC roles/permissions, exposed.schema from attributes, default sync block, testPayload stubs.
46
+
47
+ $ aifabrix repair hubspot-demo --doc
48
+ Regenerate README.md from the current deployment manifest only (other drift fixes unchanged).
49
+ `;
50
+
33
51
  /**
34
52
  * Resolve app path and type for split-json (integration first, then builder).
35
53
  *
@@ -185,6 +203,7 @@ function setupRepairCommand(program) {
185
203
  .option('--expose', 'Set exposed.schema on each datasource from all fieldMappings.attributes keys (metadata.<key>); removes deprecated exposed.attributes if present')
186
204
  .option('--sync', 'Add default sync section to datasources that lack it')
187
205
  .option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
206
+ .addHelpText('after', REPAIR_HELP_AFTER)
188
207
  .action(async(appName, options) => {
189
208
  try {
190
209
  const { repairExternalIntegration } = require('../commands/repair');
@@ -17,6 +17,22 @@ const { resolveRbacPath } = require('../utils/app-config-resolver');
17
17
 
18
18
  const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
19
19
 
20
+ /**
21
+ * Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
22
+ * @param {Object} system - Parsed system config
23
+ * @returns {Object|null} RBAC object or null
24
+ */
25
+ function extractRbacFromSystem(system) {
26
+ if (!system || typeof system !== 'object') return null;
27
+ const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
28
+ const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
29
+ if (!hasRoles && !hasPermissions) return null;
30
+ const rbac = {};
31
+ if (hasRoles) rbac.roles = system.roles;
32
+ if (hasPermissions) rbac.permissions = system.permissions;
33
+ return rbac;
34
+ }
35
+
20
36
  /**
21
37
  * Resolves capabilities from datasource (array or legacy object).
22
38
  * @param {Object} parsed - Parsed datasource
@@ -24,9 +40,15 @@ const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
24
40
  */
25
41
  function getCapabilitiesFromDatasource(parsed) {
26
42
  const cap = parsed?.capabilities;
27
- if (Array.isArray(cap)) return cap.filter(c => typeof c === 'string');
43
+ if (Array.isArray(cap)) {
44
+ const arr = cap.filter(c => typeof c === 'string');
45
+ if (arr.length > 0) return arr;
46
+ return [...DEFAULT_CAPABILITIES];
47
+ }
28
48
  if (cap && typeof cap === 'object') {
29
- return Object.keys(cap).filter(k => cap[k] === true);
49
+ const keys = Object.keys(cap).filter(k => cap[k] === true);
50
+ if (keys.length > 0) return keys;
51
+ return [...DEFAULT_CAPABILITIES];
30
52
  }
31
53
  return [...DEFAULT_CAPABILITIES];
32
54
  }
@@ -159,6 +181,7 @@ function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extrac
159
181
  }
160
182
 
161
183
  module.exports = {
184
+ extractRbacFromSystem,
162
185
  getCapabilitiesFromDatasource,
163
186
  mergeRbacFromDatasources
164
187
  };
@@ -26,7 +26,7 @@ const generator = require('../generator');
26
26
  const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
27
27
  const { generateReadmeFromDeployJson } = require('../generator/split-readme');
28
28
  const { repairDatasourceFile } = require('./repair-datasource');
29
- const { mergeRbacFromDatasources } = require('./repair-rbac');
29
+ const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
30
30
  const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
31
31
  const { normalizeDatasourceKeysAndFilenames } = require('./repair-datasource-keys');
32
32
 
@@ -49,22 +49,6 @@ function inferExternalReadmeFileExt(appPath) {
49
49
  return '.json';
50
50
  }
51
51
 
52
- /**
53
- * Extracts roles and permissions from system object for rbac.yaml
54
- * @param {Object} system - Parsed system config
55
- * @returns {Object|null} RBAC object or null
56
- */
57
- function extractRbacFromSystem(system) {
58
- if (!system || typeof system !== 'object') return null;
59
- const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
60
- const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
61
- if (!hasRoles && !hasPermissions) return null;
62
- const rbac = {};
63
- if (hasRoles) rbac.roles = system.roles;
64
- if (hasPermissions) rbac.permissions = system.permissions;
65
- return rbac;
66
- }
67
-
68
52
  /**
69
53
  * Loads first system file and returns parsed object with key
70
54
  * @param {string} appPath - Application path
@@ -85,13 +85,14 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
85
85
  }
86
86
 
87
87
  /**
88
- * Full upload to dataplane when --sync (same path as `aifabrix upload <systemKey>`).
88
+ * Publish local integration files to the dataplane before E2E (same path as `aifabrix upload <systemKey>`),
89
+ * unless ``options.noSync`` is true.
89
90
  * @param {string} systemKey
90
91
  * @param {Object} options
91
92
  * @returns {Promise<void>}
92
93
  */
93
94
  async function syncLocalIfRequested(systemKey, options) {
94
- if (options.sync !== true) return;
95
+ if (options.noSync === true) return;
95
96
  logger.log(chalk.cyan('Syncing local config to dataplane…'));
96
97
  const { uploadExternalSystem } = require('./upload');
97
98
  await uploadExternalSystem(systemKey, {
@@ -112,7 +113,7 @@ async function syncLocalIfRequested(systemKey, options) {
112
113
  * @param {boolean} [options.debug] - Include debug, write log
113
114
  * @param {boolean} [options.verbose] - Verbose output
114
115
  * @param {boolean} [options.async] - If false, sync mode (default true)
115
- * @param {boolean} [options.sync] - When true, run full upload (`uploadExternalSystem`) before per-datasource E2E
116
+ * @param {boolean} [options.noSync] - When true, skip upload (E2E uses dataplane config already deployed)
116
117
  * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
117
118
  */
118
119
  async function runTestE2EForExternalSystem(externalSystem, options = {}) {
@@ -13,6 +13,8 @@ const path = require('path');
13
13
  const fs = require('fs');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
+ const config = require('../core/config');
17
+ const { getDefaultControllerUrl } = require('../utils/controller-url');
16
18
  const pathsUtil = require('../utils/paths');
17
19
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
20
  const { isYamlPath } = require('../utils/config-format');
@@ -166,6 +168,28 @@ function patchEnvOutputPathForDeployOnly(appName) {
166
168
  }
167
169
  }
168
170
 
171
+ /**
172
+ * For ``up-platform --force`` only: clear all stored auth tokens, set ``environment`` to ``dev``,
173
+ * set ``controller`` to the default URL for the current ``developer-id`` (``http://localhost`` +
174
+ * app port = 3000 + developerId × 100), and persist config. Does not change ``developer-id``.
175
+ * Builder dirs (keycloak, miso-controller, dataplane) are cleaned separately.
176
+ *
177
+ * @returns {Promise<void>}
178
+ */
179
+ async function applyUpPlatformForceConfig() {
180
+ const deviceCleared = await config.clearAllDeviceTokens();
181
+ const clientCleared = await config.clearAllClientTokens();
182
+ await config.setCurrentEnvironment('dev');
183
+ const defaultControllerUrl = await getDefaultControllerUrl();
184
+ await config.setControllerUrl(defaultControllerUrl);
185
+ logger.log(
186
+ chalk.blue(
187
+ `--force: cleared ${deviceCleared} device token(s) and ${clientCleared} client token(s); ` +
188
+ `environment set to dev; default controller set to ${defaultControllerUrl} (run aifabrix login after platform is up)`
189
+ )
190
+ );
191
+ }
192
+
169
193
  /**
170
194
  * Removes builder app directories for the given app names. Only removes paths under the builder root
171
195
  * to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
@@ -231,6 +255,7 @@ async function ensureAppFromTemplate(appName) {
231
255
  }
232
256
 
233
257
  module.exports = {
258
+ applyUpPlatformForceConfig,
234
259
  cleanBuilderAppDirs,
235
260
  ensureAppFromTemplate,
236
261
  patchEnvOutputPathForDeployOnly,
@@ -275,7 +275,8 @@ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
275
275
  * @param {string} [options.systemIdOrKey] - System ID or key (optional)
276
276
  * @param {string} [options.sourceType] - Source type (use 'known-platform' to call platforms config endpoint)
277
277
  * @param {string} [options.platformKey] - Platform key for known-platform (e.g. 'hubspot')
278
- * @param {string} [options.appName] - App name for writing debug.log when debug=true
278
+ * @param {string} [options.appName] - Integration app key; for create-system, used as systemIdOrKey
279
+ * when not set so kv paths match the folder (avoids spec-title keys like "companies")
279
280
  * @returns {Promise<Object>} Generated configuration
280
281
  */
281
282
  async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
@@ -291,13 +292,19 @@ async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
291
292
  });
292
293
  return await getPlatformConfig(dataplaneUrl, authConfig, options.platformKey, platformPayload);
293
294
  }
295
+ // Headless/interactive create-system uses appName as integration folder key; OpenAPI title alone
296
+ // can yield a wrong system key (e.g. title "Companies" → "companies"). Prefer appName when set.
297
+ let systemIdOrKeyForPayload = options.systemIdOrKey;
298
+ if (options.mode === 'create-system' && options.appName) {
299
+ systemIdOrKeyForPayload = systemIdOrKeyForPayload || options.appName;
300
+ }
294
301
  const configPayload = buildConfigPayload({
295
302
  openapiSpec: options.openapiSpec,
296
303
  detectedType: options.detectedType,
297
304
  mode: options.mode,
298
305
  prefs,
299
306
  credentialIdOrKey: options.credentialIdOrKey,
300
- systemIdOrKey: options.systemIdOrKey,
307
+ systemIdOrKey: systemIdOrKeyForPayload,
301
308
  entityName: options.entityName,
302
309
  systemDisplayName: options.systemDisplayName
303
310
  });
@@ -410,6 +417,39 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
410
417
  }
411
418
  }
412
419
 
420
+ /**
421
+ * Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
422
+ * @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
423
+ * @param {string} format - Project format: yaml | json
424
+ */
425
+ function logWizardFileSaveFooter(appName, generatedFiles) {
426
+ logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
427
+ logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
428
+ logger.log(chalk.blue('\nNext steps:'));
429
+ logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
430
+ logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
431
+ logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
432
+ }
433
+
434
+ function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
435
+ const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
436
+ const { loadConfigFile } = require('../utils/config-format');
437
+ const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
438
+ const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
439
+ const rbacChanges = [];
440
+ const rbacUpdated = mergeRbacFromDatasources(
441
+ generatedFiles.appPath,
442
+ systemParsedForRbac,
443
+ datasourceFileNames,
444
+ extractRbacFromSystem,
445
+ { format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
446
+ );
447
+ if (rbacUpdated && rbacChanges.length) {
448
+ rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
449
+ logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
450
+ }
451
+ }
452
+
413
453
  /**
414
454
  * Handle file saving step
415
455
  * @async
@@ -418,17 +458,24 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
418
458
  * @param {Object} systemConfig - System configuration
419
459
  * @param {Object[]} datasourceConfigs - Datasource configurations
420
460
  * @param {string} systemKey - System key
421
- * @param {string} dataplaneUrl - Dataplane URL
422
- * @param {Object} authConfig - Authentication configuration
461
+ * @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
423
462
  * @returns {Promise<Object>} Generated files information
424
463
  */
425
- async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, dataplaneUrl, authConfig) {
464
+ async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
465
+ const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
426
466
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
427
467
  const spinner = ora('Saving files...').start();
428
468
  try {
429
469
  const config = require('../core/config');
430
470
  const format = (await config.getFormat()) || 'yaml';
431
471
  const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
472
+ if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
473
+ try {
474
+ mergeRbacAfterWizardFilesWritten(generatedFiles, format);
475
+ } catch (e) {
476
+ logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
477
+ }
478
+ }
432
479
  if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
433
480
  try {
434
481
  await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
@@ -437,12 +484,7 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
437
484
  }
438
485
  }
439
486
  spinner.stop();
440
- logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
441
- logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
442
- logger.log(chalk.blue('\nNext steps:'));
443
- logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
444
- logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
445
- logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
487
+ logWizardFileSaveFooter(appName, generatedFiles);
446
488
  return generatedFiles;
447
489
  } catch (error) {
448
490
  spinner.stop();
@@ -10,30 +10,87 @@ const { discoverEntities } = require('../api/wizard.api');
10
10
  const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
11
11
  const { promptForEntitySelection } = require('../generator/wizard-prompts');
12
12
 
13
+ /**
14
+ * If wizard.yaml entity name matches discover-entities list, use it; else warn.
15
+ * @param {string} trimmed - Trimmed entity name from prefill
16
+ * @param {Array<{name: string}>} entities - Discovered entities
17
+ * @returns {string|null} Resolved name or null to prompt
18
+ */
19
+ function resolvePrefillEntityName(trimmed, entities) {
20
+ const prefillCheck = validateEntityNameForOpenApi(trimmed, entities);
21
+ if (prefillCheck.valid) {
22
+ logger.log(chalk.gray(
23
+ `Using entity from wizard.yaml (${trimmed}). Skipping entity prompts.`
24
+ ));
25
+ logger.log(chalk.green(`\u2713 Selected entity: ${trimmed}`));
26
+ return trimmed;
27
+ }
28
+ logger.log(chalk.yellow(
29
+ `Warning: wizard.yaml source.entityName '${trimmed}' is not in the discover-entities list; choose manually.`
30
+ ));
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Prompt for entity and validate against list.
36
+ * @param {Array<{name: string}>} entities - Discovered entities
37
+ * @returns {Promise<string>} Valid entity name
38
+ */
39
+ async function promptForValidatedEntity(entities) {
40
+ const entityName = await promptForEntitySelection(entities);
41
+ const validation = validateEntityNameForOpenApi(entityName, entities);
42
+ if (!validation.valid) {
43
+ throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
44
+ }
45
+ logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
46
+ return entityName;
47
+ }
48
+
49
+ /**
50
+ * Discover entities and select one (single-entity shortcut, prefill, or prompt).
51
+ * @param {string} dataplaneUrl - Dataplane URL
52
+ * @param {Object} authConfig - Authentication configuration
53
+ * @param {Object} openapiSpec - OpenAPI specification
54
+ * @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
55
+ * @returns {Promise<string|null>} Selected entity name or null (skip)
56
+ */
57
+ async function discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
58
+ const response = await discoverEntities(dataplaneUrl, authConfig, openapiSpec);
59
+ const entities = response?.data?.entities;
60
+ if (!Array.isArray(entities) || entities.length === 0) return null;
61
+
62
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
63
+
64
+ if (entities.length === 1) {
65
+ const only = entities[0].name;
66
+ logger.log(chalk.green(`\u2713 Only one entity discovered; using: ${only}`));
67
+ return only;
68
+ }
69
+
70
+ const trimmed =
71
+ typeof prefillEntityName === 'string' ? prefillEntityName.trim() : '';
72
+ if (trimmed) {
73
+ const resolved = resolvePrefillEntityName(trimmed, entities);
74
+ if (resolved) return resolved;
75
+ }
76
+
77
+ return promptForValidatedEntity(entities);
78
+ }
79
+
13
80
  /**
14
81
  * Handle entity selection step (OpenAPI multi-entity).
15
- * Calls discover-entities; if entities found, prompts user to select one.
82
+ * Calls discover-entities; prompts unless prefill or a single entity applies.
16
83
  * @async
17
84
  * @param {string} dataplaneUrl - Dataplane URL
18
85
  * @param {Object} authConfig - Authentication configuration
19
86
  * @param {Object} openapiSpec - OpenAPI specification
87
+ * @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
20
88
  * @returns {Promise<string|null>} Selected entity name or null (skip)
21
89
  */
22
- async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) {
90
+ async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
23
91
  if (!openapiSpec || typeof openapiSpec !== 'object') return null;
24
92
  try {
25
- const response = await discoverEntities(dataplaneUrl, authConfig, openapiSpec);
26
- const entities = response?.data?.entities;
27
- if (!Array.isArray(entities) || entities.length === 0) return null;
28
-
29
- logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
30
- const entityName = await promptForEntitySelection(entities);
31
- const validation = validateEntityNameForOpenApi(entityName, entities);
32
- if (!validation.valid) {
33
- throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
34
- }
35
- logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
36
- return entityName;
93
+ return await discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName);
37
94
  } catch (error) {
38
95
  logger.log(chalk.yellow(`Warning: Entity discovery failed, using default: ${error.message}`));
39
96
  return null;
@@ -111,8 +111,11 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
111
111
  systemConfig,
112
112
  datasourceConfigs,
113
113
  systemKey || appName,
114
- dataplaneUrl,
115
- authConfig
114
+ {
115
+ dataplaneUrl,
116
+ authConfig,
117
+ enableRBAC: Boolean(preferences?.enableRBAC)
118
+ }
116
119
  );
117
120
  }
118
121
 
@@ -47,6 +47,12 @@ function buildSourceForSave(source) {
47
47
  out.token = source.token ? '(set)' : undefined;
48
48
  }
49
49
  if (source.type === 'known-platform' && source.platform) out.platform = source.platform;
50
+ if (
51
+ (source.type === 'openapi-file' || source.type === 'openapi-url') &&
52
+ source.entityName
53
+ ) {
54
+ out.entityName = source.entityName;
55
+ }
50
56
  return out;
51
57
  }
52
58
 
@@ -75,7 +81,13 @@ function buildWizardStateForSave(opts) {
75
81
  function formatSourceLine(source) {
76
82
  if (!source) return null;
77
83
  const s = source;
78
- return s.type + (s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
84
+ let line =
85
+ s.type +
86
+ (s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
87
+ if (s.entityName && (s.type === 'openapi-file' || s.type === 'openapi-url')) {
88
+ line += ` [entity: ${s.entityName}]`;
89
+ }
90
+ return line;
79
91
  }
80
92
 
81
93
  /**