mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-03 23:19:44 +00:00
284 lines
7.8 KiB
JavaScript
284 lines
7.8 KiB
JavaScript
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 };
|