2025-05-31 16:21:03 +02:00

284 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { createLogWrapper } from '../../../mcp-server/src/tools/utils.js';
import { findProjectRoot } from '../utils.js';
import {
LEGACY_CONFIG_FILE,
TASKMASTER_CONFIG_FILE
} from '../../../src/constants/paths.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Create a simple log wrapper for CLI use
const log = createLogWrapper({
info: (msg) => console.log(chalk.blue(''), msg),
warn: (msg) => console.log(chalk.yellow('⚠'), msg),
error: (msg) => console.error(chalk.red('✗'), msg),
success: (msg) => console.log(chalk.green('✓'), msg)
});
/**
* Main migration function
* @param {Object} options - Migration options
*/
export async function migrateProject(options = {}) {
const projectRoot = findProjectRoot() || process.cwd();
log.info(`Starting migration in: ${projectRoot}`);
// Check if .taskmaster directory already exists
const taskmasterDir = path.join(projectRoot, '.taskmaster');
if (fs.existsSync(taskmasterDir) && !options.force) {
log.warn(
'.taskmaster directory already exists. Use --force to overwrite or skip migration.'
);
return;
}
// Analyze what needs to be migrated
const migrationPlan = analyzeMigrationNeeds(projectRoot);
if (migrationPlan.length === 0) {
log.info(
'No files to migrate. Project may already be using the new structure.'
);
return;
}
// Show migration plan
log.info('Migration plan:');
for (const item of migrationPlan) {
const action = options.dryRun ? 'Would move' : 'Will move';
log.info(` ${action}: ${item.from}${item.to}`);
}
if (options.dryRun) {
log.info(
'Dry run complete. Use --dry-run=false to perform actual migration.'
);
return;
}
// Confirm migration
if (!options.yes) {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise((resolve) => {
rl.question('Proceed with migration? (y/N): ', resolve);
});
rl.close();
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
log.info('Migration cancelled.');
return;
}
}
// Perform migration
try {
await performMigration(projectRoot, migrationPlan, options);
log.success('Migration completed successfully!');
log.info('You can now use the new .taskmaster directory structure.');
if (!options.cleanup) {
log.info(
'Old files were preserved. Use --cleanup to remove them after verification.'
);
}
} catch (error) {
log.error(`Migration failed: ${error.message}`);
throw error;
}
}
/**
* Analyze what files need to be migrated
* @param {string} projectRoot - Project root directory
* @returns {Array} Migration plan items
*/
function analyzeMigrationNeeds(projectRoot) {
const migrationPlan = [];
// Check for tasks directory
const tasksDir = path.join(projectRoot, 'tasks');
if (fs.existsSync(tasksDir)) {
const tasksFiles = fs.readdirSync(tasksDir);
for (const file of tasksFiles) {
migrationPlan.push({
from: path.join('tasks', file),
to: path.join('.taskmaster', 'tasks', file),
type: 'task'
});
}
}
// Check for scripts directory files
const scriptsDir = path.join(projectRoot, 'scripts');
if (fs.existsSync(scriptsDir)) {
const scriptsFiles = fs.readdirSync(scriptsDir);
for (const file of scriptsFiles) {
const filePath = path.join(scriptsDir, file);
if (fs.statSync(filePath).isFile()) {
// Categorize files more intelligently
let destination;
const lowerFile = file.toLowerCase();
if (
lowerFile.includes('example') ||
lowerFile.includes('template') ||
lowerFile.includes('boilerplate') ||
lowerFile.includes('sample')
) {
// Template/example files go to templates (including example_prd.txt)
destination = path.join('.taskmaster', 'templates', file);
} else if (
lowerFile.includes('complexity') &&
lowerFile.includes('report') &&
lowerFile.endsWith('.json')
) {
// Only actual complexity reports go to reports
destination = path.join('.taskmaster', 'reports', file);
} else if (
lowerFile.includes('prd') ||
lowerFile.endsWith('.md') ||
lowerFile.endsWith('.txt')
) {
// Documentation files go to docs (but not examples or reports)
destination = path.join('.taskmaster', 'docs', file);
} else {
// Other files stay in scripts or get skipped - don't force everything into templates
log.warn(
`Skipping migration of '${file}' - uncertain categorization. You may need to move this manually.`
);
continue;
}
migrationPlan.push({
from: path.join('scripts', file),
to: destination,
type: 'script'
});
}
}
}
// Check for .taskmasterconfig
const oldConfig = path.join(projectRoot, LEGACY_CONFIG_FILE);
if (fs.existsSync(oldConfig)) {
migrationPlan.push({
from: LEGACY_CONFIG_FILE,
to: TASKMASTER_CONFIG_FILE,
type: 'config'
});
}
return migrationPlan;
}
/**
* Perform the actual migration
* @param {string} projectRoot - Project root directory
* @param {Array} migrationPlan - List of files to migrate
* @param {Object} options - Migration options
*/
async function performMigration(projectRoot, migrationPlan, options) {
// Create .taskmaster directory
const taskmasterDir = path.join(projectRoot, '.taskmaster');
if (!fs.existsSync(taskmasterDir)) {
fs.mkdirSync(taskmasterDir, { recursive: true });
}
// Group migration items by destination directory to create only needed subdirs
const neededDirs = new Set();
for (const item of migrationPlan) {
const destDir = path.dirname(item.to);
neededDirs.add(destDir);
}
// Create only the directories we actually need
for (const dir of neededDirs) {
const fullDirPath = path.join(projectRoot, dir);
if (!fs.existsSync(fullDirPath)) {
fs.mkdirSync(fullDirPath, { recursive: true });
log.info(`Created directory: ${dir}`);
}
}
// Create backup if requested
if (options.backup) {
const backupDir = path.join(projectRoot, '.taskmaster-migration-backup');
log.info(`Creating backup in: ${backupDir}`);
if (fs.existsSync(backupDir)) {
fs.rmSync(backupDir, { recursive: true, force: true });
}
fs.mkdirSync(backupDir, { recursive: true });
}
// Migrate files
for (const item of migrationPlan) {
const fromPath = path.join(projectRoot, item.from);
const toPath = path.join(projectRoot, item.to);
if (!fs.existsSync(fromPath)) {
log.warn(`Source file not found: ${item.from}`);
continue;
}
// Create backup if requested
if (options.backup) {
const backupPath = path.join(
projectRoot,
'.taskmaster-migration-backup',
item.from
);
const backupDir = path.dirname(backupPath);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
fs.copyFileSync(fromPath, backupPath);
}
// Ensure destination directory exists
const toDir = path.dirname(toPath);
if (!fs.existsSync(toDir)) {
fs.mkdirSync(toDir, { recursive: true });
}
// Copy file
fs.copyFileSync(fromPath, toPath);
log.info(`Migrated: ${item.from}${item.to}`);
// Remove original if cleanup is requested
if (options.cleanup) {
fs.unlinkSync(fromPath);
}
}
// Clean up empty directories if cleanup is requested
if (options.cleanup) {
const dirsToCheck = ['tasks', 'scripts'];
for (const dir of dirsToCheck) {
const dirPath = path.join(projectRoot, dir);
if (fs.existsSync(dirPath)) {
try {
const files = fs.readdirSync(dirPath);
if (files.length === 0) {
fs.rmdirSync(dirPath);
log.info(`Removed empty directory: ${dir}`);
}
} catch (error) {
// Directory not empty or other error, skip
}
}
}
}
}
export default { migrateProject };