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