@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.
- package/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +2 -2
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/secrets-env-content.js +5 -1
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/generator/index.js +3 -0
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +388 -38
- package/lib/utils/compose-generate-docker-compose.js +7 -12
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +4 -3
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +74 -2
- package/templates/applications/miso-controller/env.template +42 -3
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +4 -4
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /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
|
|
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 {
|
|
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, {
|
|
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 =
|
|
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
|
-
|
|
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);
|
package/lib/core/secrets-load.js
CHANGED
|
@@ -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
|
+
};
|