@burgan-tech/vnext-cli
Version:
CLI for creating and managing vNext domain projects with modular component sharing
1,327 lines (1,102 loc) β’ 51.8 kB
JavaScript
#!/usr/bin/env node
const fs = require('fs-extra');
const path = require('path');
const { program } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const RefResolver = require('./lib/ref-resolver');
const TemplateManager = require('./lib/template-manager');
const SchemaManager = require('./lib/schema-manager');
const packageJson = require('./package.json');
program
.name('vnext')
.description(packageJson.description)
.version(packageJson.version);
// Create command (existing functionality)
program
.command('create [project-name]')
.description('Create a new vNext domain project')
.option('-v, --version <version>', 'Template version to use (latest, v1.0.0, etc.)', 'latest')
.option('--list-versions', 'List available template versions and exit')
.option('--refresh-template', 'Force refresh template cache')
.action(async (projectName, options) => {
try {
// Initialize template manager
const templateManager = new TemplateManager();
// List versions and exit if requested
if (options.listVersions) {
console.log(chalk.blue('π Available template versions:'));
try {
const versions = await templateManager.listAvailableVersions();
if (versions.length === 0) {
console.log(chalk.gray(' No versions found'));
} else {
versions.forEach((version, index) => {
const marker = index === 0 ? chalk.green(' (latest)') : '';
console.log(` ${version}${marker}`);
});
}
} catch (error) {
console.log(chalk.red(`Error listing versions: ${error.message}`));
}
return;
}
let name = projectName;
// Get project name if not provided
if (!name) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'What is your project name?',
validate: (input) => {
if (input.trim() === '') {
return 'Project name is required';
}
return true;
}
}
]);
name = answers.projectName;
}
// Convert project name to domain name format (lowercase, hyphen-separated)
const domainName = convertToDomainFormat(name);
// Show domain conversion to user
if (domainName !== name) {
console.log(chalk.blue(`π Domain name: "${name}" β "${domainName}" (formatted for domain usage)`));
} else {
console.log(chalk.blue(`π Domain name: "${domainName}"`));
}
const targetPath = path.join(process.cwd(), name);
// Check if directory already exists
if (await fs.pathExists(targetPath)) {
console.log(chalk.red(`Error: Directory ${name} already exists`));
process.exit(1);
}
// Create project directory
await fs.ensureDir(targetPath);
// Force refresh template cache if requested
if (options.refreshTemplate) {
await templateManager.updateTemplate();
}
// Ensure template is available (download specific version if not cached)
console.log(chalk.blue(`π Using template version: ${options.version}`));
await templateManager.ensureTemplate(options.version);
// Copy template files with Git-based template
await templateManager.copyTemplate(targetPath, name, domainName);
console.log(chalk.green(`β
Successfully created ${name}`));
console.log(chalk.blue('π Project structure:'));
console.log(`
βββ ${domainName}/
β βββ Workflows/
β βββ Functions/
β βββ Views/
β βββ Extensions/
β βββ Schemas/
β βββ Tasks/
βββ .vscode/
βββ vnext.config.json
βββ .cursorrules
βββ README.md
βββ package.json
`);
console.log(chalk.blue(`\nπ Template Info:`));
console.log(`Template Version: ${templateManager.currentVersion}`);
console.log(`Repository: ${templateManager.options.templateRepo}`);
console.log(chalk.yellow('\nπ Next steps:'));
console.log(` cd ${name}`);
console.log(' npm install');
console.log(' vnext validate --resolve-refs');
console.log(' code .');
} catch (error) {
console.error(chalk.red('Error creating project:'), error.message);
process.exit(1);
}
});
// Validate command with reference resolution
program
.command('validate [file]')
.description('Validate domain components and resolve references (optionally specify a single file)')
.option('--resolve-refs', 'Resolve and validate all ref references')
.option('--strict', 'Enable strict validation mode')
.action(async (file, options) => {
try {
console.log(chalk.blue('π Validating domain components...'));
// Load vnext.config.json
const configPath = path.join(process.cwd(), 'vnext.config.json');
if (!(await fs.pathExists(configPath))) {
console.log(chalk.red('β vnext.config.json not found. Run this command in an vNext domain project.'));
process.exit(1);
}
const config = await fs.readJSON(configPath);
// Initialize schema manager and get schema path for runtime version
const schemaManager = new SchemaManager();
let schemaPath;
try {
schemaPath = await schemaManager.ensureSchemasForConfig(configPath);
console.log(chalk.green(`π Using schemas from runtime version: ${schemaManager.currentVersion}`));
} catch (error) {
console.log(chalk.red(`β Failed to load runtime schemas: ${error.message}`));
console.log(chalk.red(`β Schema validation requires NPM access to download schema package.`));
console.log(chalk.yellow(`π‘ Possible solutions:`));
console.log(chalk.yellow(` - Check your internet connection`));
console.log(chalk.yellow(` - Verify NPM registry access: ${schemaManager.options.npmRegistry}`));
console.log(chalk.yellow(` - Check schema package exists: ${schemaManager.options.schemaPackageName}`));
console.log(chalk.yellow(` - Ensure write permissions to cache directory: ${schemaManager.schemaCacheDir}`));
process.exit(1);
}
// Create resolver with config-based options and dynamic schema path
const resolver = new RefResolver({
strictMode: options.strict || config.referenceResolution?.strictMode,
validateReferenceConsistency: config.referenceResolution?.validateReferenceConsistency !== false,
validateSchemas: config.referenceResolution?.validateSchemas !== false,
schemaPath: schemaPath
});
// Load additional config
await resolver.loadValidationConfig(configPath);
let totalFiles = 0;
let validFiles = 0;
let totalRefs = 0;
let validRefs = 0;
let schemaValidationPassed = 0;
let schemaValidationFailed = 0;
let jsonFiles = [];
if (file) {
// Single file validation
const filePath = path.resolve(process.cwd(), file);
if (!(await fs.pathExists(filePath))) {
console.log(chalk.red(`β File not found: ${file}`));
process.exit(1);
}
if (path.extname(filePath) !== '.json') {
console.log(chalk.red(`β Only JSON files are supported for validation`));
process.exit(1);
}
jsonFiles = [filePath];
console.log(chalk.blue(`π Validating single file: ${file}`));
} else {
// Scan all JSON files
const scanPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
jsonFiles = await findJsonFiles(scanPath);
console.log(chalk.blue(`π Validating all components in ${config.domain}...`));
}
for (const filePath of jsonFiles) {
totalFiles++;
try {
const content = await fs.readJSON(filePath);
console.log(chalk.gray(`π Validating: ${path.relative(process.cwd(), filePath)}`));
// Always perform schema validation
let fileSchemaValid = true;
try {
await resolver.validateComponentSchema(content, path.relative(process.cwd(), filePath));
console.log(chalk.green(` β
Schema validation passed`));
schemaValidationPassed++;
} catch (error) {
fileSchemaValid = false;
console.log(chalk.red(` β Schema validation failed: ${error.message}`));
schemaValidationFailed++;
}
// Perform reference resolution if requested
if (options.resolveRefs) {
const validation = await resolver.validateAllReferences(content, config.domain);
totalRefs += validation.resolvedRefs.length;
validRefs += validation.resolvedRefs.filter(r => r.status === 'success').length;
if (validation.valid && fileSchemaValid) {
validFiles++;
console.log(chalk.green(` β
Complete validation passed (${validation.validationDetails.successful}/${validation.validationDetails.total} refs resolved)`));
// Show detailed validation info in verbose mode
if (validation.validationDetails.total > 0) {
validation.resolvedRefs.forEach(ref => {
if (ref.status === 'success' && ref.details) {
console.log(chalk.gray(` π ${ref.details.componentType}: ${ref.details.key}@${ref.details.version} (${ref.details.domain})`));
}
});
}
} else {
if (!validation.valid) {
console.log(chalk.red(` β Reference validation failed (${validation.validationDetails.failed}/${validation.validationDetails.total} refs failed):`));
validation.errors.forEach(error => {
console.log(chalk.red(` - ${error.ref}:`));
console.log(chalk.red(` ${error.error}`));
});
}
}
} else {
if (fileSchemaValid) {
validFiles++;
}
}
} catch (error) {
console.log(chalk.red(` β Error: ${error.message}`));
}
}
console.log(chalk.blue('\nπ Validation Summary:'));
console.log(`Files: ${validFiles}/${totalFiles} valid`);
console.log(`Schema Validation: ${schemaValidationPassed}/${schemaValidationPassed + schemaValidationFailed} passed`);
if (options.resolveRefs) {
console.log(`References: ${validRefs}/${totalRefs} resolved`);
}
const allValidationsPassed = validFiles === totalFiles &&
schemaValidationFailed === 0 &&
(options.resolveRefs ? validRefs === totalRefs : true);
if (allValidationsPassed) {
console.log(chalk.green('π All validations passed!'));
} else {
console.log(chalk.yellow('β οΈ Some validations failed. Check the output above.'));
if (schemaValidationFailed > 0) {
console.log(chalk.red(`β ${schemaValidationFailed} files failed schema validation`));
}
if (options.resolveRefs && validRefs < totalRefs) {
console.log(chalk.red(`β ${totalRefs - validRefs} references failed to resolve`));
}
process.exit(1);
}
} catch (error) {
console.error(chalk.red('Validation error:'), error.message);
process.exit(1);
}
});
// Build command (extract public + validate + resolve refs)
program
.command('build')
.description('Build domain package with validation and reference resolution')
.option('-o, --output <dir>', 'Output directory', 'dist')
.option('-t, --type <type>', 'Build type: reference (exports only) or runtime (complete)', 'reference')
.option('--skip-validation', 'Skip schema validation during build')
.action(async (options) => {
try {
// Validate build type
if (!['reference', 'runtime'].includes(options.type)) {
console.log(chalk.red('β Invalid build type. Use "reference" or "runtime".'));
process.exit(1);
}
console.log(chalk.blue(`ποΈ Building ${options.type} package...`));
const configPath = path.join(process.cwd(), 'vnext.config.json');
if (!(await fs.pathExists(configPath))) {
console.log(chalk.red('β vnext.config.json not found.'));
process.exit(1);
}
const config = await fs.readJSON(configPath);
const outputDir = path.join(process.cwd(), options.output);
// Step 1: Validation (unless skipped)
if (!options.skipValidation) {
console.log(chalk.blue('\nπ Step 1: Validating components...'));
// Initialize schema manager and get schema path for runtime version
const schemaManager = new SchemaManager();
let schemaPath;
try {
schemaPath = await schemaManager.ensureSchemasForConfig(configPath);
console.log(chalk.green(`π Using schemas from runtime version: ${schemaManager.currentVersion}`));
} catch (error) {
console.log(chalk.red(`β Failed to load runtime schemas: ${error.message}`));
console.log(chalk.red(`β Build process requires NPM access to download schema package.`));
console.log(chalk.yellow(`π‘ Possible solutions:`));
console.log(chalk.yellow(` - Check your internet connection`));
console.log(chalk.yellow(` - Verify NPM registry access: ${schemaManager.options.npmRegistry}`));
console.log(chalk.yellow(` - Check schema package exists: ${schemaManager.options.schemaPackageName}`));
console.log(chalk.yellow(` - Ensure write permissions to cache directory: ${schemaManager.schemaCacheDir}`));
process.exit(1);
}
const resolver = new RefResolver({
strictMode: config.referenceResolution?.strictMode,
validateReferenceConsistency: config.referenceResolution?.validateReferenceConsistency !== false,
validateSchemas: config.referenceResolution?.validateSchemas !== false,
schemaPath: schemaPath
});
await resolver.loadValidationConfig(configPath);
let totalFiles = 0;
let validFiles = 0;
const scanPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
const jsonFiles = await findJsonFiles(scanPath);
for (const filePath of jsonFiles) {
totalFiles++;
try {
const content = await fs.readJSON(filePath);
const validation = await resolver.validateAllReferences(content, config.domain);
if (validation.valid) {
validFiles++;
console.log(chalk.green(` β
${path.relative(process.cwd(), filePath)}`));
} else {
console.log(chalk.red(` β ${path.relative(process.cwd(), filePath)} - Validation failed`));
validation.errors.forEach(error => {
console.log(chalk.red(` - ${error.ref}: ${error.error}`));
});
}
} catch (error) {
console.log(chalk.red(` β ${path.relative(process.cwd(), filePath)} - ${error.message}`));
}
}
if (validFiles !== totalFiles) {
console.log(chalk.red(`\nβ Build failed: ${totalFiles - validFiles} components failed validation`));
process.exit(1);
}
console.log(chalk.green(`β
All ${totalFiles} components validated successfully`));
}
// Step 2: Clean output directory
console.log(chalk.blue('\nπ¦ Step 2: Preparing build directory...'));
await fs.remove(outputDir);
await fs.ensureDir(outputDir);
// Step 3: Prepare and copy configuration files with build type modifications
await fs.writeJSON(path.join(outputDir, 'vnext.config.json'), config, { spaces: 2 });
const packagePath = path.join(process.cwd(), 'package.json');
if (await fs.pathExists(packagePath)) {
const packageJson = await fs.readJSON(packagePath);
const originalPackageName = packageJson.name;
// Modify package name based on build type
if (options.type === 'reference') {
packageJson.name = `${originalPackageName}-reference`;
packageJson.description = `${packageJson.description || ''} (Reference Package for Cross-Domain Usage)`.trim();
} else if (options.type === 'runtime') {
packageJson.name = `${originalPackageName}-runtime`;
packageJson.description = `${packageJson.description || ''} (Runtime Package for Engine Deployment)`.trim();
}
// Add build type metadata
packageJson.vnext = {
...packageJson.vnext,
buildType: options.type,
buildTimestamp: new Date().toISOString(),
originalPackage: originalPackageName
};
await fs.writeJSON(path.join(outputDir, 'package.json'), packageJson, { spaces: 2 });
console.log(chalk.green(` β
Created ${options.type} package.json: ${packageJson.name}`));
}
// Step 4: Process and copy components based on build type
let copiedFiles = 0;
if (options.type === 'reference') {
console.log(chalk.blue('\nπ Step 3: Processing exported components with reference resolution...'));
// Initialize schema manager for reference resolution
const schemaManager = new SchemaManager();
let schemaPath;
try {
schemaPath = await schemaManager.ensureSchemasForConfig(configPath);
} catch (error) {
console.log(chalk.red(`β Failed to load runtime schemas: ${error.message}`));
console.log(chalk.red(`β Reference build requires NPM access to download schema package.`));
process.exit(1);
}
const resolver = new RefResolver({
schemaPath: schemaPath
});
await resolver.loadValidationConfig(configPath);
if (config.exports) {
const domainPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
for (const [category, files] of Object.entries(config.exports)) {
if (Array.isArray(files)) {
const categoryPath = config.paths[category] || category;
for (const filename of files) {
const sourcePath = path.join(domainPath, categoryPath, filename);
const targetPath = path.join(outputDir, config.domain, categoryPath, filename);
if (await fs.pathExists(sourcePath)) {
await fs.ensureDir(path.dirname(targetPath));
// Read, resolve references, and write processed file
const originalContent = await fs.readJSON(sourcePath);
const processedContent = await resolveReferencesToPayload(originalContent, resolver, config.domain);
await fs.writeJSON(targetPath, processedContent, { spaces: 2 });
console.log(chalk.green(` β
Processed (reference): ${categoryPath}/${filename}`));
copiedFiles++;
} else {
console.log(chalk.yellow(` β οΈ Not found: ${categoryPath}/${filename}`));
}
}
}
}
}
} else if (options.type === 'runtime') {
console.log(chalk.blue('\nπ Step 3: Processing complete domain structure with reference resolution...'));
// Initialize schema manager for runtime resolution
const schemaManager = new SchemaManager();
let schemaPath;
try {
schemaPath = await schemaManager.ensureSchemasForConfig(configPath);
} catch (error) {
console.log(chalk.red(`β Failed to load runtime schemas: ${error.message}`));
console.log(chalk.red(`β Runtime build requires NPM access to download schema package.`));
process.exit(1);
}
const resolver = new RefResolver({
schemaPath: schemaPath
});
await resolver.loadValidationConfig(configPath);
const domainPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
const targetDomainPath = path.join(outputDir, config.domain);
if (await fs.pathExists(domainPath)) {
console.log(chalk.gray(`π Source domain path: ${domainPath}`));
console.log(chalk.gray(`π Target domain path: ${targetDomainPath}`));
// Get all files recursively
const allFiles = await getAllFiles(domainPath);
console.log(chalk.gray(`π Found ${allFiles.length} files to process`));
for (const filePath of allFiles) {
const targetPath = path.join(targetDomainPath, path.relative(domainPath, filePath));
await fs.ensureDir(path.dirname(targetPath));
const relativePath = path.relative(domainPath, filePath);
// Process JSON files with reference resolution
if (path.extname(filePath) === '.json') {
try {
console.log(chalk.gray(`π Processing JSON: ${relativePath}`));
const originalContent = await fs.readJSON(filePath);
const processedContent = await resolveReferencesToPayload(originalContent, resolver, config.domain);
await fs.writeJSON(targetPath, processedContent, { spaces: 2 });
console.log(chalk.green(`β
Processed: ${relativePath}`));
copiedFiles++;
} catch (error) {
console.log(chalk.yellow(`β οΈ JSON processing failed for ${relativePath}, copying as-is: ${error.message}`));
// If JSON processing fails, copy as-is
await fs.copy(filePath, targetPath);
if (path.extname(filePath) === '.json') copiedFiles++;
}
} else {
// Copy non-JSON files as-is
console.log(chalk.gray(`π Copying non-JSON: ${relativePath}`));
await fs.copy(filePath, targetPath);
}
}
console.log(chalk.green(` β
Processed complete domain structure with reference resolution`));
console.log(chalk.gray(` - ${copiedFiles} JSON component files processed`));
console.log(chalk.gray(` - All supporting files and folders copied`));
} else {
console.log(chalk.red(` β Domain directory not found: ${domainPath}`));
}
}
console.log(chalk.blue(`\nπ Build Summary:`));
console.log(`Build type: ${options.type}`);
console.log(`Files processed: ${copiedFiles}`);
console.log(`Output directory: ${outputDir}`);
if (options.type === 'reference') {
console.log(chalk.gray('Package contents: Exported components only (for cross-domain usage)'));
} else {
console.log(chalk.gray('Package contents: Complete domain structure (for runtime deployment)'));
}
console.log(chalk.green(`π ${options.type.charAt(0).toUpperCase() + options.type.slice(1)} package built successfully!`));
} catch (error) {
console.error(chalk.red('Build error:'), error.message);
process.exit(1);
}
});
// Publish command
program
.command('publish')
.description('Publish domain package to NPM registry')
.option('-t, --type <type>', 'Publish type: reference (exports only) or runtime (complete)', 'reference')
.option('--dry-run', 'Show what would be published without actually publishing')
.option('--registry <url>', 'NPM registry URL')
.action(async (options) => {
try {
// Validate publish type
if (!['reference', 'runtime'].includes(options.type)) {
console.log(chalk.red('β Invalid publish type. Use "reference" or "runtime".'));
process.exit(1);
}
console.log(chalk.blue(`π Publishing ${options.type} package...`));
// First build the package (this includes validation and reference resolution)
console.log(chalk.blue(`Building ${options.type} package for publication...`));
await buildPackage('dist', options.type);
const distPath = path.join(process.cwd(), 'dist');
if (options.dryRun) {
console.log(chalk.yellow('π Dry run mode - showing what would be published:'));
const files = await getAllFiles(distPath);
files.forEach(file => {
console.log(` ${path.relative(distPath, file)}`);
});
return;
}
// Run npm publish in dist directory
const { execSync } = require('child_process');
const publishCmd = options.registry
? `npm publish --registry ${options.registry}`
: 'npm publish';
try {
execSync(publishCmd, {
cwd: distPath,
stdio: 'inherit'
});
console.log(chalk.green('π Package published successfully!'));
} catch (error) {
console.log(chalk.red('β Publish failed:'), error.message);
process.exit(1);
}
} catch (error) {
console.error(chalk.red('Publish error:'), error.message);
process.exit(1);
}
});
// List exports command
program
.command('list-exports [package-name]')
.description('List exported components from a domain package')
.action(async (packageName) => {
try {
const resolver = new RefResolver();
if (packageName) {
// List exports from external package
console.log(chalk.blue(`π Listing exports from ${packageName}...`));
const exports = await resolver.listPackageExports(packageName);
console.log(chalk.green(`\nπ¦ ${exports.packageName}`));
console.log(`Domain: ${exports.domain}`);
console.log(`Version: ${exports.version}`);
console.log(`Description: ${exports.description}`);
if (exports.exports && Object.keys(exports.exports).length > 0) {
console.log(chalk.blue('\nπ Exported Components:'));
for (const [category, files] of Object.entries(exports.exports)) {
if (Array.isArray(files) && files.length > 0) {
console.log(chalk.yellow(`\n ${category.toUpperCase()}:`));
files.forEach(file => {
console.log(` - ${file}`);
});
}
}
} else {
console.log(chalk.gray('\n No exported components found.'));
}
} else {
// List exports from current project
console.log(chalk.blue('π Listing exports from current project...'));
const configPath = path.join(process.cwd(), 'vnext.config.json');
if (!(await fs.pathExists(configPath))) {
console.log(chalk.red('β vnext.config.json not found.'));
process.exit(1);
}
const config = await fs.readJSON(configPath);
console.log(chalk.green(`\nπ¦ ${config.domain} (current project)`));
console.log(`Version: ${config.version}`);
console.log(`Description: ${config.description}`);
if (config.exports) {
console.log(chalk.blue('\nπ Exported Components:'));
for (const [category, files] of Object.entries(config.exports)) {
if (Array.isArray(files) && files.length > 0) {
console.log(chalk.yellow(`\n ${category.toUpperCase()}:`));
files.forEach(file => {
console.log(` - ${file}`);
});
}
}
} else {
console.log(chalk.gray('\n No exported components configured.'));
}
}
} catch (error) {
console.error(chalk.red('Error listing exports:'), error.message);
process.exit(1);
}
});
// Template management commands
program
.command('template-info')
.description('Show template information and status')
.option('-v, --version <version>', 'Template version to check (default: latest)', 'latest')
.action(async (options) => {
try {
const templateManager = new TemplateManager();
console.log(chalk.blue('π Template Information:'));
const info = await templateManager.getTemplateInfo(options.version);
console.log(`Repository: ${info.repository}`);
console.log(`Current Version: ${info.version}`);
console.log(`Cache Path: ${info.path}`);
console.log(`Cached: ${info.exists ? chalk.green('Yes') : chalk.red('No')}`);
if (info.availableVersions.length > 0) {
console.log(chalk.blue('\nπ·οΈ Available Versions:'));
info.availableVersions.forEach((version, index) => {
const marker = index === 0 ? chalk.green(' (latest)') : '';
const current = version === info.version ? chalk.yellow(' (current)') : '';
console.log(` ${version}${marker}${current}`);
});
}
if (info.config) {
console.log(chalk.blue('\nπ¦ Template Config:'));
console.log(`Version: ${info.config.version}`);
console.log(`Description: ${info.config.description}`);
console.log(`Domain: ${info.config.domain}`);
}
if (info.packageJson) {
console.log(chalk.blue('\nπ Package Info:'));
console.log(`Name: ${info.packageJson.name}`);
console.log(`Version: ${info.packageJson.version}`);
}
} catch (error) {
console.error(chalk.red('Error getting template info:'), error.message);
process.exit(1);
}
});
program
.command('template-update')
.description('Update template cache (clear all cached versions)')
.action(async () => {
try {
const templateManager = new TemplateManager();
console.log(chalk.blue('π Updating template cache...'));
await templateManager.updateTemplate();
console.log(chalk.green('β
Template cache updated successfully'));
} catch (error) {
console.error(chalk.red('Error updating template:'), error.message);
process.exit(1);
}
});
program
.command('template-versions')
.description('List all available template versions')
.action(async () => {
try {
const templateManager = new TemplateManager();
console.log(chalk.blue('π Available template versions:'));
const versions = await templateManager.listAvailableVersions();
if (versions.length === 0) {
console.log(chalk.gray(' No versions found'));
} else {
versions.forEach((version, index) => {
const marker = index === 0 ? chalk.green(' (latest)') : '';
console.log(` ${version}${marker}`);
});
}
} catch (error) {
console.error(chalk.red('Error listing versions:'), error.message);
process.exit(1);
}
});
program
.command('template-clear')
.description('Clear template cache')
.action(async () => {
try {
const templateManager = new TemplateManager();
await templateManager.clearCache();
} catch (error) {
console.error(chalk.red('Error clearing template cache:'), error.message);
process.exit(1);
}
});
// Schema management commands
program
.command('schema-info')
.description('Show schema package information and status')
.option('-v, --version <version>', 'Schema version to check (default: latest)', 'latest')
.action(async (options) => {
try {
const schemaManager = new SchemaManager();
console.log(chalk.blue('π Schema Package Information:'));
const info = await schemaManager.getSchemaInfo(options.version);
console.log(`Package: ${info.packageName}`);
console.log(`Current Version: ${info.version}`);
console.log(`Cache Path: ${info.path}`);
console.log(`Cached: ${info.exists ? chalk.green('Yes') : chalk.red('No')}`);
if (info.availableVersions.length > 0) {
console.log(chalk.blue('\nπ·οΈ Available Versions:'));
info.availableVersions.slice(0, 10).forEach((version, index) => {
const marker = index === 0 ? chalk.green(' (latest)') : '';
const current = version === info.version ? chalk.yellow(' (current)') : '';
console.log(` ${version}${marker}${current}`);
});
if (info.availableVersions.length > 10) {
console.log(chalk.gray(` ... and ${info.availableVersions.length - 10} more versions`));
}
}
if (info.packageJson) {
console.log(chalk.blue('\nπ Package Info:'));
console.log(`Name: ${info.packageJson.name}`);
console.log(`Version: ${info.packageJson.version}`);
console.log(`Description: ${info.packageJson.description || 'No description'}`);
}
if (info.schemaFiles && info.schemaFiles.length > 0) {
console.log(chalk.blue('\nπ Schema Files:'));
info.schemaFiles.forEach(file => {
console.log(` ${file}`);
});
}
} catch (error) {
console.error(chalk.red('Error getting schema info:'), error.message);
process.exit(1);
}
});
program
.command('schema-update')
.description('Update schema cache (clear all cached versions)')
.action(async () => {
try {
const schemaManager = new SchemaManager();
console.log(chalk.blue('π Updating schema cache...'));
await schemaManager.updateSchemas();
console.log(chalk.green('β
Schema cache updated successfully'));
} catch (error) {
console.error(chalk.red('Error updating schema cache:'), error.message);
process.exit(1);
}
});
program
.command('schema-versions')
.description('List all available schema package versions')
.action(async () => {
try {
const schemaManager = new SchemaManager();
console.log(chalk.blue('π Available schema package versions:'));
const versions = await schemaManager.listAvailableVersions();
if (versions.length === 0) {
console.log(chalk.gray(' No versions found'));
} else {
versions.slice(0, 20).forEach((version, index) => {
const marker = index === 0 ? chalk.green(' (latest)') : '';
console.log(` ${version}${marker}`);
});
if (versions.length > 20) {
console.log(chalk.gray(` ... and ${versions.length - 20} more versions`));
}
}
} catch (error) {
console.error(chalk.red('Error listing schema versions:'), error.message);
process.exit(1);
}
});
program
.command('schema-clear')
.description('Clear schema cache')
.action(async () => {
try {
const schemaManager = new SchemaManager();
await schemaManager.clearCache();
} catch (error) {
console.error(chalk.red('Error clearing schema cache:'), error.message);
process.exit(1);
}
});
// Visualize boundaries command
program
.command('visualize-boundaries [file]')
.description('Generate domain boundary visualization (optionally specify a single file)')
.option('-f, --format <format>', 'Output format (json, mermaid, dot)', 'json')
.option('-o, --output <file>', 'Output file path')
.action(async (file, options) => {
try {
console.log(chalk.blue('πΊοΈ Generating domain boundary visualization...'));
const configPath = path.join(process.cwd(), 'vnext.config.json');
if (!(await fs.pathExists(configPath))) {
console.log(chalk.red('β vnext.config.json not found.'));
process.exit(1);
}
const config = await fs.readJSON(configPath);
const resolver = new RefResolver();
let jsonFiles = [];
let isGlobal = !file;
if (file) {
// Single file visualization
const filePath = path.resolve(process.cwd(), file);
if (!(await fs.pathExists(filePath))) {
console.log(chalk.red(`β File not found: ${file}`));
process.exit(1);
}
if (path.extname(filePath) !== '.json') {
console.log(chalk.red(`β Only JSON files are supported for visualization`));
process.exit(1);
}
jsonFiles = [filePath];
console.log(chalk.blue(`πΊοΈ Visualizing boundaries for single file: ${file}`));
} else {
// Scan all JSON files for references
const scanPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
jsonFiles = await findJsonFiles(scanPath);
console.log(chalk.blue(`πΊοΈ Visualizing boundaries for all components in ${config.domain}...`));
}
const boundaries = {
domain: config.domain,
version: config.version,
dependencies: [],
components: [],
references: []
};
for (const filePath of jsonFiles) {
try {
const content = await fs.readJSON(filePath);
const scanPath = isGlobal ?
path.join(process.cwd(), config.paths?.componentsRoot || config.domain) :
path.dirname(filePath);
const relativePath = path.relative(scanPath, filePath);
boundaries.components.push({
path: relativePath,
type: resolver.detectComponentType(relativePath),
key: content.key || path.basename(filePath, '.json')
});
const validation = await resolver.validateAllReferences(content, config.domain);
validation.resolvedRefs.forEach(ref => {
boundaries.references.push({
from: relativePath,
to: ref.ref,
status: ref.status,
resolvedFrom: ref.resolved
});
// Extract external dependencies
if (ref.ref.startsWith('@') && ref.status === 'success') {
const packageMatch = ref.ref.match(/^(@[^/]+\/[^/]+)/);
if (packageMatch && !boundaries.dependencies.includes(packageMatch[1])) {
boundaries.dependencies.push(packageMatch[1]);
}
}
});
} catch (error) {
console.log(chalk.gray(` Skipping ${path.relative(process.cwd(), filePath)}: ${error.message}`));
}
}
// Generate output based on format
let output = '';
switch (options.format) {
case 'json':
output = JSON.stringify(boundaries, null, 2);
break;
case 'mermaid':
output = generateMermaidDiagram(boundaries);
break;
case 'dot':
output = generateDotDiagram(boundaries);
break;
default:
console.log(chalk.red(`β Unknown format: ${options.format}`));
process.exit(1);
}
if (options.output) {
await fs.writeFile(options.output, output);
console.log(chalk.green(`β
Visualization saved to ${options.output}`));
} else {
// Auto-generate output file name and path
let outputFile = '';
if (isGlobal) {
// Global mode: use domain name
outputFile = `${config.domain}.${options.format}`;
} else {
// Single file mode: use file name + "-visualize" suffix
const inputFile = path.resolve(process.cwd(), file);
const inputDir = path.dirname(inputFile);
const inputName = path.basename(inputFile, '.json');
outputFile = path.join(inputDir, `${inputName}-visualize.${options.format}`);
}
await fs.writeFile(outputFile, output);
console.log(chalk.green(`β
Visualization saved to ${outputFile}`));
}
console.log(chalk.blue(`\nπ Boundary Analysis:`));
if (isGlobal) {
console.log(`Components: ${boundaries.components.length}`);
console.log(`References: ${boundaries.references.length}`);
console.log(`External Dependencies: ${boundaries.dependencies.length}`);
} else {
console.log(`Component: ${boundaries.components[0]?.key || 'Unknown'}`);
console.log(`Type: ${boundaries.components[0]?.type || 'Unknown'}`);
console.log(`References: ${boundaries.references.length}`);
console.log(`External Dependencies: ${boundaries.dependencies.length}`);
}
} catch (error) {
console.error(chalk.red('Visualization error:'), error.message);
process.exit(1);
}
});
// Helper functions (existing and new)
/**
* Convert project name to domain format
* @param {string} projectName - Original project name
* @returns {string} Domain-formatted name
*/
function convertToDomainFormat(projectName) {
return projectName
.trim()
.toLowerCase()
// Replace spaces and underscores with hyphens
.replace(/[\s_]+/g, '-')
// Remove non-alphanumeric characters except hyphens
.replace(/[^a-z0-9-]/g, '')
// Remove multiple consecutive hyphens
.replace(/-+/g, '-')
// Remove leading/trailing hyphens
.replace(/^-+|-+$/g, '');
}
async function findJsonFiles(dirPath) {
const files = [];
if (!(await fs.pathExists(dirPath))) {
return files;
}
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
const subFiles = await findJsonFiles(itemPath);
files.push(...subFiles);
} else if (path.extname(item) === '.json') {
files.push(itemPath);
}
}
return files;
}
async function getAllFiles(dirPath) {
const files = [];
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
const subFiles = await getAllFiles(itemPath);
files.push(...subFiles);
} else {
files.push(itemPath);
}
}
return files;
}
function generateMermaidDiagram(boundaries) {
let diagram = 'graph TD\n';
// Add domain node
diagram += ` ${boundaries.domain}[${boundaries.domain}]\n`;
// Add dependency nodes
boundaries.dependencies.forEach(dep => {
const nodeId = dep.replace(/[@/-]/g, '_');
diagram += ` ${nodeId}[${dep}]\n`;
diagram += ` ${nodeId} --> ${boundaries.domain}\n`;
});
return diagram;
}
function generateDotDiagram(boundaries) {
let diagram = 'digraph DomainBoundaries {\n';
diagram += ' rankdir=LR;\n';
diagram += ' node [shape=box];\n';
// Add domain node
diagram += ` "${boundaries.domain}" [style=filled, fillcolor=lightblue];\n`;
// Add dependencies
boundaries.dependencies.forEach(dep => {
diagram += ` "${dep}" [style=filled, fillcolor=lightgray];\n`;
diagram += ` "${dep}" -> "${boundaries.domain}";\n`;
});
diagram += '}\n';
return diagram;
}
/**
* Resolves all ref references in a JSON object to their payload equivalents
* @param {Object} obj - JSON object to process
* @param {RefResolver} resolver - Reference resolver instance
* @param {string} currentDomain - Current domain context
* @returns {Promise<Object>} Object with references resolved to payloads
*/
async function resolveReferencesToPayload(obj, resolver, currentDomain) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
const resolvedArray = [];
for (const item of obj) {
resolvedArray.push(await resolveReferencesToPayload(item, resolver, currentDomain));
}
return resolvedArray;
}
// Check if this object has a ref property
if (obj.ref && typeof obj.ref === 'string') {
try {
console.log(chalk.gray(` π Resolving ref: ${obj.ref}`));
// Resolve the reference to get the full component
const resolvedComponent = await resolver.resolveRef(obj.ref, currentDomain);
// Extract the payload (key, version, domain, flow)
const payload = {
key: resolvedComponent.key,
version: resolvedComponent.version,
domain: resolvedComponent.domain,
flow: resolvedComponent.flow
};
console.log(chalk.green(` β
Resolved to payload: ${payload.key}@${payload.version} (${payload.domain})`));
return payload;
} catch (error) {
console.log(chalk.red(` β Failed to resolve ref: ${obj.ref} - ${error.message}`));
// Return original ref if resolution fails
return obj;
}
}
// Recursively process all properties
const resolvedObj = {};
for (const [key, value] of Object.entries(obj)) {
resolvedObj[key] = await resolveReferencesToPayload(value, resolver, currentDomain);
}
return resolvedObj;
}
/**
* Builds package with validation and reference resolution
* @param {string} outputDir - Output directory
* @param {string} type - Build type (reference or runtime)
* @returns {Promise<void>}
*/
async function buildPackage(outputDir, type = 'reference') {
const configPath = path.join(process.cwd(), 'vnext.config.json');
if (!(await fs.pathExists(configPath))) {
throw new Error('vnext.config.json not found');
}
const config = await fs.readJSON(configPath);
// Run validation with runtime-specific schemas
const schemaManager = new SchemaManager();
let schemaPath;
try {
schemaPath = await schemaManager.ensureSchemasForConfig(configPath);
} catch (error) {
throw new Error(`Schema validation failed: ${error.message}. NPM access required to download schema package.`);
}
const resolver = new RefResolver({
strictMode: config.referenceResolution?.strictMode,
validateReferenceConsistency: config.referenceResolution?.validateReferenceConsistency !== false,
validateSchemas: config.referenceResolution?.validateSchemas !== false,
schemaPath: schemaPath
});
await resolver.loadValidationConfig(configPath);
const scanPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
const jsonFiles = await findJsonFiles(scanPath);
// Validate all files
for (const filePath of jsonFiles) {
const content = await fs.readJSON(filePath);
const validation = await resolver.validateAllReferences(content, config.domain);
if (!validation.valid) {
throw new Error(`Validation failed for ${path.relative(process.cwd(), filePath)}`);
}
}
// Clean and prepare output
const fullOutputDir = path.join(process.cwd(), outputDir);
await fs.remove(fullOutputDir);
await fs.ensureDir(fullOutputDir);
// Copy configuration
await fs.writeJSON(path.join(fullOutputDir, 'vnext.config.json'), config, { spaces: 2 });
const packagePath = path.join(process.cwd(), 'package.json');
if (await fs.pathExists(packagePath)) {
const packageJson = await fs.readJSON(packagePath);
const originalPackageName = packageJson.name;
// Modify package name based on build type
if (type === 'reference') {
packageJson.name = `${originalPackageName}-reference`;
packageJson.description = `${packageJson.description || ''} (Reference Package for Cross-Domain Usage)`.trim();
} else if (type === 'runtime') {
packageJson.name = `${originalPackageName}-runtime`;
packageJson.description = `${packageJson.description || ''} (Runtime Package for Engine Deployment)`.trim();
}
// Add build type metadata
packageJson.vnext = {
...packageJson.vnext,
buildType: type,
buildTimestamp: new Date().toISOString(),
originalPackage: originalPackageName
};
await fs.writeJSON(path.join(fullOutputDir, 'package.json'), packageJson, { spaces: 2 });
}
// Process components based on type
const domainPath = path.join(process.cwd(), config.paths?.componentsRoot || config.domain);
if (type === 'reference') {
// Reference build: only exported components with reference resolution
if (config.exports) {
for (const [category, files] of Object.entries(config.exports)) {
if (Array.isArray(files)) {
const categoryPath = config.paths[category] || category;
for (const filename of files) {
const sourcePath = path.join(domainPath, categoryPath, filename);
const targetPath = path.join(fullOutputDir, config.domain, categoryPath, filename);
if (await fs.pathExists(sourcePath)) {
await fs.ensureDir(path.dirname(targetPath));
const originalContent = await fs.readJSON(sourcePath);
const processedContent = await resolveReferencesToPayload(originalContent, resolver, config.domain);
await fs.writeJSON(targetPath, processedContent, { spaces: 2 });
}
}
}
}
}
} else if (type === 'runtime') {
// Runtime build: complete domain structure with reference resolution
console.log(chalk.blue(`π§ Processing runtime build for domain: ${config.domain}`));
const targetDomainPath = path.join(fullOutputDir, config.domain);
if (await fs.pathExists(domainPath)) {
console.log(chalk.gray(`π Source domain path: ${domainPath}`));
console.log(chalk.gray(`π Target domain path: ${targetDomainPath}`));
// Get all files recursively
const allFiles = await getAllF