@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.
- 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/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/run.js +6 -4
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -2
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +30 -10
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- package/lib/commands/auth-config.js +2 -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/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +5 -1
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +8 -9
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- package/lib/commands/up-integration-server.js +112 -0
- 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 +12 -5
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +17 -10
- package/lib/core/secrets-env-write.js +11 -4
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/capability-manifest.js +1 -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/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +3 -0
- package/lib/generator/split-variables.js +2 -2
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- 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/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- 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 +440 -56
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +9 -14
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- 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/declarative-url-ports.js +3 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -8
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- 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 +6 -4
- 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 +2 -2
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +116 -2
- package/templates/applications/miso-controller/env.template +62 -17
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +88 -10
- 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 };
|
|
@@ -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
|
+
};
|