@aifabrix/builder 2.60.0 → 2.61.2

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 (112) 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/configuration.api.js +131 -0
  14. package/lib/api/system-secrets.api.js +72 -0
  15. package/lib/app/run-docker-fallback.js +6 -1
  16. package/lib/app/run-helpers.js +2 -1
  17. package/lib/app/run-parameter-sync.js +142 -0
  18. package/lib/build/docker-build-args.js +5 -2
  19. package/lib/build/index.js +3 -2
  20. package/lib/build/standard-docker-build.js +2 -2
  21. package/lib/cli/setup-app.js +5 -0
  22. package/lib/cli/setup-environment.js +156 -0
  23. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  24. package/lib/commands/datasource-capability.js +4 -2
  25. package/lib/commands/env-secret-context.js +113 -0
  26. package/lib/commands/env-secret-list.js +200 -0
  27. package/lib/commands/env-secret-push-confirm.js +54 -0
  28. package/lib/commands/env-secret-push-run.js +227 -0
  29. package/lib/commands/env-secret-push.js +168 -0
  30. package/lib/commands/repair-datasource-apply.js +2 -0
  31. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  32. package/lib/commands/repair-datasource-run.js +1 -0
  33. package/lib/commands/setup-modes.js +1 -8
  34. package/lib/commands/setup-prompts.js +2 -180
  35. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  36. package/lib/commands/verify-operations-steps.js +13 -1
  37. package/lib/commands/wizard-config-normalizer.js +7 -4
  38. package/lib/commands/wizard-core.js +5 -157
  39. package/lib/commands/wizard-file-saving.js +163 -0
  40. package/lib/core/env-platform-expand.js +5 -1
  41. package/lib/core/secrets-env-content.js +5 -1
  42. package/lib/core/secrets-env-write.js +10 -3
  43. package/lib/core/secrets-load.js +4 -2
  44. package/lib/datasource/binary-documents-validator.js +190 -0
  45. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  46. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  47. package/lib/datasource/capability/upsert-operations.js +138 -0
  48. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  49. package/lib/datasource/validate.js +12 -5
  50. package/lib/generator/index.js +3 -0
  51. package/lib/lifecycle/product-model.js +4 -3
  52. package/lib/lifecycle/report-display.js +3 -2
  53. package/lib/parameters/infra-parameter-catalog.js +1 -1
  54. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  55. package/lib/programmatic/builder-help-governance.js +1 -1
  56. package/lib/programmatic/builder-help.js +1 -1
  57. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  58. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  59. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  60. package/lib/role-assistant/test-runner-workhub.js +28 -19
  61. package/lib/schema/application-schema.json +14 -2
  62. package/lib/schema/external-datasource.schema.json +23 -3
  63. package/lib/schema/infra.parameter.yaml +388 -38
  64. package/lib/utils/compose-generate-docker-compose.js +7 -12
  65. package/lib/utils/datasource-binary-evidence.js +92 -0
  66. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  67. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  68. package/lib/utils/datasource-test-run-display.js +8 -2
  69. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  70. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  71. package/lib/utils/env-copy.js +11 -10
  72. package/lib/utils/external-system-system-test-tty.js +3 -2
  73. package/lib/utils/image-tags.js +2 -2
  74. package/lib/utils/platform-kv-ref.js +1 -1
  75. package/lib/utils/prepare-local-data-mount.js +58 -0
  76. package/lib/utils/secrets-helpers.js +0 -1
  77. package/lib/utils/system-secret-mapping.js +125 -0
  78. package/lib/utils/test-log-writer.js +2 -1
  79. package/lib/validation/external-manifest-validator.js +5 -0
  80. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  81. package/lib/validation/validate-external-file.js +5 -1
  82. package/package.json +4 -3
  83. package/templates/agent-kit/agent-kit.yaml +1 -1
  84. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  85. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  86. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  87. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  88. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  89. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  90. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  91. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  92. package/templates/agent-kit/skills/shared/status.md +3 -12
  93. package/templates/applications/builder-api/application.yaml +1 -1
  94. package/templates/applications/builder-api/env.template +5 -1
  95. package/templates/applications/dataplane/application.yaml +1 -1
  96. package/templates/applications/dataplane/env.template +10 -12
  97. package/templates/applications/keycloak/application.yaml +6 -1
  98. package/templates/applications/miso-controller/application.yaml +74 -2
  99. package/templates/applications/miso-controller/env.template +42 -3
  100. package/templates/applications/miso-controller/rbac.yaml +17 -0
  101. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  102. package/templates/marketplace/main.json +4 -4
  103. package/templates/python/docker-compose.hbs +1 -1
  104. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  105. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  106. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  107. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  108. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  109. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  110. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  111. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  112. /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 };
