UNPKG

aicm

Version:

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

487 lines (486 loc) 19.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.installPackage = installPackage; exports.install = install; exports.installCommand = installCommand; const chalk_1 = __importDefault(require("chalk")); const fs_extra_1 = __importDefault(require("fs-extra")); const node_path_1 = __importDefault(require("node:path")); const child_process_1 = require("child_process"); const config_1 = require("../utils/config"); const working_directory_1 = require("../utils/working-directory"); const is_ci_1 = require("../utils/is-ci"); const rules_file_writer_1 = require("../utils/rules-file-writer"); function getTargetPaths() { const projectDir = process.cwd(); return { cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"), windsurf: node_path_1.default.join(projectDir, ".aicm"), codex: node_path_1.default.join(projectDir, ".aicm"), }; } function writeCursorRules(rules, cursorRulesDir) { fs_extra_1.default.emptyDirSync(cursorRulesDir); for (const rule of rules) { let rulePath; const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean); if (rule.presetName) { // For rules from presets, create a namespaced directory structure const namespace = extractNamespaceFromPresetPath(rule.presetName); // Path will be: cursorRulesDir/namespace/rule-name.mdc rulePath = node_path_1.default.join(cursorRulesDir, ...namespace, ...ruleNameParts); } else { // For local rules, maintain the original flat structure rulePath = node_path_1.default.join(cursorRulesDir, ...ruleNameParts); } const ruleFile = rulePath + ".mdc"; fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile)); fs_extra_1.default.writeFileSync(ruleFile, rule.content); } } function extractNamespaceFromPresetPath(presetPath) { // Special case: npm package names always use forward slashes, regardless of platform if (presetPath.startsWith("@")) { // For scoped packages like @scope/package/subdir, create nested directories return presetPath.split("/"); } const parts = presetPath.split(node_path_1.default.sep); return parts.filter((part) => part.length > 0); // Filter out empty segments } /** * Write rules to a shared directory and update the given rules file */ function writeRulesForFile(rules, ruleDir, rulesFile) { fs_extra_1.default.emptyDirSync(ruleDir); const ruleFiles = rules.map((rule) => { let rulePath; const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean); if (rule.presetName) { // For rules from presets, create a namespaced directory structure const namespace = extractNamespaceFromPresetPath(rule.presetName); // Path will be: ruleDir/namespace/rule-name.md rulePath = node_path_1.default.join(ruleDir, ...namespace, ...ruleNameParts); } else { // For local rules, maintain the original flat structure rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts); } const physicalRulePath = rulePath + ".md"; fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath)); fs_extra_1.default.writeFileSync(physicalRulePath, rule.content); const relativeRuleDir = node_path_1.default.basename(ruleDir); // For the rules file, maintain the same structure let windsurfPath; if (rule.presetName) { const namespace = extractNamespaceFromPresetPath(rule.presetName); windsurfPath = node_path_1.default.join(relativeRuleDir, ...namespace, ...ruleNameParts) + ".md"; } else { windsurfPath = node_path_1.default.join(relativeRuleDir, ...ruleNameParts) + ".md"; } // Normalize to POSIX style for cross-platform compatibility const windsurfPathPosix = windsurfPath.replace(/\\/g, "/"); return { name: rule.name, path: windsurfPathPosix, metadata: (0, rules_file_writer_1.parseRuleFrontmatter)(rule.content), }; }); const rulesContent = (0, rules_file_writer_1.generateRulesFileContent)(ruleFiles); (0, rules_file_writer_1.writeRulesFile)(rulesContent, node_path_1.default.join(process.cwd(), rulesFile)); } /** * Write all collected rules to their respective IDE targets */ function writeRulesToTargets(rules, targets) { const targetPaths = getTargetPaths(); for (const target of targets) { switch (target) { case "cursor": if (rules.length > 0) { writeCursorRules(rules, targetPaths.cursor); } break; case "windsurf": if (rules.length > 0) { writeRulesForFile(rules, targetPaths.windsurf, ".windsurfrules"); } break; case "codex": if (rules.length > 0) { writeRulesForFile(rules, targetPaths.codex, "AGENTS.md"); } break; } } } /** * Write MCP servers configuration to IDE targets */ function writeMcpServersToTargets(mcpServers, targets, cwd) { if (!mcpServers || Object.keys(mcpServers).length === 0) return; for (const target of targets) { if (target === "cursor") { const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json"); writeMcpServersToFile(mcpServers, mcpPath); } // Windsurf and Codex do not support project mcpServers, so skip } } /** * Write MCP servers configuration to a specific file */ function writeMcpServersToFile(mcpServers, mcpPath) { var _a; fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(mcpPath)); const existingConfig = fs_extra_1.default.existsSync(mcpPath) ? fs_extra_1.default.readJsonSync(mcpPath) : {}; const existingMcpServers = (_a = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.mcpServers) !== null && _a !== void 0 ? _a : {}; // Filter out any existing aicm-managed servers (with aicm: true) // This removes stale aicm servers that are no longer in the configuration const userMcpServers = {}; for (const [key, value] of Object.entries(existingMcpServers)) { if (typeof value === "object" && value !== null && value.aicm !== true) { userMcpServers[key] = value; } } // Mark new aicm servers as managed and filter out canceled servers const aicmMcpServers = {}; for (const [key, value] of Object.entries(mcpServers)) { if (value !== false) { aicmMcpServers[key] = { ...value, aicm: true, }; } } // Merge user servers with aicm servers (aicm servers override user servers with same key) const mergedMcpServers = { ...userMcpServers, ...aicmMcpServers, }; const mergedConfig = { ...existingConfig, mcpServers: mergedMcpServers, }; fs_extra_1.default.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 }); } function mergeWorkspaceMcpServers(packages) { const merged = {}; const info = {}; for (const pkg of packages) { for (const [key, value] of Object.entries(pkg.config.mcpServers)) { if (value === false) continue; const json = JSON.stringify(value); if (!info[key]) { info[key] = { configs: new Set([json]), packages: [pkg.relativePath], chosen: pkg.relativePath, }; } else { info[key].packages.push(pkg.relativePath); info[key].configs.add(json); info[key].chosen = pkg.relativePath; } merged[key] = value; } } const conflicts = []; for (const [key, data] of Object.entries(info)) { if (data.configs.size > 1) { conflicts.push({ key, packages: data.packages, chosen: data.chosen }); } } return { merged, conflicts }; } /** * Discover all packages with aicm configurations using git ls-files */ function findAicmFiles(rootDir) { try { const output = (0, child_process_1.execSync)("git ls-files --cached --others --exclude-standard aicm.json **/aicm.json", { cwd: rootDir, encoding: "utf8", }); return output .trim() .split("\n") .filter(Boolean) .map((file) => node_path_1.default.resolve(rootDir, file)); } catch (_a) { // Fallback to manual search if git is not available return []; } } /** * Discover all packages with aicm configurations */ async function discoverPackagesWithAicm(rootDir) { const aicmFiles = findAicmFiles(rootDir); const packages = []; for (const aicmFile of aicmFiles) { const packageDir = node_path_1.default.dirname(aicmFile); const relativePath = node_path_1.default.relative(rootDir, packageDir); // Normalize to forward slashes for cross-platform compatibility const normalizedRelativePath = relativePath.replace(/\\/g, "/"); const config = await (0, config_1.loadConfig)(packageDir); if (config) { packages.push({ relativePath: normalizedRelativePath || ".", absolutePath: packageDir, config, }); } } // Sort packages by relativePath for deterministic order return packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); } /** * Install rules for a single package (used within workspaces and standalone installs) */ async function installPackage(options = {}) { const cwd = options.cwd || process.cwd(); return (0, working_directory_1.withWorkingDirectory)(cwd, async () => { let resolvedConfig; if (options.config) { resolvedConfig = options.config; } else { resolvedConfig = await (0, config_1.loadConfig)(cwd); } if (!resolvedConfig) { return { success: false, error: new Error("Configuration file not found"), installedRuleCount: 0, packagesCount: 0, }; } const { config, rules, mcpServers } = resolvedConfig; if (config.skipInstall === true) { return { success: true, installedRuleCount: 0, packagesCount: 0, }; } try { if (!options.dryRun) { // Write rules to targets writeRulesToTargets(rules, config.targets); // Write MCP servers if (mcpServers && Object.keys(mcpServers).length > 0) { writeMcpServersToTargets(mcpServers, config.targets, cwd); } } return { success: true, installedRuleCount: rules.length, packagesCount: 1, }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)), installedRuleCount: 0, packagesCount: 0, }; } }); } /** * Install aicm configurations for all packages in a workspace */ async function installWorkspacesPackages(packages, options = {}) { const results = []; let totalRuleCount = 0; // Install packages sequentially for now (can be parallelized later) for (const pkg of packages) { const packagePath = pkg.absolutePath; try { const result = await installPackage({ ...options, cwd: packagePath, config: pkg.config, }); totalRuleCount += result.installedRuleCount; results.push({ path: pkg.relativePath, success: result.success, error: result.error, installedRuleCount: result.installedRuleCount, }); } catch (error) { results.push({ path: pkg.relativePath, success: false, error: error instanceof Error ? error : new Error(String(error)), installedRuleCount: 0, }); } } const failedPackages = results.filter((r) => !r.success); return { success: failedPackages.length === 0, packages: results, totalRuleCount, }; } /** * Install rules across multiple packages in a workspace */ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = false) { return (0, working_directory_1.withWorkingDirectory)(cwd, async () => { if (verbose) { console.log(chalk_1.default.blue("🔍 Discovering packages...")); } const allPackages = await discoverPackagesWithAicm(cwd); const packages = allPackages.filter((pkg) => { if (pkg.config.config.skipInstall === true) { return false; } const isRoot = pkg.relativePath === "."; if (!isRoot) return true; // For root directories, only keep if it has rules or presets const hasRules = pkg.config.rules && pkg.config.rules.length > 0; const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0; return hasRules || hasPresets; }); if (packages.length === 0) { return { success: false, error: new Error("No packages with aicm configurations found"), installedRuleCount: 0, packagesCount: 0, }; } if (verbose) { console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations:`)); packages.forEach((pkg) => { console.log(chalk_1.default.gray(` - ${pkg.relativePath}`)); }); console.log(chalk_1.default.blue(`📦 Installing configurations...`)); } const result = await installWorkspacesPackages(packages, { installOnCI, verbose, dryRun, }); const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages); const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor")); if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) { const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json"); writeMcpServersToFile(rootMcp, mcpPath); } for (const conflict of conflicts) { console.warn(`Warning: MCP configuration conflict detected\n Key: "${conflict.key}"\n Packages: ${conflict.packages.join(", ")}\n Using configuration from: ${conflict.chosen}`); } if (verbose) { result.packages.forEach((pkg) => { if (pkg.success) { console.log(chalk_1.default.green(`✅ ${pkg.path} (${pkg.installedRuleCount} rules)`)); } else { console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`)); } }); } const failedPackages = result.packages.filter((r) => !r.success); if (failedPackages.length > 0) { console.log(chalk_1.default.yellow(`Installation completed with errors`)); if (verbose) { console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rules total)`)); console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`)); } const errorDetails = failedPackages .map((p) => `${p.path}: ${p.error}`) .join("; "); return { success: false, error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`), installedRuleCount: result.totalRuleCount, packagesCount: result.packages.length, }; } return { success: true, installedRuleCount: result.totalRuleCount, packagesCount: result.packages.length, }; }); } /** * Core implementation of the rule installation logic */ async function install(options = {}) { const cwd = options.cwd || process.cwd(); const installOnCI = options.installOnCI === true; // Default to false if not specified const inCI = (0, is_ci_1.isCIEnvironment)(); if (inCI && !installOnCI) { console.log(chalk_1.default.yellow("Detected CI environment, skipping install.")); return { success: true, installedRuleCount: 0, packagesCount: 0, }; } return (0, working_directory_1.withWorkingDirectory)(cwd, async () => { let resolvedConfig; if (options.config) { resolvedConfig = options.config; } else { resolvedConfig = await (0, config_1.loadConfig)(cwd); } const shouldUseWorkspaces = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.config.workspaces) || (!resolvedConfig && (0, config_1.detectWorkspacesFromPackageJson)(cwd)); if (shouldUseWorkspaces) { return await installWorkspaces(cwd, installOnCI, options.verbose, options.dryRun); } return installPackage(options); }); } /** * CLI command wrapper for install */ async function installCommand(installOnCI, verbose, dryRun) { var _a; const result = await install({ installOnCI, verbose, dryRun }); if (!result.success) { throw (_a = result.error) !== null && _a !== void 0 ? _a : new Error("Installation failed with unknown error"); } else { const rulesInstalledMessage = `${result.installedRuleCount} rule${result.installedRuleCount === 1 ? "" : "s"}`; if (dryRun) { if (result.packagesCount > 1) { console.log(`Dry run: validated ${rulesInstalledMessage} across ${result.packagesCount} packages`); } else { console.log(`Dry run: validated ${rulesInstalledMessage}`); } } else if (result.installedRuleCount === 0) { console.log("No rules installed"); } else if (result.packagesCount > 1) { console.log(`Successfully installed ${rulesInstalledMessage} across ${result.packagesCount} packages`); } else { console.log(`Successfully installed ${rulesInstalledMessage}`); } } }