@aifabrix/builder 2.42.1 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/app/register.js +3 -1
  38. package/lib/app/rotate-secret.js +3 -0
  39. package/lib/cli/setup-app.js +2 -2
  40. package/lib/cli/setup-auth.js +19 -11
  41. package/lib/cli/setup-dev.js +62 -32
  42. package/lib/cli/setup-environment.js +6 -21
  43. package/lib/cli/setup-infra.js +13 -0
  44. package/lib/cli/setup-secrets.js +45 -6
  45. package/lib/cli/setup-service-user.js +146 -20
  46. package/lib/cli/setup-utility.js +12 -0
  47. package/lib/commands/auth-config.js +4 -8
  48. package/lib/commands/datasource.js +46 -1
  49. package/lib/commands/dev-init.js +1 -1
  50. package/lib/commands/repair-env-template.js +14 -8
  51. package/lib/commands/repair-rbac.js +25 -19
  52. package/lib/commands/repair.js +96 -30
  53. package/lib/commands/secrets-remove.js +1 -1
  54. package/lib/commands/secrets-validate.js +17 -4
  55. package/lib/commands/service-user.js +231 -2
  56. package/lib/commands/up-common.js +25 -0
  57. package/lib/commands/up-dataplane.js +2 -2
  58. package/lib/core/admin-secrets.js +2 -0
  59. package/lib/core/config.js +7 -5
  60. package/lib/core/ensure-encryption-key.js +1 -3
  61. package/lib/core/secrets.js +32 -9
  62. package/lib/core/templates.js +1 -1
  63. package/lib/datasource/abac-validator.js +157 -0
  64. package/lib/datasource/field-reference-validator.js +74 -36
  65. package/lib/datasource/log-viewer.js +221 -0
  66. package/lib/datasource/resolve-app.js +109 -0
  67. package/lib/datasource/test-e2e.js +11 -20
  68. package/lib/datasource/test-integration.js +42 -22
  69. package/lib/datasource/validate.js +5 -2
  70. package/lib/external-system/generator.js +12 -8
  71. package/lib/external-system/test-system-level.js +1 -1
  72. package/lib/generator/external-controller-manifest.js +3 -3
  73. package/lib/generator/external.js +7 -7
  74. package/lib/generator/helpers.js +13 -9
  75. package/lib/generator/index.js +4 -4
  76. package/lib/generator/split.js +45 -10
  77. package/lib/generator/wizard.js +9 -6
  78. package/lib/infrastructure/helpers.js +50 -35
  79. package/lib/infrastructure/index.js +39 -23
  80. package/lib/schema/env-config.yaml +19 -2
  81. package/lib/schema/external-datasource.schema.json +11 -1
  82. package/lib/utils/app-config-resolver.js +23 -1
  83. package/lib/utils/config-paths.js +48 -4
  84. package/lib/utils/credential-secrets-env.js +16 -1
  85. package/lib/utils/env-map.js +7 -3
  86. package/lib/utils/error-formatter.js +37 -0
  87. package/lib/utils/external-env-template.js +180 -0
  88. package/lib/utils/external-system-display.js +43 -0
  89. package/lib/utils/external-system-validators.js +2 -2
  90. package/lib/utils/help-builder.js +3 -5
  91. package/lib/utils/local-secrets.js +26 -3
  92. package/lib/utils/paths.js +2 -1
  93. package/lib/utils/secrets-generator.js +2 -2
  94. package/lib/utils/secrets-utils.js +4 -0
  95. package/lib/utils/secure-file-permissions.js +91 -0
  96. package/lib/utils/token-manager.js +36 -3
  97. package/lib/utils/yaml-preserve.js +59 -1
  98. package/lib/validation/env-template-auth.js +50 -2
  99. package/lib/validation/external-manifest-validator.js +8 -0
  100. package/lib/validation/validate.js +8 -0
  101. package/lib/validation/validator.js +10 -13
  102. package/package.json +5 -1
  103. package/templates/applications/dataplane/env.template +5 -1
  104. package/templates/applications/miso-controller/application.yaml +1 -1
  105. package/templates/applications/miso-controller/env.template +13 -2
  106. package/templates/external-system/env.template.hbs +22 -0
  107. package/integration/hubspot/README.md +0 -102
  108. package/integration/hubspot/env.template +0 -4
  109. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  110. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  111. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  112. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  113. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  114. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  115. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  116. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  117. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -17,6 +17,7 @@ const { compareDatasources } = require('../datasource/diff');