@@ -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
  }
@@ -470,7 +470,11 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
470
470
  }
471
471
 
472
472
  // Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
473
- const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
473
+ const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
474
+ appPath,
475
+ envOnly,
476
+ runEnvKey: opts.runEnvKey
477
+ });
474
478
 
475
479
  if (noWrite) {
476
480
  return null;
@@ -157,8 +157,11 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
157
157
  const environment = options.environment || 'docker';
158
158
  const secretsPath = options.secretsPath || null;
159
159
  const force = options.force === true;
160
+ const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
160
161
 
161
- let resolved = await secrets.generateEnvContent(appName, secretsPath, environment, force);
162
+ let resolved = genOpts
163
+ ? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
164
+ : await secrets.generateEnvContent(appName, secretsPath, environment, force);
162
165
  resolved = await injectRegistryTokens(resolved, secretsPath, appName);
163
166
  resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
164
167
 
@@ -185,7 +188,8 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
185
188
  * @async
186
189
  * @function resolveAndGetEnvMap
187
190
  * @param {string} appName - Application name
188
- * @param {Object} [options] - Options (same as resolveAndWriteEnvFile)
191
+ * @param {Object} [options] - Options (same as resolveAndWriteEnvFile); `runEnvKey` selects the
192
+ * manifest environment for {NAME} resolution
189
193
  * @returns {Promise<Object.<string, string>>} Map of variable name to value
190
194
  */
191
195
  async function resolveAndGetEnvMap(appName, options = {}) {
@@ -193,7 +197,10 @@ async function resolveAndGetEnvMap(appName, options = {}) {
193
197
  const environment = options.environment || 'docker';
194
198
  const secretsPath = options.secretsPath || null;
195
199
  const force = options.force === true;
196
- let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
200
+ const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
201
+ let content = genOpts
202
+ ? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
203
+ : await secrets.generateEnvContent(appName, secretsPath, environment, force);
197
204
  content = await injectRegistryTokens(content, secretsPath, appName);
198
205
  content = await injectBashPrefixedExportLines(content, secretsPath, appName);
199
206
  const map = parseEnvContentToMap(content);
@@ -167,9 +167,11 @@ async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, he
167
167
  }
168
168
 
169
169
  async function loadMergedConfigAndUserSecrets() {
170
- const userSecrets = loadPrimaryUserSecrets();
171
- const helpers = createMergeHelpers(userSecrets);
172
170
  const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
171
+ const userSecrets = await decryptSecretsObject(loadPrimaryUserSecrets(), {
172
+ defaultSourceLabel: userPath
173
+ });
174
+ const helpers = createMergeHelpers(userSecrets);
173
175
  /** @type {Record<string, string>} */
174
176
  const keySources = {};
175
177
  for (const k of Object.keys(userSecrets || {})) {
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Offline semantic checks for the binary/documents pairing (dataplane schema 2.9.13).
3
+ *
4
+ * The schema alone cannot express these: `output.mode: documents` and
5
+ * `fetch.responseType: binary` are individually valid anywhere, but only mean something
6
+ * when paired with a documentStorage entity and a binary-capable fetch source. The
7
+ * dataplane rejects the bad combinations at upload; these checks give the same answer
8
+ * offline, under the same error codes, so an author is not told "valid" by the CLI and
9
+ * "invalid" by the server.
10
+ *
11
+ * Codes mirror the dataplane validator and must not be renamed here.
12
+ *
13
+ * @fileoverview Semantic validation for binary responses and document output
14
+ * @author AI Fabrix Team
15
+ * @version 1.0.0
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ /** Sources whose response body the runtime can read as bytes. */
21
+ const BINARY_FETCH_SOURCES = new Set(['openapi', 'http']);
22
+
23
+ /** documentStorage.binaryOperationRef default, per type/document-storage.json. */
24
+ const DEFAULT_BINARY_OPERATION = 'get';
25
+
26
+ const CODES = Object.freeze({
27
+ entityType: 'cip_documents_output_entity_type',
28
+ storageDisabled: 'cip_documents_output_storage_disabled',
29
+ binarySource: 'cip_fetch_binary_source_invalid',
30
+ withoutBinaryFetch: 'cip_documents_output_without_binary_fetch'
31
+ });
32
+
33
+ /**
34
+ * @param {Object} datasource - Datasource definition
35
+ * @returns {Array<[string, Object]>} CIP operation entries, empty when not a CIP datasource
36
+ */
37
+ function cipOperations(datasource) {
38
+ const cip = datasource?.execution?.engine === 'cip' ? datasource.execution.cip : null;
39
+ const operations = cip && cip.operations;
40
+ if (!operations || typeof operations !== 'object') {
41
+ return [];
42
+ }
43
+ return Object.entries(operations).filter(([, operation]) => operation && typeof operation === 'object');
44
+ }
45
+
46
+ /**
47
+ * @param {Object} operation - CIP operation
48
+ * @returns {Array<Object>} Its steps
49
+ */
50
+ function stepsOf(operation) {
51
+ return Array.isArray(operation.steps) ? operation.steps : [];
52
+ }
53
+
54
+ /**
55
+ * A step declares a binary body. The schema also promotes a binary Content-Type when
56
+ * responseType is omitted, but that is a runtime decision we cannot make offline, so only
57
+ * the explicit declaration counts here.
58
+ * @param {Object} step - CIP step
59
+ * @returns {boolean} True when the step fetches a binary body
60
+ */
61
+ function isBinaryFetch(step) {
62
+ return Boolean(step && step.fetch && String(step.fetch.responseType || '').trim().toLowerCase() === 'binary');
63
+ }
64
+
65
+ /**
66
+ * @param {Object} step - CIP step
67
+ * @returns {boolean} True when the step finalizes as documents
68
+ */
69
+ function isDocumentsOutput(step) {
70
+ return Boolean(step && step.output && step.output.mode === 'documents');
71
+ }
72
+
73
+ /**
74
+ * `binary` is a body contract, so it only applies where the runtime holds the response
75
+ * bytes. A datasource, viewpoint or record source produces rows, never a file body.
76
+ * @param {string} operationName - Operation key
77
+ * @param {Array<Object>} steps - Operation steps
78
+ * @param {Array<Object>} errors - Collected errors
79
+ * @returns {void}
80
+ */
81
+ function checkBinaryFetchSources(operationName, steps, errors) {
82
+ steps.forEach((step, index) => {
83
+ if (!isBinaryFetch(step)) {
84
+ return;
85
+ }
86
+ const source = String(step.fetch.source || 'openapi').trim().toLowerCase();
87
+ if (BINARY_FETCH_SOURCES.has(source)) {
88
+ return;
89
+ }
90
+ errors.push({
91
+ code: CODES.binarySource,
92
+ message:
93
+ `operations.${operationName}.steps[${index}].fetch.responseType is "binary" but ` +
94
+ `fetch.source is "${source || 'unset'}"; binary bodies come from openapi or http sources only`
95
+ });
96
+ });
97
+ }
98
+
99
+ /**
100
+ * @param {Object} datasource - Datasource definition
101
+ * @param {string} operationName - Operation key that emits documents
102
+ * @param {number} stepIndex - Index of the output step
103
+ * @param {Array<Object>} errors - Collected errors
104
+ * @returns {void}
105
+ */
106
+ function checkDocumentsEntity(datasource, operationName, stepIndex, errors) {
107
+ const where = `operations.${operationName}.steps[${stepIndex}].output.mode is "documents"`;
108
+ if (!['documentstorage', 'document-storage'].includes(String(datasource.entityType || '').trim().toLowerCase().replace(/_/g, '-'))) {
109
+ errors.push({
110
+ code: CODES.entityType,
111
+ message: `${where} but entityType is "${datasource.entityType || 'unset'}"; documents output requires entityType documentStorage`
112
+ });
113
+ return;
114
+ }
115
+ const storage = datasource.documentStorage;
116
+ if (!storage || storage.enabled === false) {
117
+ errors.push({
118
+ code: CODES.storageDisabled,
119
+ message: `${where} but documentStorage is ${storage ? 'not enabled' : 'missing'}; set documentStorage.enabled true`
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The operation named by binaryOperationRef must actually retrieve bytes, otherwise the
126
+ * document pipeline has nothing to store. A missing reference and a reference to a
127
+ * JSON-only operation are the same defect from the author's point of view.
128
+ * @param {Object} datasource - Datasource definition
129
+ * @param {string} operationName - Operation key that emits documents
130
+ * @param {Array<Object>} warnings - Collected warnings
131
+ * @returns {void}
132
+ */
133
+ function checkBinaryOperationRef(datasource, operationName, warnings) {
134
+ const storage = datasource.documentStorage || {};
135
+ const ref = String(storage.binaryOperationRef || DEFAULT_BINARY_OPERATION);
136
+ const operations = new Map(cipOperations(datasource));
137
+ const referenced = operations.get(ref);
138
+ if (ref === operationName && referenced && stepsOf(referenced).some(isBinaryFetch)) {
139
+ return;
140
+ }
141
+ const reason = ref !== operationName && referenced
142
+ ? `binaryOperationRef "${ref}" does not name this operation "${operationName}"`
143
+ : referenced
144
+ ? `operation "${ref}" has no step with fetch.responseType "binary"`
145
+ : `operation "${ref}" does not exist in execution.cip.operations`;
146
+ warnings.push({
147
+ code: CODES.withoutBinaryFetch,
148
+ message:
149
+ `operations.${operationName} emits documents but ${reason}; ` +
150
+ 'set documentStorage.binaryOperationRef to the operation that fetches the file'
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Semantic checks for the binary/documents pairing.
156
+ * @param {Object} datasource - Datasource definition
157
+ * @returns {{ errors: Array<Object>, warnings: Array<Object> }} Findings with dataplane codes
158
+ */
159
+ function validateBinaryDocumentsContract(datasource) {
160
+ const errors = [];
161
+ const warnings = [];
162
+ if (!datasource || typeof datasource !== 'object') {
163
+ return { errors, warnings };
164
+ }
165
+ for (const [operationName, operation] of cipOperations(datasource)) {
166
+ if (operation.enabled === false) continue;
167
+ const steps = stepsOf(operation);
168
+ checkBinaryFetchSources(operationName, steps, errors);
169
+ steps.forEach((step, index) => {
170
+ if (!isDocumentsOutput(step)) {
171
+ return;
172
+ }
173
+ checkDocumentsEntity(datasource, operationName, index, errors);
174
+ const preceding = steps.slice(0, index + 1).some(candidate =>
175
+ isBinaryFetch(candidate) && BINARY_FETCH_SOURCES.has(String(candidate.fetch.source || 'openapi').trim().toLowerCase()));
176
+ if (!preceding) {
177
+ warnings.push({ code: CODES.withoutBinaryFetch,
178
+ message: `operations.${operationName} has no step with fetch.responseType "binary" preceding documents output` });
179
+ } else {
180
+ checkBinaryOperationRef(datasource, operationName, warnings);
181
+ }
182
+ });
183
+ }
184
+ return { errors, warnings };
185
+ }
186
+
187
+ module.exports = {
188
+ CODES,
189
+ validateBinaryDocumentsContract
190
+ };