@aifabrix/builder 2.60.0 → 2.62.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 (177) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/api/applications.api.js +22 -2
  14. package/lib/api/bootstrap-snapshot.api.js +115 -0
  15. package/lib/api/configuration.api.js +131 -0
  16. package/lib/api/system-secrets.api.js +72 -0
  17. package/lib/app/deploy.js +14 -5
  18. package/lib/app/run-docker-fallback.js +6 -1
  19. package/lib/app/run-env-compose.js +5 -6
  20. package/lib/app/run-helpers.js +2 -1
  21. package/lib/app/run-parameter-sync.js +142 -0
  22. package/lib/app/run.js +6 -4
  23. package/lib/build/docker-build-args.js +5 -2
  24. package/lib/build/index.js +3 -2
  25. package/lib/build/standard-docker-build.js +6 -2
  26. package/lib/channels/channel-artifact-generator.js +13 -3
  27. package/lib/cli/infra-guided-footers.js +10 -8
  28. package/lib/cli/infra-guided.js +30 -10
  29. package/lib/cli/setup-app.js +5 -0
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  32. package/lib/cli/setup-platform.js +2 -0
  33. package/lib/commands/auth-config.js +2 -0
  34. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  35. package/lib/commands/datasource-capability.js +4 -2
  36. package/lib/commands/env-secret-context.js +113 -0
  37. package/lib/commands/env-secret-list.js +200 -0
  38. package/lib/commands/env-secret-push-confirm.js +54 -0
  39. package/lib/commands/env-secret-push-run.js +227 -0
  40. package/lib/commands/env-secret-push.js +168 -0
  41. package/lib/commands/login.js +4 -0
  42. package/lib/commands/repair-datasource-apply.js +5 -1
  43. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  44. package/lib/commands/repair-datasource-legacy.js +94 -0
  45. package/lib/commands/repair-datasource-run.js +1 -0
  46. package/lib/commands/setup-image-refresh.js +26 -0
  47. package/lib/commands/setup-modes.js +8 -9
  48. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  49. package/lib/commands/setup-prompts.js +2 -180
  50. package/lib/commands/setup.js +3 -0
  51. package/lib/commands/up-builder-api.js +24 -8
  52. package/lib/commands/up-common.js +16 -1
  53. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  54. package/lib/commands/up-dataplane-credentials.js +21 -0
  55. package/lib/commands/up-dataplane.js +37 -18
  56. package/lib/commands/up-integration-server.js +112 -0
  57. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  58. package/lib/commands/verify-operations-steps.js +13 -1
  59. package/lib/commands/wizard-config-normalizer.js +12 -5
  60. package/lib/commands/wizard-core.js +5 -157
  61. package/lib/commands/wizard-file-saving.js +163 -0
  62. package/lib/core/admin-secrets-env-overlay.js +8 -3
  63. package/lib/core/env-platform-expand.js +5 -1
  64. package/lib/core/environment-access-policy.js +121 -0
  65. package/lib/core/environment-mode-policy.js +177 -0
  66. package/lib/core/local-env-overrides.js +149 -0
  67. package/lib/core/secrets-ensure.js +4 -2
  68. package/lib/core/secrets-env-content.js +17 -10
  69. package/lib/core/secrets-env-write.js +11 -4
  70. package/lib/core/secrets-load.js +4 -2
  71. package/lib/datasource/binary-documents-validator.js +190 -0
  72. package/lib/datasource/capability/capability-manifest.js +1 -0
  73. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  74. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  75. package/lib/datasource/capability/upsert-operations.js +138 -0
  76. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  77. package/lib/datasource/validate.js +12 -5
  78. package/lib/deployment/installation/index.js +14 -1
  79. package/lib/deployment/installation/infra-catalog.js +3 -0
  80. package/lib/deployment/installation/local-environment-stage.js +285 -0
  81. package/lib/deployment/installation/local-installation.js +76 -0
  82. package/lib/deployment/installation/stage-input.js +5 -1
  83. package/lib/generator/builders.js +3 -1
  84. package/lib/generator/index.js +3 -0
  85. package/lib/generator/split-variables.js +2 -2
  86. package/lib/lifecycle/product-model.js +4 -3
  87. package/lib/lifecycle/report-display.js +3 -2
  88. package/lib/parameters/database-adoption.js +125 -0
  89. package/lib/parameters/infra-parameter-catalog.js +1 -1
  90. package/lib/parameters/physical-database-naming.js +136 -0
  91. package/lib/parameters/physical-secret-name.js +194 -0
  92. package/lib/parameters/rsa-secret-values.js +165 -0
  93. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  94. package/lib/programmatic/builder-help-governance.js +1 -1
  95. package/lib/programmatic/builder-help.js +1 -1
  96. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  97. package/lib/programmatic/run-channel-package.js +3 -1
  98. package/lib/programmatic/workspace-context.js +37 -2
  99. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  100. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  101. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  102. package/lib/role-assistant/test-runner-workhub.js +28 -19
  103. package/lib/schema/application-schema.json +14 -2
  104. package/lib/schema/external-datasource.schema.json +23 -3
  105. package/lib/schema/infra.parameter.yaml +440 -56
  106. package/lib/schema/infrastructure-schema.json +213 -120
  107. package/lib/utils/compose-generate-docker-compose.js +9 -14
  108. package/lib/utils/compose-generator.js +4 -7
  109. package/lib/utils/compose-miso-env.js +2 -2
  110. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  111. package/lib/utils/datasource-binary-evidence.js +92 -0
  112. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  113. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  114. package/lib/utils/datasource-test-run-display.js +8 -2
  115. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  116. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  117. package/lib/utils/declarative-url-ports.js +3 -0
  118. package/lib/utils/env-copy.js +11 -10
  119. package/lib/utils/env-environment-file-paths.js +105 -0
  120. package/lib/utils/environment-scoped-resources.js +48 -14
  121. package/lib/utils/external-system-system-test-tty.js +3 -2
  122. package/lib/utils/image-tags.js +2 -2
  123. package/lib/utils/paths-system-builder-keys.js +7 -1
  124. package/lib/utils/paths.js +7 -2
  125. package/lib/utils/platform-kv-ref.js +1 -1
  126. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  127. package/lib/utils/prepare-local-data-mount.js +58 -0
  128. package/lib/utils/redis-env-scope.js +21 -1
  129. package/lib/utils/registry-auth-sources.js +154 -0
  130. package/lib/utils/registry-credentials.js +131 -14
  131. package/lib/utils/secrets-generator.js +30 -21
  132. package/lib/utils/secrets-helpers.js +5 -8
  133. package/lib/utils/secrets-kv-scope.js +127 -28
  134. package/lib/utils/secrets-materialize-local.js +9 -0
  135. package/lib/utils/secrets-missing-error.js +35 -1
  136. package/lib/utils/system-secret-mapping.js +125 -0
  137. package/lib/utils/test-log-writer.js +2 -1
  138. package/lib/utils/token-manager.js +14 -8
  139. package/lib/utils/url-public-path-prefix.js +6 -10
  140. package/lib/validation/external-manifest-validator.js +5 -0
  141. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  142. package/lib/validation/validate-external-file.js +5 -1
  143. package/package.json +6 -4
  144. package/templates/agent-kit/agent-kit.yaml +1 -1
  145. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  146. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  147. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  148. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  149. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  150. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  151. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  152. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  153. package/templates/agent-kit/skills/shared/status.md +3 -12
  154. package/templates/applications/builder-api/application.yaml +1 -1
  155. package/templates/applications/builder-api/env.template +5 -1
  156. package/templates/applications/dataplane/application.yaml +2 -2
  157. package/templates/applications/dataplane/env.template +10 -12
  158. package/templates/applications/integration-server/application.yaml +91 -0
  159. package/templates/applications/integration-server/env.template +215 -0
  160. package/templates/applications/keycloak/application.yaml +6 -1
  161. package/templates/applications/mirrored-templates.json +52 -0
  162. package/templates/applications/miso-controller/application.yaml +116 -2
  163. package/templates/applications/miso-controller/env.template +62 -17
  164. package/templates/applications/miso-controller/rbac.yaml +17 -0
  165. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  166. package/templates/marketplace/createUiDefinition.json +52 -4
  167. package/templates/marketplace/main.json +88 -10
  168. package/templates/python/docker-compose.hbs +1 -1
  169. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  170. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  171. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  172. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  173. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  174. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  175. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  176. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  177. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -3,28 +3,24 @@
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
- /* eslint-disable max-lines */
7
6
 