17
17
  const { deployDatasource } = require('../datasource/deploy');
18
18
  const { runDatasourceTestIntegration } = require('../datasource/test-integration');
19
19
  const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+ const { runLogViewer } = require('../datasource/log-viewer');
20
21
  const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
21
22
 
22
23
  function setupDatasourceValidateCommand(datasource) {
@@ -81,9 +82,10 @@ function setupDatasourceUploadCommand(datasource) {
81
82
  function setupDatasourceTestIntegrationCommand(datasource) {
82
83
  datasource.command('test-integration <datasourceKey>')
83
84
  .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('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
85
86
  .option('-p, --payload <file>', 'Path to custom test payload file')
86
87
  .option('-e, --env <env>', 'Environment: dev, tst, or pro')
88
+ .option('-v, --verbose', 'Show detailed step and validation output')
87
89
  .option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
88
90
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
89
91
  .action(async(datasourceKey, options) => {
@@ -144,18 +146,61 @@ function setupDatasourceTestE2ECommand(datasource) {
144
146
  });
145
147
  }
146
148
 
149
+ function setupDatasourceLogE2ECommand(datasource) {
150
+ datasource.command('log-e2e <datasourceKey>')
151
+ .description('Display latest or specified E2E test log in readable format')
152
+ .option('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
153
+ .option('-f, --file <path>', 'Path to log file (default: latest in app logs folder)')
154
+ .action(async(datasourceKey, options) => {
155
+ try {
156
+ await runLogViewer(datasourceKey, {
157
+ app: options.app,
158
+ file: options.file,
159
+ logType: 'test-e2e'
160
+ });
161
+ } catch (error) {
162
+ logger.error(chalk.red('❌ log-e2e failed:'), error.message);
163
+ process.exit(1);
164
+ }
165
+ });
166
+ }
167
+
168
+ function setupDatasourceLogIntegrationCommand(datasource) {
169
+ datasource.command('log-integration <datasourceKey>')
170
+ .description('Display latest or specified integration test log in readable format')
171
+ .option('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
172
+ .option('-f, --file <path>', 'Path to log file (default: latest in app logs folder)')
173
+ .action(async(datasourceKey, options) => {
174
+ try {
175
+ await runLogViewer(datasourceKey, {
176
+ app: options.app,
177
+ file: options.file,
178
+ logType: 'test-integration'
179
+ });
180
+ } catch (error) {
181
+ logger.error(chalk.red('❌ log-integration failed:'), error.message);
182
+ process.exit(1);
183
+ }
184
+ });
185
+ }
186
+
147
187
  /**
148
188
  * Setup datasource management commands
149
189
  * @param {Command} program - Commander program instance
150
190
  */
151
191
  function setupDatasourceCommands(program) {
152
192
  const datasource = program.command('datasource').description('Manage external data sources');
193
+ if (typeof datasource.alias === 'function') {
194
+ datasource.alias('ds');
195
+ }
153
196
  setupDatasourceValidateCommand(datasource);
154
197
  setupDatasourceListCommand(datasource);
155
198
  setupDatasourceDiffCommand(datasource);
156
199
  setupDatasourceUploadCommand(datasource);
157
200
  setupDatasourceTestIntegrationCommand(datasource);
158
201
  setupDatasourceTestE2ECommand(datasource);
202
+ setupDatasourceLogE2ECommand(datasource);
203
+ setupDatasourceLogIntegrationCommand(datasource);
159
204
  }
160
205
 
161
206
  module.exports = { setupDatasourceCommands };
@@ -341,7 +341,7 @@ async function runDevRefresh(options = {}) {
341
341
  logger.log(chalk.blue('\n🔄 Fetching settings from Builder Server...\n'));
342
342
  const settings = await devApi.getSettings(auth.serverUrl, clientCertPem, clientKeyPem || undefined);
343
343
  await config.mergeRemoteSettings(settings);
344
- logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev config" to verify.\n'));
344
+ logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev show" to verify.\n'));
345
345
  }
346
346
 
347
347
  module.exports = { runDevInit, runDevRefresh };
@@ -11,6 +11,7 @@ const path = require('path');
11
11
  const fs = require('fs');
12
12
  const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
13
13
  const { extractEnvTemplate } = require('../generator/split');
14
+ const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
14
15
 
15
16
  /**
16
17
  * Normalizes a keyvault config entry to canonical KV_* name and path-style value.
@@ -126,20 +127,25 @@ function buildExpectedByKey(effective) {
126
127
  }
127
128
 
128
129
  /**
129
- * Creates env.template when missing. Returns true if created (and change pushed).
130
+ * Creates env.template when missing using the Handlebars template (Authentication + Configuration sections).
130
131
  * @param {string} envPath - Path to env.template
131
- * @param {Map<string, string>} expectedByKey - Expected key->line map
132
+ * @param {Map<string, string>} expectedByKey - Expected key->line map (fallback when no systemParsed)
132
133
  * @param {boolean} dryRun - If true, do not write
133
134
  * @param {string[]} changes - Array to append to
135
+ * @param {Object} [systemParsed] - Parsed system config for template context (preferred)
134
136
  * @returns {boolean}
135
137
  */
136
- function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes) {
138
+ function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes, systemParsed) {
137
139
  if (fs.existsSync(envPath)) return false;
138
- const lines = Array.from(expectedByKey.values());
139
- const content = lines.join('\n');
140
- if (!content) return false;
140
+ if (expectedByKey.size === 0) return false;
141
+ const content = systemParsed
142
+ ? generateExternalEnvTemplateContent(systemParsed)
143
+ : Array.from(expectedByKey.values()).join('\n');
144
+ if (!content || !content.trim()) return false;
145
+ const hasKeyValueLine = /^[A-Z_][A-Z0-9_]*=/m.test(content);
146
+ if (!hasKeyValueLine) return false;
141
147
  if (!dryRun) {
142
- fs.writeFileSync(envPath, content + '\n', { mode: 0o644, encoding: 'utf8' });
148
+ fs.writeFileSync(envPath, content + (content.endsWith('\n') ? '' : '\n'), { mode: 0o644, encoding: 'utf8' });
143
149
  }
144
150
  changes.push('Created env.template from system configuration');
145
151
  return true;
@@ -236,7 +242,7 @@ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
236
242
  const expectedByKey = buildExpectedByKey(effective);
237
243
  const envPath = path.join(appPath, 'env.template');
238
244
 
239
- if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes)) {
245
+ if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes, systemParsed)) {
240
246
  return true;
241
247
  }
242
248
  if (!fs.existsSync(envPath)) {
@@ -10,10 +10,10 @@
10
10
 
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
- const yaml = require('js-yaml');
14
13
  const chalk = require('chalk');
15
14
  const logger = require('../utils/logger');
16
- const { loadConfigFile } = require('../utils/config-format');
15
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
16
+ const { resolveRbacPath } = require('../utils/app-config-resolver');
17
17
 
18
18
  const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
19
19
 
@@ -55,26 +55,30 @@ function collectPermissionNames(appPath, datasourceFiles) {
55
55
  }
56
56
 
57
57
  /**
58
- * Loads existing rbac or creates empty structure. Uses extractRbacFromSystem when provided.
58
+ * Loads existing RBAC file (rbac.yaml, rbac.yml, or rbac.json) or creates empty structure.
59
+ * Uses extractRbacFromSystem when no file exists. New file path respects format (rbac.json when format is 'json').
60
+ *
59
61
  * @param {string} appPath - Application path
60
62
  * @param {Object} [systemParsed] - Parsed system for extractRbacFromSystem
61
63
  * @param {Function} extractRbacFromSystem - (system) => rbac or null
62
- * @returns {{ rbac: Object, rbacPath: string, rbacYmlPath: string }}
64
+ * @param {string} [format] - 'json' or 'yaml'; used only when creating a new file (default 'yaml')
65
+ * @returns {{ rbac: Object, rbacPath: string }} rbacPath is resolved path or path.join(appPath, 'rbac.{json|yaml}') for new file
63
66
  */
64
- function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem) {
65
- const rbacPath = path.join(appPath, 'rbac.yaml');
66
- const rbacYmlPath = path.join(appPath, 'rbac.yml');
67
+ function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, format) {
68
+ const resolvedPath = resolveRbacPath(appPath);
67
69
  let rbac;
68
- if (fs.existsSync(rbacPath)) {
69
- rbac = loadConfigFile(rbacPath);
70
- } else if (fs.existsSync(rbacYmlPath)) {
71
- rbac = loadConfigFile(rbacYmlPath);
70
+ let rbacPath;
71
+ if (resolvedPath) {
72
+ rbac = loadConfigFile(resolvedPath);
73
+ rbacPath = resolvedPath;
72
74
  } else {
73
75
  rbac = extractRbacFromSystem(systemParsed) || { roles: [], permissions: [] };
74
76
  if (!Array.isArray(rbac.roles)) rbac.roles = [];
75
77
  if (!Array.isArray(rbac.permissions)) rbac.permissions = [];
78
+ const ext = (format === 'json') ? 'rbac.json' : 'rbac.yaml';
79
+ rbacPath = path.join(appPath, ext);
76
80
  }
77
- return { rbac, rbacPath, rbacYmlPath };
81
+ return { rbac, rbacPath };
78
82
  }
79
83
 
80
84
  /**
@@ -123,31 +127,33 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
123
127
  if (p.name && listGetPerms.includes(p.name) && !p.roles.includes(readerValue)) p.roles.push(readerValue);
124
128
  if (!p.roles.includes(adminValue)) p.roles.push(adminValue);
125
129
  }
126
- changes.push('Added default Admin and Reader roles to rbac.yaml');
130
+ changes.push('Added default Admin and Reader roles to rbac file');
127
131
  return true;
128
132
  }
129
133
 
130
134
  /**
131
135
  * Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
136
+ * When creating a new RBAC file, uses rbac.json if format is 'json', otherwise rbac.yaml.
137
+ *
132
138
  * @param {string} appPath - Application path
133
139
  * @param {Object} systemParsed - Parsed system (key, displayName)
134
140
  * @param {string[]} datasourceFiles - Datasource file names
135
141
  * @param {Function} extractRbacFromSystem - (system) => rbac or null
136
- * @param {boolean} dryRun - If true, do not write
137
- * @param {string[]} changes - Array to append change descriptions to
142
+ * @param {{ format?: string, dryRun: boolean, changes: string[] }} options - format ('json'|'yaml'), dryRun, changes array
138
143
  * @returns {boolean} True if rbac was updated (or would be in dry-run)
139
144
  */
140
- function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, dryRun, changes) {
145
+ function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, options) {
146
+ const { format = 'yaml', dryRun, changes } = options;
147
+ const rbacFormat = format === 'json' ? 'json' : 'yaml';
141
148
  const permissionNames = collectPermissionNames(appPath, datasourceFiles);
142
149
  if (permissionNames.size === 0) return false;
143
150
  const systemKey = systemParsed?.key || 'system';
144
151
  const displayName = systemParsed?.displayName || systemKey;
145
- const { rbac, rbacPath, rbacYmlPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem);
152
+ const { rbac, rbacPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, rbacFormat);
146
153
  let updated = addMissingPermissions(rbac, permissionNames, changes);
147
154
  updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
148
155
  if (updated && !dryRun) {
149
- const outPath = fs.existsSync(rbacPath) ? rbacPath : (fs.existsSync(rbacYmlPath) ? rbacYmlPath : rbacPath);
150
- fs.writeFileSync(outPath, yaml.dump(rbac, { indent: 2, lineWidth: -1 }), { mode: 0o644, encoding: 'utf8' });
156
+ writeConfigFile(rbacPath, rbac);
151
157
  }
152
158
  return updated;
153
159
  }
@@ -16,14 +16,14 @@
16
16
  const path = require('path');
17
17
  const fs = require('fs');
18
18
  const chalk = require('chalk');
19
- const yaml = require('js-yaml');
20
- const { detectAppType } = require('../utils/paths');
21
- const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
19
+ const { detectAppType, getDeployJsonPath } = require('../utils/paths');
20
+ const { resolveApplicationConfigPath, resolveRbacPath } = require('../utils/app-config-resolver');
22
21
  const { loadConfigFile, writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
23
22
  const { systemKeyToKvPrefix, securityKeyToVar } = require('../utils/credential-secrets-env');
24
23
  const logger = require('../utils/logger');
25
24
  const generator = require('../generator');
26
25
  const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
26
+ const { generateReadmeFromDeployJson } = require('../generator/split-readme');
27
27
  const { repairDatasourceFile } = require('./repair-datasource');
28
28
  const { mergeRbacFromDatasources } = require('./repair-rbac');
29
29
  const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
@@ -249,6 +249,11 @@ function normalizedAuthPartFromConfigName(name, systemKey) {
249
249
  const rest = n.slice(kvPrefix.length);
250
250
  return rest.toUpperCase().replace(/_/g, '');
251
251
  }
252
+ // Old-style or other KV_* names: treat last segment as var (e.g. KV_HUBSPOT_CLIENTID → CLIENTID)
253
+ if (n.toUpperCase().startsWith('KV_')) {
254
+ const parts = n.split('_').filter(Boolean);
255
+ if (parts.length >= 2) return parts[parts.length - 1].toUpperCase();
256
+ }
252
257
  return n.toUpperCase().replace(/_/g, '');
253
258
  }
254
259
 
@@ -273,21 +278,20 @@ function removeAuthVarsFromConfiguration(systemParsed, systemKey, dryRun, change
273
278
  return true;
274
279
  }
275
280
 
276
- function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes) {
277
- const rbacPath = path.join(appPath, 'rbac.yaml');
278
- const rbacYmlPath = path.join(appPath, 'rbac.yml');
279
- if (fs.existsSync(rbacPath) || fs.existsSync(rbacYmlPath)) return false;
281
+ function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes, format) {
282
+ if (resolveRbacPath(appPath)) return false;
280
283
  const rbacFromSystem = extractRbacFromSystem(systemParsed);
281
284
  if (!rbacFromSystem) return false;
282
285
  if (!dryRun) {
283
- const rbacYaml = yaml.dump(rbacFromSystem, { indent: 2, lineWidth: -1 });
284
- fs.writeFileSync(rbacPath, rbacYaml, { mode: 0o644, encoding: 'utf8' });
286
+ const rbacFormat = format === 'json' ? 'json' : 'yaml';
287
+ const defaultRbacPath = path.join(appPath, rbacFormat === 'json' ? 'rbac.json' : 'rbac.yaml');
288
+ writeConfigFile(defaultRbacPath, rbacFromSystem, rbacFormat);
285
289
  delete systemParsed.roles;
286
290
  delete systemParsed.permissions;
287
291
  writeConfigFile(systemFilePath, systemParsed);
288
292
  }
289
293
  changes.push('Created rbac.yaml from system roles/permissions');
290
- changes.push('Removed roles/permissions from system file (now in rbac.yaml)');
294
+ changes.push('Removed roles/permissions from system file (now in rbac file)');
291
295
  return true;
292
296
  }
293
297
 
@@ -338,6 +342,36 @@ async function regenerateManifest(appName, appPath, changes) {
338
342
  }
339
343
  }
340
344
 
345
+ /**
346
+ * Regenerates README.md from deployment manifest when options.doc is set.
347
+ * @param {string} appName - Application name
348
+ * @param {string} appPath - Application path
349
+ * @param {Object} options - Options (doc, dryRun)
350
+ * @param {string[]} changes - Array to append change messages to
351
+ * @returns {Promise<boolean>} True if README was regenerated
352
+ */
353
+ async function regenerateReadmeIfRequested(appName, appPath, options, changes) {
354
+ if (!options.doc) return false;
355
+ const deployJsonPath = getDeployJsonPath(appName, 'external', true);
356
+ if (!fs.existsSync(deployJsonPath) && !options.dryRun) {
357
+ await regenerateManifest(appName, appPath, changes);
358
+ }
359
+ if (!fs.existsSync(deployJsonPath)) return false;
360
+ try {
361
+ const deployment = JSON.parse(fs.readFileSync(deployJsonPath, 'utf8'));
362
+ const readmeContent = generateReadmeFromDeployJson(deployment);
363
+ const readmePath = path.join(appPath, 'README.md');
364
+ if (!options.dryRun) {
365
+ fs.writeFileSync(readmePath, readmeContent, { mode: 0o644, encoding: 'utf8' });
366
+ }
367
+ changes.push('Regenerated README.md from deployment manifest');
368
+ return true;
369
+ } catch (err) {
370
+ logger.log(chalk.yellow(`⚠ Could not regenerate README: ${err.message}`));
371
+ return false;
372
+ }
373
+ }
374
+
341
375
  function persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent) {
342
376
  if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
343
377
  writeYamlPreservingComments(configPath, originalYamlContent, variables);
@@ -410,7 +444,7 @@ function runRepairSteps(ctx) {
410
444
  ctx.appPath, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
411
445
  );
412
446
  const rbacFileCreated = createRbacFromSystemIfNeeded(
413
- ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes
447
+ ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes, ctx.format
414
448
  );
415
449
  const envTemplateRepaired = repairEnvTemplate(
416
450
  ctx.appPath, ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
@@ -431,7 +465,8 @@ function runRepairSteps(ctx) {
431
465
  * @param {string} appName - Application/integration name
432
466
  * @param {Object} [options] - Options
433
467
  * @param {boolean} [options.dryRun] - If true, only report changes; do not write
434
- * @returns {Promise<{ updated: boolean, changes: string[], systemFiles: string[], datasourceFiles: string[], appKeyFixed?: boolean, datasourceKeysFixed?: boolean, rbacFileCreated?: boolean, envTemplateRepaired?: boolean, manifestRegenerated?: boolean }>}
468
+ * @param {boolean} [options.doc] - If true, regenerate README.md from deployment manifest
469
+ * @returns {Promise<{ updated: boolean, changes: string[], systemFiles: string[], datasourceFiles: string[], appKeyFixed?: boolean, datasourceKeysFixed?: boolean, rbacFileCreated?: boolean, envTemplateRepaired?: boolean, manifestRegenerated?: boolean, readmeRegenerated?: boolean }>}
435
470
  */
436
471
  /**
437
472
  * Loads application config and discovers integration files; validates at least one system file exists.
@@ -453,7 +488,36 @@ function loadConfigAndDiscover(appPath, configPath) {
453
488
  return { variables, originalYamlContent, systemFiles, datasourceFiles };
454
489
  }
455
490
 
456
- async function repairExternalIntegration(appName, options = {}) {
491
+ /**
492
+ * Builds the repair result object from steps and flags.
493
+ * @param {Object} steps - Result of runRepairSteps
494
+ * @param {boolean} anyUpdated - Whether any repair made changes
495
+ * @param {boolean} manifestRegenerated - Whether manifest was regenerated
496
+ * @param {boolean} readmeRegenerated - Whether README was regenerated
497
+ * @param {{ changes: string[], systemFiles: string[], datasourceFiles: string[] }} ctx - changes and file lists
498
+ * @returns {Object} Combined result object
499
+ */
500
+ function buildRepairResult(steps, anyUpdated, manifestRegenerated, readmeRegenerated, ctx) {
501
+ return Object.assign(
502
+ { updated: anyUpdated, changes: ctx.changes, systemFiles: ctx.systemFiles, datasourceFiles: ctx.datasourceFiles },
503
+ {
504
+ appKeyFixed: steps.appKeyFixed,
505
+ datasourceKeysFixed: steps.datasourceKeysFixed,
506
+ rbacFileCreated: steps.rbacFileCreated,
507
+ envTemplateRepaired: steps.envTemplateRepaired,
508
+ manifestRegenerated,
509
+ readmeRegenerated
510
+ }
511
+ );
512
+ }
513
+
514
+ /**
515
+ * Validates repair inputs and resolves app path and config path.
516
+ * @param {string} appName - Application name
517
+ * @param {Object} options - Command options (auth)
518
+ * @returns {Promise<{ appPath: string, configPath: string, dryRun: boolean, authOption: string|undefined }>}
519
+ */
520
+ async function validateAndResolveRepairPaths(appName, options) {
457
521
  if (!appName || typeof appName !== 'string') throw new Error('App name is required');
458
522
  const { dryRun = false, auth: authOption } = options;
459
523
  if (authOption !== undefined && authOption !== null && typeof authOption !== 'string') {
@@ -463,6 +527,11 @@ async function repairExternalIntegration(appName, options = {}) {
463
527
  if (!isExternal) throw new Error(`App '${appName}' is not an external integration`);
464
528
  const configPath = resolveApplicationConfigPath(appPath);
465
529
  if (!fs.existsSync(configPath)) throw new Error(`Application config not found: ${configPath}`);
530
+ return { appPath, configPath, dryRun, authOption };
531
+ }
532
+
533
+ async function repairExternalIntegration(appName, options = {}) {
534
+ const { appPath, configPath, dryRun, authOption } = await validateAndResolveRepairPaths(appName, options);
466
535
 
467
536
  const { variables, originalYamlContent, systemFiles, datasourceFiles: initialDatasourceFiles } = loadConfigAndDiscover(appPath, configPath);
468
537
  const changes = [];
@@ -486,30 +555,27 @@ async function repairExternalIntegration(appName, options = {}) {
486
555
  datasourceFiles,
487
556
  dryRun,
488
557
  changes,
489
- auth: authOption
558
+ auth: authOption,
559
+ format: options.format === 'json' ? 'json' : 'yaml'
490
560
  });
491
-
492
- const opts = { expose: Boolean(options.expose), sync: Boolean(options.sync), test: Boolean(options.test) };
493
- const datasourceRepairUpdated = runDatasourceRepairs(appPath, datasourceFiles, opts, dryRun, changes);
561
+ const datasourceRepairUpdated = runDatasourceRepairs(appPath, datasourceFiles, {
562
+ expose: Boolean(options.expose),
563
+ sync: Boolean(options.sync),
564
+ test: Boolean(options.test)
565
+ }, dryRun, changes);
494
566
  const rbacMergeUpdated = options.rbac
495
- ? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, dryRun, changes)
567
+ ? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, {
568
+ format: options.format === 'json' ? 'json' : 'yaml',
569
+ dryRun,
570
+ changes
571
+ })
496
572
  : false;
497
573
  const anyUpdated = keysNormalized || steps.updated || datasourceRepairUpdated || rbacMergeUpdated;
498
574
  const manifestRegenerated = (anyUpdated && !dryRun)
499
575
  ? await persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent)
500
576
  : false;
501
-
502
- return {
503
- updated: anyUpdated,
504
- changes,
505
- systemFiles,
506
- datasourceFiles,
507
- appKeyFixed: steps.appKeyFixed,
508
- datasourceKeysFixed: steps.datasourceKeysFixed,
509
- rbacFileCreated: steps.rbacFileCreated,
510
- envTemplateRepaired: steps.envTemplateRepaired,
511
- manifestRegenerated
512
- };
577
+ const readmeRegenerated = await regenerateReadmeIfRequested(appName, appPath, options, changes);
578
+ return buildRepairResult(steps, anyUpdated, manifestRegenerated, readmeRegenerated, { changes, systemFiles, datasourceFiles });
513
579
  }
514
580
 
515
581
  module.exports = {
@@ -33,7 +33,7 @@ function removeKeyFromFile(key, filePath) {
33
33
  throw new Error(`Secret '${key}' not found.`);
34
34
  }
35
35
  delete data[key];
36
- const yamlContent = yaml.dump(data, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false });
36
+ const yamlContent = yaml.dump(data, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false });
37
37
  fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
