UNPKG

aicm

Version:

A TypeScript CLI tool for managing AI IDE rules across different projects and teams

386 lines (385 loc) 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.cleanPackage = cleanPackage; exports.cleanWorkspaces = cleanWorkspaces; exports.clean = clean; exports.cleanCommand = cleanCommand; const chalk_1 = __importDefault(require("chalk")); const fs_extra_1 = __importDefault(require("fs-extra")); const node_path_1 = __importDefault(require("node:path")); const config_1 = require("../utils/config"); const working_directory_1 = require("../utils/working-directory"); const rules_file_writer_1 = require("../utils/rules-file-writer"); const workspace_discovery_1 = require("../utils/workspace-discovery"); function cleanFile(filePath, verbose) { if (!fs_extra_1.default.existsSync(filePath)) return false; try { fs_extra_1.default.removeSync(filePath); if (verbose) console.log(chalk_1.default.gray(` Removed ${filePath}`)); return true; } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to remove ${filePath}`)); return false; } } function cleanRulesBlock(filePath, verbose) { if (!fs_extra_1.default.existsSync(filePath)) return false; try { const content = fs_extra_1.default.readFileSync(filePath, "utf8"); const cleanedContent = (0, rules_file_writer_1.removeRulesBlock)(content); if (content === cleanedContent) return false; if (cleanedContent.trim() === "") { fs_extra_1.default.removeSync(filePath); if (verbose) console.log(chalk_1.default.gray(` Removed empty file ${filePath}`)); } else { fs_extra_1.default.writeFileSync(filePath, cleanedContent); if (verbose) console.log(chalk_1.default.gray(` Cleaned rules block from ${filePath}`)); } return true; } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to clean ${filePath}`)); return false; } } function cleanMcpServers(cwd, verbose) { const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json"); if (!fs_extra_1.default.existsSync(mcpPath)) return false; try { const content = fs_extra_1.default.readJsonSync(mcpPath); const mcpServers = content.mcpServers; if (!mcpServers) return false; let hasChanges = false; const newMcpServers = {}; for (const [key, value] of Object.entries(mcpServers)) { if (typeof value === "object" && value !== null && "aicm" in value && value.aicm === true) { hasChanges = true; } else { newMcpServers[key] = value; } } if (!hasChanges) return false; // If no servers remain and no other properties, remove the file if (Object.keys(newMcpServers).length === 0 && Object.keys(content).length === 1) { fs_extra_1.default.removeSync(mcpPath); if (verbose) console.log(chalk_1.default.gray(` Removed empty ${mcpPath}`)); } else { content.mcpServers = newMcpServers; fs_extra_1.default.writeJsonSync(mcpPath, content, { spaces: 2 }); if (verbose) console.log(chalk_1.default.gray(` Cleaned aicm MCP servers from ${mcpPath}`)); } return true; } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to clean MCP servers`)); return false; } } function cleanHooks(cwd, verbose) { const hooksJsonPath = node_path_1.default.join(cwd, ".cursor", "hooks.json"); const hooksDir = node_path_1.default.join(cwd, ".cursor", "hooks", "aicm"); let hasChanges = false; // Clean hooks directory if (fs_extra_1.default.existsSync(hooksDir)) { fs_extra_1.default.removeSync(hooksDir); if (verbose) console.log(chalk_1.default.gray(` Removed ${hooksDir}`)); hasChanges = true; } // Clean hooks.json if (fs_extra_1.default.existsSync(hooksJsonPath)) { try { const content = fs_extra_1.default.readJsonSync(hooksJsonPath); // Filter out aicm-managed hooks (those pointing to hooks/aicm/) const userConfig = { version: content.version || 1, hooks: {}, }; let removedAny = false; if (content.hooks) { for (const [hookType, hookCommands] of Object.entries(content.hooks)) { if (Array.isArray(hookCommands)) { const userCommands = hookCommands.filter((cmd) => !cmd.command || !cmd.command.includes("hooks/aicm/")); if (userCommands.length < hookCommands.length) { removedAny = true; } if (userCommands.length > 0) { userConfig.hooks[hookType] = userCommands; } } } } if (removedAny) { const hasUserHooks = userConfig.hooks && Object.keys(userConfig.hooks).length > 0; if (!hasUserHooks) { fs_extra_1.default.removeSync(hooksJsonPath); if (verbose) console.log(chalk_1.default.gray(` Removed empty ${hooksJsonPath}`)); } else { fs_extra_1.default.writeJsonSync(hooksJsonPath, userConfig, { spaces: 2 }); if (verbose) console.log(chalk_1.default.gray(` Cleaned aicm hooks from ${hooksJsonPath}`)); } hasChanges = true; } } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to clean hooks.json`)); } } return hasChanges; } /** * Clean aicm-managed skills from a skills directory * Only removes skills that have .aicm.json (presence indicates aicm management) */ function cleanSkills(cwd, verbose) { let cleanedCount = 0; // Skills directories for each target const skillsDirs = [ node_path_1.default.join(cwd, ".cursor", "skills"), node_path_1.default.join(cwd, ".claude", "skills"), node_path_1.default.join(cwd, ".codex", "skills"), ]; for (const skillsDir of skillsDirs) { if (!fs_extra_1.default.existsSync(skillsDir)) { continue; } try { const entries = fs_extra_1.default.readdirSync(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) { continue; } const skillPath = node_path_1.default.join(skillsDir, entry.name); const metadataPath = node_path_1.default.join(skillPath, ".aicm.json"); // Only clean skills that have .aicm.json (presence indicates aicm management) if (fs_extra_1.default.existsSync(metadataPath)) { fs_extra_1.default.removeSync(skillPath); if (verbose) { console.log(chalk_1.default.gray(` Removed skill ${skillPath}`)); } cleanedCount++; } } // Remove the skills directory if it's now empty const remainingEntries = fs_extra_1.default.readdirSync(skillsDir); if (remainingEntries.length === 0) { fs_extra_1.default.removeSync(skillsDir); if (verbose) { console.log(chalk_1.default.gray(` Removed empty directory ${skillsDir}`)); } } } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to clean skills in ${skillsDir}`)); } } return cleanedCount; } /** * Clean aicm-managed agents from agents directories * Only removes agents that are tracked in .aicm.json metadata file */ function cleanAgents(cwd, verbose) { let cleanedCount = 0; // Agents directories for each target const agentsDirs = [ node_path_1.default.join(cwd, ".cursor", "agents"), node_path_1.default.join(cwd, ".claude", "agents"), ]; for (const agentsDir of agentsDirs) { const metadataPath = node_path_1.default.join(agentsDir, ".aicm.json"); if (!fs_extra_1.default.existsSync(metadataPath)) { continue; } try { const metadata = fs_extra_1.default.readJsonSync(metadataPath); // Remove all managed agents (names only) for (const agentName of metadata.managedAgents || []) { // Skip invalid names containing path separators (security check) if (agentName.includes("/") || agentName.includes("\\")) { console.warn(chalk_1.default.yellow(`Warning: Skipping invalid agent name "${agentName}" (contains path separator)`)); continue; } const fullPath = node_path_1.default.join(agentsDir, agentName + ".md"); if (fs_extra_1.default.existsSync(fullPath)) { fs_extra_1.default.removeSync(fullPath); if (verbose) { console.log(chalk_1.default.gray(` Removed agent ${fullPath}`)); } cleanedCount++; } } // Remove the metadata file fs_extra_1.default.removeSync(metadataPath); if (verbose) { console.log(chalk_1.default.gray(` Removed ${metadataPath}`)); } // Remove the agents directory if it's now empty if (fs_extra_1.default.existsSync(agentsDir)) { const remainingEntries = fs_extra_1.default.readdirSync(agentsDir); if (remainingEntries.length === 0) { fs_extra_1.default.removeSync(agentsDir); if (verbose) { console.log(chalk_1.default.gray(` Removed empty directory ${agentsDir}`)); } } } } catch (_a) { console.warn(chalk_1.default.yellow(`Warning: Failed to clean agents in ${agentsDir}`)); } } return cleanedCount; } function cleanEmptyDirectories(cwd, verbose) { let cleanedCount = 0; const dirsToCheck = [ node_path_1.default.join(cwd, ".cursor", "rules"), node_path_1.default.join(cwd, ".cursor", "commands"), node_path_1.default.join(cwd, ".cursor", "assets"), node_path_1.default.join(cwd, ".cursor", "hooks"), node_path_1.default.join(cwd, ".cursor", "skills"), node_path_1.default.join(cwd, ".cursor", "agents"), node_path_1.default.join(cwd, ".cursor"), node_path_1.default.join(cwd, ".claude", "skills"), node_path_1.default.join(cwd, ".claude", "agents"), node_path_1.default.join(cwd, ".claude"), node_path_1.default.join(cwd, ".codex", "skills"), node_path_1.default.join(cwd, ".codex"), ]; for (const dir of dirsToCheck) { if (fs_extra_1.default.existsSync(dir)) { try { const contents = fs_extra_1.default.readdirSync(dir); if (contents.length === 0) { fs_extra_1.default.removeSync(dir); if (verbose) console.log(chalk_1.default.gray(` Removed empty directory ${dir}`)); cleanedCount++; } } catch (_a) { // Ignore errors when checking/removing empty directories } } } return cleanedCount; } async function cleanPackage(options = {}) { const cwd = options.cwd || process.cwd(); const verbose = options.verbose || false; return (0, working_directory_1.withWorkingDirectory)(cwd, async () => { let cleanedCount = 0; const filesToClean = [ node_path_1.default.join(cwd, ".cursor", "rules", "aicm"), node_path_1.default.join(cwd, ".cursor", "commands", "aicm"), node_path_1.default.join(cwd, ".cursor", "assets", "aicm"), node_path_1.default.join(cwd, ".aicm"), ]; const rulesFilesToClean = [ node_path_1.default.join(cwd, ".windsurfrules"), node_path_1.default.join(cwd, "AGENTS.md"), node_path_1.default.join(cwd, "CLAUDE.md"), ]; // Clean directories and files for (const file of filesToClean) { if (cleanFile(file, verbose)) cleanedCount++; } // Clean rules blocks from files for (const file of rulesFilesToClean) { if (cleanRulesBlock(file, verbose)) cleanedCount++; } // Clean MCP servers if (cleanMcpServers(cwd, verbose)) cleanedCount++; // Clean hooks if (cleanHooks(cwd, verbose)) cleanedCount++; // Clean skills cleanedCount += cleanSkills(cwd, verbose); // Clean agents cleanedCount += cleanAgents(cwd, verbose); // Clean empty directories cleanedCount += cleanEmptyDirectories(cwd, verbose); return { success: true, cleanedCount, }; }); } async function cleanWorkspaces(cwd, verbose = false) { if (verbose) console.log(chalk_1.default.blue("🔍 Discovering packages...")); const packages = await (0, workspace_discovery_1.discoverPackagesWithAicm)(cwd); if (verbose && packages.length > 0) { console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations.`)); } let totalCleaned = 0; // Clean all discovered packages for (const pkg of packages) { if (verbose) console.log(chalk_1.default.blue(`Cleaning package: ${pkg.relativePath}`)); const result = await cleanPackage({ cwd: pkg.absolutePath, verbose, }); totalCleaned += result.cleanedCount; } // Always clean root directory (for merged artifacts like mcp.json and commands) const rootPackage = packages.find((p) => p.absolutePath === cwd); if (!rootPackage) { if (verbose) console.log(chalk_1.default.blue(`Cleaning root workspace artifacts...`)); const rootResult = await cleanPackage({ cwd, verbose }); totalCleaned += rootResult.cleanedCount; } return { success: true, cleanedCount: totalCleaned, }; } async function clean(options = {}) { const cwd = options.cwd || process.cwd(); const verbose = options.verbose || false; const shouldUseWorkspaces = await (0, config_1.checkWorkspacesEnabled)(cwd); if (shouldUseWorkspaces) { return cleanWorkspaces(cwd, verbose); } return cleanPackage(options); } async function cleanCommand(verbose) { const result = await clean({ verbose }); if (result.cleanedCount === 0) { console.log("Nothing to clean."); } else { console.log(chalk_1.default.green(`Successfully cleaned ${result.cleanedCount} file(s)/director(y/ies).`)); } }