7
+ const path = require('path');
8
8
  const chalk = require('chalk');
9
9
  const ora = require('ora');
10
- const path = require('path');
11
- const fs = require('fs').promises;
12
10
  const logger = require('../utils/logger');
13
11
  const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
14
12
  const { resolveControllerUrl } = require('../utils/controller-url');
15
13
  const { normalizeWizardConfigs } = require('./wizard-config-normalizer');
16
- const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-layout-chalk');
14
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
17
15
  const {
18
16
  createWizardSession,
19
17
  updateWizardSession,
20
18
  detectType,
21
19
  getPlatformConfig,
22
20
  generateConfig,
23
- validateWizardConfig,
24
- getDeploymentDocs,
25
- postDeploymentDocs
21
+ validateWizardConfig
26
22
  } = require('../api/wizard.api');
27
- const { generateWizardFiles } = require('../generator/wizard');
23
+ const { handleFileSaving, validateAndCheckAppDirectory } = require('./wizard-file-saving');
28
24
  const {
29
25
  parseOpenApiSource,
30
26
  testMcpServerConnection,
@@ -44,38 +40,6 @@ const {
44
40
  const { validateDatasourceKeysBeforePlatformConfig } = require('../validation/validate-datasource-keys-api');
45
41
  const { handleEntitySelection } = require('./wizard-entity-selection');
46
42
 
47
- /**
48
- * Validate app name and check if directory exists
49
- * @async
50
- * @function validateAndCheckAppDirectory
51
- * @param {string} appName - Application name
52
- * @param {boolean} [interactive=true] - Whether to prompt for confirmation
53
- * @returns {Promise<boolean>} True if should continue, false if cancelled
54
- */
55
- async function validateAndCheckAppDirectory(appName, interactive = true) {
56
- if (!/^[a-z0-9-_]+$/.test(appName)) {
57
- throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
58
- }
59
- const appPath = path.join(process.cwd(), 'integration', appName);
60
- try {
61
- await fs.access(appPath);
62
- if (interactive) {
63
- const { overwrite } = await require('inquirer').prompt([{
64
- type: 'confirm', name: 'overwrite',
65
- message: `Directory ${appPath} already exists. Overwrite?`, default: false
66
- }]);
67
- if (!overwrite) {
68
- logger.log(chalk.yellow('Wizard cancelled.')); return false;
69
- }
70
- } else {
71
- logger.log(chalk.yellow(`Warning: Directory ${appPath} exists. Overwriting...`));
72
- }
73
- } catch (error) {
74
- if (error.code !== 'ENOENT') throw error;
75
- }
76
- return true;
77
- }
78
-
79
43
  /**
80
44
  * Extract session ID from response data
81
45
  * @function extractSessionId
@@ -393,7 +357,7 @@ async function handleConfigurationGeneration(dataplaneUrl, authConfig, options)
393
357
  }
394
358
  const result = extractConfigurationFromResponse(generateResponse);
395
359
  const systemConfig = resolveSystemConfigForWizardMode(result.systemConfig, options);
396
- const normalized = normalizeWizardConfigs(systemConfig, result.datasourceConfigs);
360
+ const normalized = normalizeWizardConfigs(systemConfig, result.datasourceConfigs, options.openapiSpec);
397
361
  await maybeWriteWizardDebugLog(generateResponse, prefs, options);
398
362
  logger.log(formatSuccessLine('Configuration generated successfully'));
399
363
  return {
@@ -446,122 +410,6 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
446
410
  }
447
411
  }
448
412
 
449
- /**
450
- * Fetches deployment docs and writes README.md when application config and deploy JSON are available.
451
- * @async
452
- * @param {string} appPath - Application path
453
- * @param {string} appName - Application name
454
- * @param {string} dataplaneUrl - Dataplane URL
455
- * @param {Object} authConfig - Authentication configuration
456
- * @param {string} systemKey - System key
457
- */
458
- async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
459
- const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
460
- const deployPath = path.join(appPath, `${appName}-deploy.json`);
461
- let variablesYaml = null;
462
- let deployJson = null;
463
- try {
464
- const configPath = resolveApplicationConfigPath(appPath);
465
- variablesYaml = await fs.readFile(configPath, 'utf8');
466
- } catch {
467
- // optional
468
- }
469
- try {
470
- const deployContent = await fs.readFile(deployPath, 'utf8');
471
- deployJson = JSON.parse(deployContent);
472
- } catch {
473
- // optional
474
- }
475
- const hasBody = variablesYaml !== null || deployJson !== null;
476
- const body = hasBody ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
477
- const docsResponse = body
478
- ? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
479
- : await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
480
- const content = docsResponse?.data?.content ?? docsResponse?.content;
481
- // Only overwrite README when the API returns substantial content; otherwise keep the template README
482
- const MIN_README_LENGTH = 400;
483
- if (content && typeof content === 'string' && content.trim().length >= MIN_README_LENGTH) {
484
- const readmePath = path.join(appPath, 'README.md');
485
- await fs.writeFile(readmePath, content, 'utf8');
486
- logger.log(chalk.gray(' Updated README.md from deployment-docs API (application config + deploy JSON).'));
487
- }
488
- }
489
-
490
- /**
491
- * Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
492
- * @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
493
- * @param {string} format - Project format: yaml | json
494
- */
495
- function logWizardFileSaveFooter(appName, generatedFiles) {
496
- logger.log(formatSuccessParagraph('Wizard completed successfully!'));
497
- logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
498
- logger.log(chalk.blue('\nNext steps:'));
499
- logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
500
- logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
501
- logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
502
- }
503
-
504
- function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
505
- const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
506
- const { loadConfigFile } = require('../utils/config-format');
507
- const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
508
- const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
509
- const rbacChanges = [];
510
- const rbacUpdated = mergeRbacFromDatasources(
511
- generatedFiles.appPath,
512
- systemParsedForRbac,
513
- datasourceFileNames,
514
- extractRbacFromSystem,
515
- { format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
516
- );
517
- if (rbacUpdated && rbacChanges.length) {
518
- rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
519
- logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
520
- }
521
- }
522
-
523
- /**
524
- * Handle file saving step
525
- * @async
526
- * @function handleFileSaving
527
- * @param {string} appName - Application name
528
- * @param {Object} systemConfig - System configuration
529
- * @param {Object[]} datasourceConfigs - Datasource configurations
530
- * @param {string} systemKey - System key
531
- * @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
532
- * @returns {Promise<Object>} Generated files information
533
- */
534
- async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
535
- const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
536
- logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
537
- const spinner = ora('Saving files...').start();
538
- try {
539
- const config = require('../core/config');
540
- const format = ctx?.format || (await config.getFormat()) || 'yaml';
541
- const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
542
- if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
543
- try {
544
- mergeRbacAfterWizardFilesWritten(generatedFiles, format);
545
- } catch (e) {
546
- logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
547
- }
548
- }
549
- if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
550
- try {
551
- await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
552
- } catch (e) {
553
- logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
554
- }
555
- }
556
- spinner.stop();
557
- logWizardFileSaveFooter(appName, generatedFiles);
558
- return generatedFiles;
559
- } catch (error) {
560
- spinner.stop();
561
- throw error;
562
- }
563
- }
564
-
565
413
  /**
566
414
  * Setup dataplane URL and authentication
567
415
  * @async
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @fileoverview Save wizard artifacts and update their README and RBAC files.
3
+ * @author AI Fabrix Team
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const path = require('path');
9
+ const fs = require('fs').promises;
10
+ const logger = require('../utils/logger');
11
+ const { formatSuccessParagraph } = require('../utils/cli-layout-chalk');
12
+ const { getDeploymentDocs, postDeploymentDocs } = require('../api/wizard.api');
13
+ const { generateWizardFiles } = require('../generator/wizard');
14
+
15
+ /**
16
+ * Fetches deployment docs and writes README.md when application config and deploy JSON are available.
17
+ * @async
18
+ * @param {string} appPath - Application path
19
+ * @param {string} appName - Application name
20
+ * @param {string} dataplaneUrl - Dataplane URL
21
+ * @param {Object} authConfig - Authentication configuration
22
+ * @param {string} systemKey - System key
23
+ */
24
+ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
25
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
26
+ const deployPath = path.join(appPath, `${appName}-deploy.json`);
27
+ let variablesYaml = null;
28
+ let deployJson = null;
29
+ try {
30
+ const configPath = resolveApplicationConfigPath(appPath);
31
+ variablesYaml = await fs.readFile(configPath, 'utf8');
32
+ } catch {
33
+ // optional
34
+ }
35
+ try {
36
+ const deployContent = await fs.readFile(deployPath, 'utf8');
37
+ deployJson = JSON.parse(deployContent);
38
+ } catch {
39
+ // optional
40
+ }
41
+ const hasBody = variablesYaml !== null || deployJson !== null;
42
+ const body = hasBody ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
43
+ const docsResponse = body
44
+ ? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
45
+ : await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
46
+ const content = docsResponse?.data?.content ?? docsResponse?.content;
47
+ // Only overwrite README when the API returns substantial content; otherwise keep the template README
48
+ const MIN_README_LENGTH = 400;
49
+ if (content && typeof content === 'string' && content.trim().length >= MIN_README_LENGTH) {
50
+ const readmePath = path.join(appPath, 'README.md');
51
+ await fs.writeFile(readmePath, content, 'utf8');
52
+ logger.log(chalk.gray(' Updated README.md from deployment-docs API (application config + deploy JSON).'));
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
58
+ * @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
59
+ * @param {string} format - Project format: yaml | json
60
+ */
61
+ function logWizardFileSaveFooter(appName, generatedFiles) {
62
+ logger.log(formatSuccessParagraph('Wizard completed successfully!'));
63
+ logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
64
+ logger.log(chalk.blue('\nNext steps:'));
65
+ logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
66
+ logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
67
+ logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
68
+ }
69
+
70
+ function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
71
+ const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
72
+ const { loadConfigFile } = require('../utils/config-format');
73
+ const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
74
+ const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
75
+ const rbacChanges = [];
76
+ const rbacUpdated = mergeRbacFromDatasources(
77
+ generatedFiles.appPath,
78
+ systemParsedForRbac,
79
+ datasourceFileNames,
80
+ extractRbacFromSystem,
81
+ { format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
82
+ );
83
+ if (rbacUpdated && rbacChanges.length) {
84
+ rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
85
+ logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Handle file saving step
91
+ * @async
92
+ * @function handleFileSaving
93
+ * @param {string} appName - Application name
94
+ * @param {Object} systemConfig - System configuration
95
+ * @param {Object[]} datasourceConfigs - Datasource configurations
96
+ * @param {string} systemKey - System key
97
+ * @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
98
+ * @returns {Promise<Object>} Generated files information
99
+ */
100
+ async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
101
+ const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
102
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
103
+ const spinner = ora('Saving files...').start();
104
+ try {
105
+ const config = require('../core/config');
106
+ const format = ctx?.format || (await config.getFormat()) || 'yaml';
107
+ const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
108
+ if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
109
+ try {
110
+ mergeRbacAfterWizardFilesWritten(generatedFiles, format);
111
+ } catch (e) {
112
+ logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
113
+ }
114
+ }
115
+ if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
116
+ try {
117
+ await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
118
+ } catch (e) {
119
+ logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
120
+ }
121
+ }
122
+ spinner.stop();
123
+ logWizardFileSaveFooter(appName, generatedFiles);
124
+ return generatedFiles;
125
+ } catch (error) {
126
+ spinner.stop();
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Validate app name and check if directory exists
133
+ * @async
134
+ * @function validateAndCheckAppDirectory
135
+ * @param {string} appName - Application name
136
+ * @param {boolean} [interactive=true] - Whether to prompt for confirmation
137
+ * @returns {Promise<boolean>} True if should continue, false if cancelled
138
+ */
139
+ async function validateAndCheckAppDirectory(appName, interactive = true) {
140
+ if (!/^[a-z0-9-_]+$/.test(appName)) {
141
+ throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
142
+ }
143
+ const appPath = path.join(process.cwd(), 'integration', appName);
144
+ try {
145
+ await fs.access(appPath);
146
+ if (interactive) {
147
+ const { overwrite } = await require('inquirer').prompt([{
148
+ type: 'confirm', name: 'overwrite',
149
+ message: `Directory ${appPath} already exists. Overwrite?`, default: false
150
+ }]);
151
+ if (!overwrite) {
152
+ logger.log(chalk.yellow('Wizard cancelled.')); return false;
153
+ }
154
+ } else {
155
+ logger.log(chalk.yellow(`Warning: Directory ${appPath} exists. Overwriting...`));
156
+ }
157
+ } catch (error) {
158
+ if (error.code !== 'ENOENT') throw error;
159
+ }
160
+ return true;
161
+ }
162
+
163
+ module.exports = { handleFileSaving, validateAndCheckAppDirectory };
@@ -10,9 +10,10 @@
10
10
 
11
11
  /**
12
12
  * @param {Object.<string, string>} adminObj
13
+ * @param {string} [appName]
13
14
  * @returns {Object.<string, string>}
14
15
  */
15
- function buildPlatformEnvOverlayFromAdminSecrets(adminObj) {
16
+ function buildPlatformEnvOverlayFromAdminSecrets(adminObj, appName) {
16
17
  if (!adminObj || typeof adminObj !== 'object') return {};
17
18
  const platformPwd =
18
19
  adminObj.PLATFORM_ADMIN_PASSWORD ||
@@ -39,6 +40,9 @@ function buildPlatformEnvOverlayFromAdminSecrets(adminObj) {
39
40
  if (email) {
40
41
  overlay.ONBOARDING_ADMIN_EMAIL = email;
41
42
  }
43
+ if (appName === 'miso-controller' && adminObj.POSTGRES_PASSWORD) {
44
+ overlay.POSTGRES_ADMIN_PASSWORD = adminObj.POSTGRES_PASSWORD;
45
+ }
42
46
  return overlay;
43
47
  }
44
48
 
@@ -46,9 +50,10 @@ function buildPlatformEnvOverlayFromAdminSecrets(adminObj) {
46
50
  * Apply admin-secrets values over resolved app env (admin-secrets wins for platform keys).
47
51
  * @param {Object.<string, string>} envMap
48
52
  * @param {Object.<string, string>} adminObj
53
+ * @param {string} [appName]
49
54
  */
50
- function applyAdminSecretsPlatformOverlay(envMap, adminObj) {
51
- const overlay = buildPlatformEnvOverlayFromAdminSecrets(adminObj);
55
+ function applyAdminSecretsPlatformOverlay(envMap, adminObj, appName) {
56
+ const overlay = buildPlatformEnvOverlayFromAdminSecrets(adminObj, appName);
52
57
  for (const [key, value] of Object.entries(overlay)) {
53
58
  if (value !== undefined && value !== null && String(value).trim() !== '') {
54
59
  envMap[key] = String(value);
@@ -39,9 +39,13 @@ function expandValue(value, values, key, strict, context = {}) {
39
39
  const where = context.resourceGroup
40
40
  ? `resource group "${context.resourceGroup}" and environment "${context.environment || ''}"`
41
41
  : `environment "${context.environment || ''}" (no resource group, so platform values are unavailable)`;
42
+ const hint = context.resourceGroup
43
+ ? ''
44
+ : ' The environment key comes from --env, or from the current CLI environment' +
45
+ ' (aifabrix auth set-environment <key>).';
42
46
  throw new Error(
43
47
  `${key} references {${name}}, which no platform or environment configuration resolves for ` +
44
- `${where}. Declare it under environments, or run against a resource group that supplies it.`
48
+ `${where}. Declare it under environments, or run against a resource group that supplies it.${hint}`
45
49
  );
46
50
  });
47
51
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Who may act on which environment (plan 210.0).
3
+ *
4
+ * Cross-environment management is gated by the **signed-in user's** access, never by a
5
+ * standing service identity. The distinction matters because the bootstrap credential that
6
+ * makes a silent `dev` install possible would otherwise also reach `pro`: one shared secret
7
+ * on a developer host would become a route into production, and nothing in the audit trail
8
+ * would name a person.
9
+ *
10
+ * The entitled set therefore has to come from the user's own token. That is a property of
11
+ * the caller rather than of this module, so the rule is made mechanical: a caller states
12
+ * where the list came from, and anything other than a signed-in user is refused here rather
13
+ * than trusted to have checked.
14
+ *
15
+ * **A request touching several environments is all-or-nothing, decided before anything
16
+ * runs.** A partly applied cross-environment operation leaves the installation in a state
17
+ * nobody asked for, and the exit code does not say which half happened; refusing the whole
18
+ * request is recoverable, half-doing it is not.
19
+ *
20
+ * Pure: no I/O, no token parsing. The caller supplies what it learned.
21
+ *
22
+ * @fileoverview Environment entitlement checks for cross-environment commands
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ /** The only acceptable provenance for an entitled-environment list. */
28
+ const USER_ACCESS_SOURCE = 'user';
29
+
30
+ /**
31
+ * @param {unknown} value - Environment key
32
+ * @returns {string}
33
+ */
34
+ function normalizeEnvKey(value) {
35
+ return String(value ?? '').trim().toLowerCase();
36
+ }
37
+
38
+ /**
39
+ * Normalise an entitled-environment list to comparable keys.
40
+ *
41
+ * @param {Array<unknown>|null|undefined} entitled - Environments the user may act on
42
+ * @returns {string[]} Unique, normalised, in the order given
43
+ */
44
+ function normalizeEntitledEnvironments(entitled) {
45
+ if (!Array.isArray(entitled)) {
46
+ return [];
47
+ }
48
+ const seen = new Set();
49
+ const out = [];
50
+ for (const item of entitled) {
51
+ const key = normalizeEnvKey(typeof item === 'object' && item ? item.key ?? item.name : item);
52
+ if (!key || seen.has(key)) continue;
53
+ seen.add(key);
54
+ out.push(key);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Split a request into what this user may and may not act on.
61
+ *
62
+ * @param {Array<unknown>} requested - Environments the command would touch
63
+ * @param {Array<unknown>} entitled - Environments the user may act on
64
+ * @returns {{ allowed: string[], refused: string[] }}
65
+ */
66
+ function partitionByEntitlement(requested, entitled) {
67
+ const permitted = new Set(normalizeEntitledEnvironments(entitled));
68
+ const allowed = [];
69
+ const refused = [];
70
+ for (const item of Array.isArray(requested) ? requested : [requested]) {
71
+ const key = normalizeEnvKey(item);
72
+ if (!key) continue;
73
+ (permitted.has(key) ? allowed : refused).push(key);
74
+ }
75
+ return { allowed, refused };
76
+ }
77
+
78
+ /**
79
+ * Refuse a request that reaches an environment this user is not entitled to.
80
+ *
81
+ * @param {Array<unknown>|string} requested - Environments the command would touch
82
+ * @param {Object} access - What the caller learned about the signed-in user
83
+ * @param {Array<unknown>} access.entitled - Environments the user may act on
84
+ * @param {string} access.source - Provenance; must be `user`
85
+ * @returns {string[]} The requested environments, normalised, when every one is permitted
86
+ * @throws {Error} When the list is not user-scoped, or any environment is not permitted
87
+ */
88
+ function assertEnvironmentsEntitled(requested, access = {}) {
89
+ if (normalizeEnvKey(access.source) !== USER_ACCESS_SOURCE) {
90
+ throw new Error(
91
+ 'Environment access must come from the signed-in user, not a service identity. ' +
92
+ 'Run "aifabrix login" and retry.'
93
+ );
94
+ }
95
+ const entitled = normalizeEntitledEnvironments(access.entitled);
96
+ const { allowed, refused } = partitionByEntitlement(requested, access.entitled);
97
+
98
+ if (entitled.length === 0) {
99
+ throw new Error(
100
+ 'You are signed in but entitled to no environment. Ask an administrator for access, ' +
101
+ 'or check the controller with: aifabrix env secret list'
102
+ );
103
+ }
104
+ if (refused.length > 0) {
105
+ // Named in full, and nothing is run. Reporting only the first would hide the rest
106
+ // behind a retry loop, and running the entitled ones first would half-apply the request.
107
+ throw new Error(
108
+ `Not entitled to ${refused.join(', ')}. You may act on ${entitled.join(', ')}.\n` +
109
+ 'Nothing was changed: a request spanning environments is refused as a whole so it ' +
110
+ 'cannot be left half applied.'
111
+ );
112
+ }
113
+ return allowed;
114
+ }
115
+
116
+ module.exports = {
117
+ USER_ACCESS_SOURCE,
118
+ normalizeEntitledEnvironments,
119
+ partitionByEntitlement,
120
+ assertEnvironmentsEntitled
121
+ };