38
38
  }
39
39
 
@@ -12,6 +12,7 @@ const chalk = require('chalk');
12
12
  const path = require('path');
13
13
  const logger = require('../utils/logger');
14
14
  const { validateSecretsFile } = require('../utils/secrets-validation');
15
+ const { validateDataplaneSecrets } = require('../utils/token-manager');
15
16
  const pathsUtil = require('../utils/paths');
16
17
  const secretsEnsure = require('../core/secrets-ensure');
17
18
 
@@ -38,13 +39,25 @@ async function handleSecretsValidate(pathArg, options = {}) {
38
39
  }
39
40
 
40
41
  const result = validateSecretsFile(filePath, { checkNaming: Boolean(options.naming) });
42
+ const dataplaneResult = validateDataplaneSecrets(filePath);
43
+ const allValid = result.valid && dataplaneResult.valid;
41
44
  if (result.valid) {
42
45
  logger.log(chalk.green(`✓ Secrets file is valid: ${result.path}`));
43
- return { valid: true, errors: [] };
46
+ } else {
47
+ logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
48
+ result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
44
49
  }
45
- logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
46
- result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
47
- return { valid: false, errors: result.errors };
50
+ if (!dataplaneResult.valid) {
51
+ logger.log(chalk.yellow(`⚠ ${dataplaneResult.hint}`));
52
+ if (result.valid) {
53
+ logger.log(chalk.yellow(' Wizard/dataplane calls may fail until dataplane credentials are present.'));
54
+ }
55
+ }
56
+ return {
57
+ valid: allValid,
58
+ errors: allValid ? [] : [...result.errors, ...(dataplaneResult.valid ? [] : [dataplaneResult.hint])],
59
+ dataplaneValid: dataplaneResult.valid
60
+ };
48
61
  }
49
62
 
50
63
  module.exports = { handleSecretsValidate };