@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
403 lines (361 loc) • 14.5 kB
text/typescript
import chalk from "chalk";
import ora from "ora";
import { glob } from "glob";
import fs from "fs-extra";
import path from "path";
import axios from "axios";
import { loadConfig, validateConfig } from "../utils/config";
import { checkMigrationAccess, validateMigrationRequest, MIGRATION_TIERS } from "../../../lib/migration-tier-system";
interface MigrateOptions {
layers?: string;
output?: string;
recursive?: boolean;
include?: string;
exclude?: string;
config?: string;
dryRun?: boolean;
backup?: boolean;
verbose?: boolean;
}
interface MigrationResult {
success: boolean;
file: string;
transformed?: string;
analysis: any;
issues: any[];
fixes: any[];
performanceMetrics?: any;
}
/**
* Perform full migration using NeuroLintProEnhanced with all layers
* This leverages the existing infrastructure for enterprise-grade migrations
*/
async function performMigration(
code: string,
filename: string,
config: any,
options: MigrateOptions
): Promise<MigrationResult> {
try {
// Migration uses all layers (1-6) with proper tier authorization
const migrationPayload = {
code,
filename,
layers: "1,2,3,4,5,6", // All layers enabled for migration
applyFixes: !options.dryRun,
// REMOVED: migration bypass flag - now uses proper tier system
metadata: {
recursive: options.recursive,
outputFormat: options.output || "detailed-json",
verbose: options.verbose || true,
backup: options.backup !== false, // Default to backup enabled
migrationService: true, // Indicates this is a migration service request
timestamp: new Date().toISOString(),
},
};
const response = await axios.post(
`${config.api.url}/analyze`,
migrationPayload,
{
headers: {
"X-API-Key": config.apiKey,
"Content-Type": "application/json",
"X-Migration-Service": "true", // Indicates migration service usage
},
timeout: 300000, // 5 minutes for complex migrations
maxContentLength: 100 * 1024 * 1024, // 100MB for large files
}
);
return {
success: response.data.success || true,
file: filename,
transformed: response.data.transformed || response.data.code,
analysis: response.data.analysis || response.data,
issues: response.data.analysis?.detectedIssues || [],
fixes: response.data.analysis?.appliedFixes || [],
performanceMetrics: response.data.performanceMetrics,
};
} catch (error) {
console.log(chalk.yellow(`Warning: Migration failed for ${filename}`));
if (axios.isAxiosError(error)) {
console.log(chalk.gray(`API Error: ${error.response?.status} ${error.response?.statusText}`));
}
return {
success: false,
file: filename,
analysis: {},
issues: [],
fixes: [],
};
}
}
/**
* Generate detailed migration report
*/
function generateMigrationReport(results: MigrationResult[], options: MigrateOptions) {
const totalFiles = results.length;
const successfulFiles = results.filter(r => r.success).length;
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
const totalFixes = results.reduce((sum, r) => sum + r.fixes.length, 0);
const report = {
migrationSummary: {
totalFiles,
successfulFiles,
failedFiles: totalFiles - successfulFiles,
totalIssues,
totalFixes,
timestamp: new Date().toISOString(),
},
fileResults: results.map(r => ({
file: r.file,
success: r.success,
issuesFound: r.issues.length,
fixesApplied: r.fixes.length,
issues: r.issues,
fixes: r.fixes,
performanceMetrics: r.performanceMetrics,
})),
modernizationAreas: {
configuration: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 1).length, 0),
entityCleanup: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 2).length, 0),
components: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 3).length, 0),
hydration: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 4).length, 0),
nextjsAppRouter: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 5).length, 0),
testing: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 6).length, 0),
},
};
return report;
}
export async function migrateCommand(targetPath: string, options: MigrateOptions) {
const spinner = ora("Initializing one-time migration...").start();
const startTime = Date.now();
try {
// Load and validate configuration
const config = await loadConfig(options.config);
const configValidation = await validateConfig(config);
if (!configValidation.valid) {
spinner.fail("Configuration validation failed");
configValidation.errors.forEach((error) =>
console.log(chalk.red(`ERROR: ${error}`))
);
return;
}
// Verify API key for migration service
if (!config.apiKey) {
spinner.fail("Migration service requires authentication");
console.log(chalk.red("ERROR: API key required for one-time migration service"));
console.log(chalk.gray("Please run 'neurolint login' first"));
return;
}
// Verify migration access using proper tier system
try {
const userResponse = await axios.get(
`${config.api.url}/auth/current-user`,
{
headers: { "X-API-Key": config.apiKey },
timeout: 10000,
}
);
const userData = userResponse.data.user || userResponse.data;
const migrationAccess = checkMigrationAccess(
userData.plan || userData.tier || "free",
userData.migration_quote_approved,
userData.migration_expires_at ? new Date(userData.migration_expires_at) : undefined
);
if (!migrationAccess.hasAccess) {
spinner.fail("Migration service access required");
console.log(chalk.yellow(migrationAccess.reason || "Migration service requires enterprise access"));
console.log(chalk.gray("One-time migration service: $999-$9,999 (quote-based)"));
console.log(chalk.gray("Request quote: https://app.neurolint.dev/migration-request"));
console.log(chalk.gray("Contact: migration@neurolint.dev"));
return;
}
// Display migration tier info
if (migrationAccess.config) {
console.log(chalk.gray(`Migration tier: ${migrationAccess.tier}`));
console.log(chalk.gray(`Support level: ${migrationAccess.config.supportLevel}`));
if (migrationAccess.tier === 'migration' && userData.migration_expires_at) {
const expiresAt = new Date(userData.migration_expires_at);
console.log(chalk.gray(`Service expires: ${expiresAt.toLocaleDateString()}`));
}
}
} catch (error) {
spinner.fail("Unable to verify migration access");
console.log(chalk.red("Please contact support for migration service access"));
console.log(chalk.gray("Support: migration-support@neurolint.dev"));
return;
}
// Discover files to migrate
spinner.text = "Discovering files for migration...";
const targetFiles: string[] = [];
if (!targetPath) {
targetPath = process.cwd();
}
try {
// Use comprehensive patterns for migration
const migrationPatterns = [
"**/*.{ts,tsx,js,jsx}",
"**/*.json", // Config files
"**/package.json",
"**/tsconfig.json",
"**/next.config.js",
];
for (const pattern of migrationPatterns) {
const foundFiles = await glob(pattern, {
cwd: targetPath,
ignore: [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/coverage/**",
"**/.git/**",
],
});
targetFiles.push(...foundFiles.map(f => path.resolve(targetPath, f)));
}
} catch (error) {
spinner.fail("File discovery failed");
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
return;
}
if (targetFiles.length === 0) {
spinner.fail("No files found for migration");
console.log(chalk.yellow("No TypeScript, JavaScript, or configuration files found"));
return;
}
// Remove duplicates and filter existing files
const uniqueFiles = [...new Set(targetFiles)];
const existingFiles = [];
for (const file of uniqueFiles) {
if (await fs.pathExists(file)) {
existingFiles.push(file);
}
}
console.log();
console.log(chalk.white.bold("One-Time Migration Service"));
console.log(chalk.gray(`Migrating ${existingFiles.length} files with all layers (1-6)`));
console.log(chalk.gray(`Target: ${targetPath}`));
console.log(chalk.gray(`Mode: ${options.dryRun ? "DRY RUN" : "LIVE MIGRATION"}`));
console.log();
if (options.dryRun) {
console.log(chalk.yellow("WARNING: DRY RUN MODE - No files will be modified"));
} else {
console.log(chalk.green("LIVE MIGRATION - Files will be transformed"));
if (options.backup !== false) {
console.log(chalk.gray("Automatic backups enabled"));
}
}
console.log();
// Create backup directory if not dry run
let backupDir = "";
if (!options.dryRun && options.backup !== false) {
backupDir = path.join(targetPath, `.neurolint-backup-${Date.now()}`);
await fs.ensureDir(backupDir);
console.log(chalk.gray(`Backup directory: ${backupDir}`));
}
// Process files for migration
spinner.text = `Migrating ${existingFiles.length} files...`;
const migrationResults: MigrationResult[] = [];
let processedCount = 0;
for (const file of existingFiles) {
try {
processedCount++;
spinner.text = `Migrating ${processedCount}/${existingFiles.length}: ${path.basename(file)}`;
const code = await fs.readFile(file, "utf-8");
// Create backup if enabled
if (!options.dryRun && options.backup !== false) {
const relativePath = path.relative(targetPath, file);
const backupFile = path.join(backupDir, relativePath);
await fs.ensureDir(path.dirname(backupFile));
await fs.copy(file, backupFile);
}
// Perform migration
const result = await performMigration(code, file, config, options);
migrationResults.push(result);
// Write transformed code if not dry run and transformation succeeded
if (!options.dryRun && result.success && result.transformed) {
await fs.writeFile(file, result.transformed, "utf-8");
}
} catch (fileError) {
console.log(chalk.yellow(`Warning: Could not process ${file}`));
migrationResults.push({
success: false,
file,
analysis: {},
issues: [],
fixes: [],
});
}
}
const processingTime = Date.now() - startTime;
spinner.succeed(`Migration completed for ${existingFiles.length} files`);
// Generate and display results
const report = generateMigrationReport(migrationResults, options);
console.log();
console.log(chalk.white.bold("Migration Results"));
console.log();
console.log(chalk.white("Files processed: ") + chalk.cyan(report.migrationSummary.totalFiles));
console.log(chalk.white("Successful: ") + chalk.green(report.migrationSummary.successfulFiles));
console.log(chalk.white("Failed: ") + (report.migrationSummary.failedFiles > 0 ? chalk.red(report.migrationSummary.failedFiles) : chalk.green("0")));
console.log(chalk.white("Issues found: ") + chalk.yellow(report.migrationSummary.totalIssues));
console.log(chalk.white("Fixes applied: ") + chalk.green(report.migrationSummary.totalFixes));
console.log(chalk.white("Duration: ") + chalk.gray(`${Math.round(processingTime / 1000)}s`));
console.log();
// Show modernization areas
console.log(chalk.white("Modernization by Layer:"));
console.log(chalk.gray(` Configuration (Layer 1): ${report.modernizationAreas.configuration} issues`));
console.log(chalk.gray(` Entity Cleanup (Layer 2): ${report.modernizationAreas.entityCleanup} issues`));
console.log(chalk.gray(` Components (Layer 3): ${report.modernizationAreas.components} issues`));
console.log(chalk.gray(` Hydration (Layer 4): ${report.modernizationAreas.hydration} issues`));
console.log(chalk.gray(` Next.js App Router (Layer 5): ${report.modernizationAreas.nextjsAppRouter} issues`));
console.log(chalk.gray(` Testing (Layer 6): ${report.modernizationAreas.testing} issues`));
console.log();
// Save detailed report
const reportPath = path.join(targetPath, `neurolint-migration-report-${Date.now()}.json`);
await fs.writeJson(reportPath, report, { spaces: 2 });
console.log(chalk.white("Detailed report saved: ") + chalk.cyan(reportPath));
// Upload results to dashboard
try {
await axios.post(
`${config.api.url}/analysis`,
{
type: "migration",
results: report,
metadata: {
isMigration: true,
timestamp: new Date().toISOString(),
targetPath,
options,
},
},
{
headers: {
"X-API-Key": config.apiKey,
"Content-Type": "application/json",
},
}
);
console.log(chalk.gray("Results uploaded to dashboard: https://app.neurolint.dev"));
} catch (uploadError) {
console.log(chalk.yellow("Warning: Could not upload results to dashboard"));
}
console.log();
if (options.dryRun) {
console.log(chalk.white("Dry run completed - no files were modified"));
console.log(chalk.gray("Remove --dry-run flag to apply migrations"));
} else {
console.log(chalk.white("Migration completed successfully"));
if (options.backup !== false) {
console.log(chalk.gray(`Backups available in: ${backupDir}`));
}
}
console.log();
console.log(chalk.white("30-day priority support included"));
console.log(chalk.gray("Contact: migration-support@neurolint.dev"));
console.log(chalk.gray("Dashboard: https://app.neurolint.dev"));
} catch (error) {
spinner.fail("Migration failed");
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
}
}