aicm
Version:
A TypeScript CLI tool for managing AI IDE rules across different projects and teams
386 lines (385 loc) • 15.6 kB
JavaScript
;
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).`));
}
}