UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

250 lines 9.61 kB
/** * V3 CLI Cleanup Command * Removes project artifacts created by claude-flow/ruflo * * Created with ruv.io */ import { output } from '../output.js'; import { existsSync, statSync, rmSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; /** * Ruflo-owned subdirectories within .claude/ that are safe to delete. * Everything else in .claude/ (agents, skills, commands, settings.local.json, * memory.db, worktrees, launch.json) belongs to Claude Code and must be preserved. * See: https://github.com/ruvnet/ruflo/issues/1557 */ const CLAUDE_OWNED_SUBDIRS = [ { path: join('.claude', 'helpers'), description: 'Ruflo hook scripts' }, ]; /** * Artifact directories and files that claude-flow/ruflo may create */ const ARTIFACT_DIRS = [ { path: '.claude-flow', description: 'Capabilities and configuration' }, { path: 'data', description: 'Memory databases' }, { path: '.swarm', description: 'Swarm state' }, { path: '.hive-mind', description: 'Consensus state' }, { path: 'coordination', description: 'Coordination data' }, { path: 'memory', description: 'Memory storage' }, ]; const ARTIFACT_FILES = [ { path: 'claude-flow.config.json', description: 'Claude Flow configuration' }, ]; /** * Paths to preserve when --keep-config is set */ const KEEP_CONFIG_PATHS = [ 'claude-flow.config.json', join('.claude', 'settings.json'), ]; /** * Calculate the total size of a path (file or directory) in bytes */ function getSize(fullPath) { try { const stat = statSync(fullPath); if (stat.isFile()) { return stat.size; } if (stat.isDirectory()) { let total = 0; const entries = readdirSync(fullPath, { withFileTypes: true }); for (const entry of entries) { total += getSize(join(fullPath, entry.name)); } return total; } } catch { // Permission errors, broken symlinks, etc. } return 0; } /** * Format bytes into a human-readable string */ function formatSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, i); return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } /** * Cleanup command definition */ export const cleanupCommand = { name: 'cleanup', description: 'Remove project artifacts created by claude-flow/ruflo', aliases: ['clean'], options: [ { name: 'dry-run', short: 'n', description: 'Show what would be removed without deleting (default behavior)', type: 'boolean', default: true, }, { name: 'force', short: 'f', description: 'Actually delete the artifacts', type: 'boolean', default: false, }, { name: 'keep-config', short: 'k', description: 'Preserve claude-flow.config.json and .claude/settings.json', type: 'boolean', default: false, }, ], examples: [ { command: 'cleanup', description: 'Show what would be removed (dry run)', }, { command: 'cleanup --force', description: 'Remove all claude-flow artifacts', }, { command: 'cleanup --force --keep-config', description: 'Remove artifacts but keep configuration files', }, ], action: async (ctx) => { const force = ctx.flags.force === true; const keepConfig = ctx.flags['keep-config'] === true; const cwd = ctx.cwd; const dryRun = !force; output.writeln(); output.writeln(output.bold(dryRun ? 'Claude Flow Cleanup (dry run)' : 'Claude Flow Cleanup')); output.writeln(); const found = []; let totalSize = 0; // Scan ruflo-owned subdirs within .claude/ (surgical — preserves Claude Code files) for (const artifact of CLAUDE_OWNED_SUBDIRS) { const fullPath = join(cwd, artifact.path); if (existsSync(fullPath)) { const size = getSize(fullPath); found.push({ path: artifact.path, description: artifact.description, size, type: 'dir' }); totalSize += size; } } // Check if .claude/settings.json has ruflo hooks/claudeFlow blocks to clean const settingsPath = join(cwd, '.claude', 'settings.json'); if (existsSync(settingsPath)) { found.push({ path: join('.claude', 'settings.json'), description: 'Remove ruflo hooks/claudeFlow blocks (preserves rest)', size: 0, type: 'file' }); } // Scan standalone artifact directories for (const artifact of ARTIFACT_DIRS) { const fullPath = join(cwd, artifact.path); if (existsSync(fullPath)) { const size = getSize(fullPath); found.push({ path: artifact.path, description: artifact.description, size, type: 'dir' }); totalSize += size; } } // Scan files for (const artifact of ARTIFACT_FILES) { const fullPath = join(cwd, artifact.path); if (existsSync(fullPath)) { const size = getSize(fullPath); found.push({ path: artifact.path, description: artifact.description, size, type: 'file' }); totalSize += size; } } if (found.length === 0) { output.writeln(output.info('No claude-flow artifacts found in the current directory.')); return { success: true, message: 'Nothing to clean' }; } // Mark items that would be skipped due to --keep-config if (keepConfig) { for (const item of found) { if (KEEP_CONFIG_PATHS.includes(item.path)) { item.skipped = true; } } } // Display what was found output.writeln(output.bold('Artifacts found:')); output.writeln(); let removedCount = 0; let removedSize = 0; let skippedCount = 0; for (const item of found) { const sizeStr = formatSize(item.size); const typeLabel = item.type === 'dir' ? 'dir ' : 'file'; if (item.skipped) { output.writeln(output.dim(` [skip] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`)); skippedCount++; continue; } if (dryRun) { output.writeln(output.warning(` [would remove] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`)); } else { // Actually delete try { const fullPath = join(cwd, item.path); // Special handling: surgically clean settings.json instead of deleting if (item.path === join('.claude', 'settings.json')) { try { const raw = JSON.parse(readFileSync(fullPath, 'utf-8')); delete raw.hooks; delete raw.claudeFlow; writeFileSync(fullPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8'); } catch { /* settings.json parse failed, skip */ } } else if (item.type === 'dir') { rmSync(fullPath, { recursive: true, force: true }); } else { rmSync(fullPath, { force: true }); } output.writeln(output.success(` [removed] ${typeLabel} ${item.path} (${sizeStr}) - ${item.description}`)); removedCount++; removedSize += item.size; } catch (err) { const msg = err instanceof Error ? err.message : String(err); output.writeln(output.error(` [failed] ${typeLabel} ${item.path} - ${msg}`)); } } } // Summary output.writeln(); output.writeln(output.bold('Summary:')); if (dryRun) { const actionable = found.filter(f => !f.skipped); output.writeln(` Found ${actionable.length} artifact(s) totaling ${formatSize(totalSize)}`); if (skippedCount > 0) { output.writeln(` ${skippedCount} item(s) would be preserved (--keep-config)`); } output.writeln(); output.writeln(output.dim(' This was a dry run. Use --force to actually remove artifacts.')); } else { output.writeln(` Removed ${removedCount} artifact(s) totaling ${formatSize(removedSize)}`); if (skippedCount > 0) { output.writeln(` Preserved ${skippedCount} item(s) (--keep-config)`); } } output.writeln(); return { success: true, message: dryRun ? `Dry run: ${found.length} artifact(s) found` : `Removed ${removedCount} artifact(s)`, data: { found, removedCount, removedSize, dryRun }, }; }, }; export default cleanupCommand; //# sourceMappingURL=cleanup.js.map