aicm
Version:
A TypeScript CLI tool for managing AI IDE rules across different projects and teams
737 lines (736 loc) • 30.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeAssetsToTargets = writeAssetsToTargets;
exports.writeCommandsToTargets = writeCommandsToTargets;
exports.writeSkillsToTargets = writeSkillsToTargets;
exports.warnPresetSkillCollisions = warnPresetSkillCollisions;
exports.dedupeSkillsForInstall = dedupeSkillsForInstall;
exports.writeAgentsToTargets = writeAgentsToTargets;
exports.warnPresetAgentCollisions = warnPresetAgentCollisions;
exports.dedupeAgentsForInstall = dedupeAgentsForInstall;
exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
exports.writeMcpServersToFile = writeMcpServersToFile;
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 config_1 = require("../utils/config");
const hooks_1 = require("../utils/hooks");
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");
const install_workspaces_1 = require("./install-workspaces");
/**
* Rewrite asset references from source paths to installation paths
* Only rewrites the ../assets/ pattern - everything else is preserved
*
* @param content - The file content to rewrite
* @param presetName - The preset name if this file is from a preset
* @param fileInstallDepth - The depth of the file's installation directory relative to .cursor/
* For example: .cursor/commands/aicm/file.md has depth 2 (commands, aicm)
* .cursor/rules/aicm/preset/file.mdc has depth 3 (rules, aicm, preset)
*/
function rewriteAssetReferences(content, presetName, fileInstallDepth = 2) {
// Calculate the relative path from the file to .cursor/assets/aicm/
// We need to go up fileInstallDepth levels to reach .cursor/, then down to assets/aicm/
const upLevels = "../".repeat(fileInstallDepth);
// If this is from a preset, include the preset namespace in the asset path
let assetBasePath = "assets/aicm/";
if (presetName) {
const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
assetBasePath = node_path_1.default.posix.join("assets", "aicm", ...namespace) + "/";
}
const targetPath = upLevels + assetBasePath;
// Replace ../assets/ with the calculated target path
// Handles both forward slashes and backslashes for cross-platform compatibility
return content.replace(/\.\.[\\/]assets[\\/]/g, targetPath);
}
function getTargetPaths() {
const projectDir = process.cwd();
return {
cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
assetsAicm: node_path_1.default.join(projectDir, ".cursor", "assets", "aicm"),
aicm: 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 = (0, config_1.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));
// Calculate the depth for asset path rewriting
// cursorRulesDir is .cursor/rules/aicm (depth 2 from .cursor)
// Add namespace depth if present
let fileInstallDepth = 2; // rules, aicm
if (rule.presetName) {
const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
fileInstallDepth += namespace.length;
}
// Add any subdirectories in the rule name
fileInstallDepth += ruleNameParts.length - 1; // -1 because the last part is the filename
// Rewrite asset references before writing
const content = rewriteAssetReferences(rule.content, rule.presetName, fileInstallDepth);
fs_extra_1.default.writeFileSync(ruleFile, content);
}
}
function writeCursorCommands(commands, cursorCommandsDir) {
fs_extra_1.default.removeSync(cursorCommandsDir);
for (const command of commands) {
const commandNameParts = command.name
.replace(/\\/g, "/")
.split("/")
.filter(Boolean);
const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
const commandFile = commandPath + ".md";
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
// Calculate the depth for asset path rewriting
// cursorCommandsDir is .cursor/commands/aicm (depth 2 from .cursor)
// Commands are NOT namespaced by preset, but we still need to account for subdirectories
let fileInstallDepth = 2; // commands, aicm
// Add any subdirectories in the command name
fileInstallDepth += commandNameParts.length - 1; // -1 because the last part is the filename
// Rewrite asset references before writing
const content = rewriteAssetReferences(command.content, command.presetName, fileInstallDepth);
fs_extra_1.default.writeFileSync(commandFile, content);
}
}
/**
* Write rules to a shared directory and update the given rules file
*/
function writeRulesForFile(rules, assets, 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 = (0, config_1.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);
}
// For windsurf/codex/claude, assets are installed at the same namespace level as rules
// Example: .aicm/my-preset/rule.md and .aicm/my-preset/asset.json
// So we need to remove the 'assets/' part from the path
// ../assets/file.json -> ../file.json
// ../../assets/file.json -> ../../file.json
const content = rule.content.replace(/(\.\.[/\\])assets[/\\]/g, "$1");
const physicalRulePath = rulePath + ".md";
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
fs_extra_1.default.writeFileSync(physicalRulePath, content);
const relativeRuleDir = node_path_1.default.basename(ruleDir);
// For the rules file, maintain the same structure
let windsurfPath;
if (rule.presetName) {
const namespace = (0, config_1.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)(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));
}
function writeAssetsToTargets(assets, targets) {
const targetPaths = getTargetPaths();
for (const target of targets) {
let targetDir;
switch (target) {
case "cursor":
targetDir = targetPaths.assetsAicm;
break;
case "windsurf":
case "codex":
case "claude":
targetDir = targetPaths.aicm;
break;
default:
continue;
}
for (const asset of assets) {
let assetPath;
if (asset.presetName) {
const namespace = (0, config_1.extractNamespaceFromPresetPath)(asset.presetName);
assetPath = node_path_1.default.join(targetDir, ...namespace, asset.name);
}
else {
assetPath = node_path_1.default.join(targetDir, asset.name);
}
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(assetPath));
fs_extra_1.default.writeFileSync(assetPath, asset.content);
}
}
}
/**
* Write all collected rules to their respective IDE targets
*/
function writeRulesToTargets(rules, assets, 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, assets, targetPaths.aicm, ".windsurfrules");
}
break;
case "codex":
if (rules.length > 0) {
writeRulesForFile(rules, assets, targetPaths.aicm, "AGENTS.md");
}
break;
case "claude":
if (rules.length > 0) {
writeRulesForFile(rules, assets, targetPaths.aicm, "CLAUDE.md");
}
break;
}
}
// Write assets after rules so they don't get wiped by emptyDirSync
writeAssetsToTargets(assets, targets);
}
function writeCommandsToTargets(commands, targets) {
const projectDir = process.cwd();
const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
for (const target of targets) {
if (target === "cursor") {
const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
writeCursorCommands(commands, commandsDir);
}
// Other targets do not support commands yet
}
}
/**
* Get the skills installation path for a target
* Returns null for targets that don't support skills
*/
function getSkillsTargetPath(target) {
const projectDir = process.cwd();
switch (target) {
case "cursor":
return node_path_1.default.join(projectDir, ".cursor", "skills");
case "claude":
return node_path_1.default.join(projectDir, ".claude", "skills");
case "codex":
return node_path_1.default.join(projectDir, ".codex", "skills");
case "windsurf":
// Windsurf does not support skills
return null;
default:
return null;
}
}
/**
* Write a single skill to the target directory
* Copies the entire skill directory and writes .aicm.json metadata
*/
function writeSkillToTarget(skill, targetSkillsDir) {
const skillTargetPath = node_path_1.default.join(targetSkillsDir, skill.name);
// Remove existing skill directory if it exists (to ensure clean install)
if (fs_extra_1.default.existsSync(skillTargetPath)) {
fs_extra_1.default.removeSync(skillTargetPath);
}
// Copy the entire skill directory
fs_extra_1.default.copySync(skill.sourcePath, skillTargetPath);
// Write .aicm.json metadata file
// The presence of this file indicates the skill is managed by aicm
const metadata = {
source: skill.source,
};
if (skill.presetName) {
metadata.presetName = skill.presetName;
}
const metadataPath = node_path_1.default.join(skillTargetPath, ".aicm.json");
fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
}
/**
* Write skills to all supported target directories
*/
function writeSkillsToTargets(skills, targets) {
if (skills.length === 0)
return;
for (const target of targets) {
const targetSkillsDir = getSkillsTargetPath(target);
if (!targetSkillsDir) {
// Target doesn't support skills
continue;
}
// Ensure the skills directory exists
fs_extra_1.default.ensureDirSync(targetSkillsDir);
for (const skill of skills) {
writeSkillToTarget(skill, targetSkillsDir);
}
}
}
/**
* Warn about skill name collisions from different presets
*/
function warnPresetSkillCollisions(skills) {
const collisions = new Map();
for (const skill of skills) {
if (!skill.presetName)
continue;
const entry = collisions.get(skill.name);
if (entry) {
entry.presets.add(skill.presetName);
entry.lastPreset = skill.presetName;
}
else {
collisions.set(skill.name, {
presets: new Set([skill.presetName]),
lastPreset: skill.presetName,
});
}
}
for (const [skillName, { presets, lastPreset }] of collisions) {
if (presets.size > 1) {
const presetList = Array.from(presets).sort().join(", ");
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`));
}
}
}
/**
* Dedupe skills by name (last one wins)
*/
function dedupeSkillsForInstall(skills) {
const unique = new Map();
for (const skill of skills) {
unique.set(skill.name, skill);
}
return Array.from(unique.values());
}
/**
* Get the agents installation path for a target
* Returns null for targets that don't support agents
*/
function getAgentsTargetPath(target) {
const projectDir = process.cwd();
switch (target) {
case "cursor":
return node_path_1.default.join(projectDir, ".cursor", "agents");
case "claude":
return node_path_1.default.join(projectDir, ".claude", "agents");
case "codex":
case "windsurf":
// Codex and Windsurf do not support agents
return null;
default:
return null;
}
}
/**
* Write agents to all supported target directories
* Similar to skills, agents are written directly to the agents directory
* with a .aicm.json metadata file tracking which agents are managed
*/
function writeAgentsToTargets(agents, targets) {
if (agents.length === 0)
return;
for (const target of targets) {
const targetAgentsDir = getAgentsTargetPath(target);
if (!targetAgentsDir) {
// Target doesn't support agents
continue;
}
// Ensure the agents directory exists
fs_extra_1.default.ensureDirSync(targetAgentsDir);
// Read existing metadata to clean up old managed agents
const metadataPath = node_path_1.default.join(targetAgentsDir, ".aicm.json");
if (fs_extra_1.default.existsSync(metadataPath)) {
try {
const existingMetadata = fs_extra_1.default.readJsonSync(metadataPath);
// Remove previously managed agents
for (const agentName of existingMetadata.managedAgents || []) {
// Skip invalid names containing path separators
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(targetAgentsDir, agentName + ".md");
if (fs_extra_1.default.existsSync(fullPath)) {
fs_extra_1.default.removeSync(fullPath);
}
}
}
catch (_a) {
// Ignore errors reading metadata
}
}
const managedAgents = [];
for (const agent of agents) {
// Use base name only
const agentName = node_path_1.default.basename(agent.name, node_path_1.default.extname(agent.name));
const agentFile = node_path_1.default.join(targetAgentsDir, agentName + ".md");
fs_extra_1.default.writeFileSync(agentFile, agent.content);
managedAgents.push(agentName);
}
// Write metadata file to track managed agents
const metadata = {
managedAgents,
};
fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
}
}
/**
* Warn about agent name collisions from different presets
*/
function warnPresetAgentCollisions(agents) {
const collisions = new Map();
for (const agent of agents) {
if (!agent.presetName)
continue;
const entry = collisions.get(agent.name);
if (entry) {
entry.presets.add(agent.presetName);
entry.lastPreset = agent.presetName;
}
else {
collisions.set(agent.name, {
presets: new Set([agent.presetName]),
lastPreset: agent.presetName,
});
}
}
for (const [agentName, { presets, lastPreset }] of collisions) {
if (presets.size > 1) {
const presetList = Array.from(presets).sort().join(", ");
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${agentName}" agent (${presetList}). Using definition from ${lastPreset}.`));
}
}
}
/**
* Dedupe agents by name (last one wins)
*/
function dedupeAgentsForInstall(agents) {
const unique = new Map();
for (const agent of agents) {
unique.set(agent.name, agent);
}
return Array.from(unique.values());
}
function warnPresetCommandCollisions(commands) {
const collisions = new Map();
for (const command of commands) {
if (!command.presetName)
continue;
const entry = collisions.get(command.name);
if (entry) {
entry.presets.add(command.presetName);
entry.lastPreset = command.presetName;
}
else {
collisions.set(command.name, {
presets: new Set([command.presetName]),
lastPreset: command.presetName,
});
}
}
for (const [commandName, { presets, lastPreset }] of collisions) {
if (presets.size > 1) {
const presetList = Array.from(presets).sort().join(", ");
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${commandName}" command (${presetList}). Using definition from ${lastPreset}.`));
}
}
}
function dedupeCommandsForInstall(commands) {
const unique = new Map();
for (const command of commands) {
unique.set(command.name, command);
}
return Array.from(unique.values());
}
/**
* 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 hooks to IDE targets
*/
function writeHooksToTargets(hooksConfig, hookFiles, targets, cwd) {
const hasHooks = hooksConfig.hooks && Object.keys(hooksConfig.hooks).length > 0;
if (!hasHooks && hookFiles.length === 0) {
return;
}
for (const target of targets) {
if (target === "cursor") {
(0, hooks_1.writeHooksToCursor)(hooksConfig, hookFiles, cwd);
}
// Other targets do not support hooks yet
}
}
/**
* 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 });
}
/**
* 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,
installedCommandCount: 0,
installedAssetCount: 0,
installedHookCount: 0,
installedSkillCount: 0,
installedAgentCount: 0,
packagesCount: 0,
};
}
const { config, rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = resolvedConfig;
if (config.skipInstall === true) {
return {
success: true,
installedRuleCount: 0,
installedCommandCount: 0,
installedAssetCount: 0,
installedHookCount: 0,
installedSkillCount: 0,
installedAgentCount: 0,
packagesCount: 0,
};
}
warnPresetCommandCollisions(commands);
const commandsToInstall = dedupeCommandsForInstall(commands);
warnPresetSkillCollisions(skills);
const skillsToInstall = dedupeSkillsForInstall(skills);
warnPresetAgentCollisions(agents);
const agentsToInstall = dedupeAgentsForInstall(agents);
try {
if (!options.dryRun) {
writeRulesToTargets(rules, assets, config.targets);
writeCommandsToTargets(commandsToInstall, config.targets);
writeSkillsToTargets(skillsToInstall, config.targets);
writeAgentsToTargets(agentsToInstall, config.targets);
if (mcpServers && Object.keys(mcpServers).length > 0) {
writeMcpServersToTargets(mcpServers, config.targets, cwd);
}
if (hooks && ((0, hooks_1.countHooks)(hooks) > 0 || hookFiles.length > 0)) {
writeHooksToTargets(hooks, hookFiles, config.targets, cwd);
}
}
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
const uniqueSkillCount = skillsToInstall.length;
const uniqueAgentCount = agentsToInstall.length;
return {
success: true,
installedRuleCount: uniqueRuleCount,
installedCommandCount: uniqueCommandCount,
installedAssetCount: assets.length,
installedHookCount: uniqueHookCount,
installedSkillCount: uniqueSkillCount,
installedAgentCount: uniqueAgentCount,
packagesCount: 1,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
installedRuleCount: 0,
installedCommandCount: 0,
installedAssetCount: 0,
installedHookCount: 0,
installedSkillCount: 0,
installedAgentCount: 0,
packagesCount: 0,
};
}
});
}
/**
* 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,
installedCommandCount: 0,
installedAssetCount: 0,
installedHookCount: 0,
installedSkillCount: 0,
installedAgentCount: 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 (0, install_workspaces_1.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 ruleCount = result.installedRuleCount;
const commandCount = result.installedCommandCount;
const hookCount = result.installedHookCount;
const skillCount = result.installedSkillCount;
const agentCount = result.installedAgentCount;
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
const commandMessage = commandCount > 0
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
: null;
const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
const skillMessage = skillCount > 0
? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
: null;
const agentMessage = agentCount > 0
? `${agentCount} agent${agentCount === 1 ? "" : "s"}`
: null;
const countsParts = [];
if (ruleMessage) {
countsParts.push(ruleMessage);
}
if (commandMessage) {
countsParts.push(commandMessage);
}
if (hookMessage) {
countsParts.push(hookMessage);
}
if (skillMessage) {
countsParts.push(skillMessage);
}
if (agentMessage) {
countsParts.push(agentMessage);
}
const countsMessage = countsParts.length > 0
? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
: "0 rules";
if (dryRun) {
if (result.packagesCount > 1) {
console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
}
else {
console.log(`Dry run: validated ${countsMessage}`);
}
}
else if (ruleCount === 0 &&
commandCount === 0 &&
hookCount === 0 &&
skillCount === 0 &&
agentCount === 0) {
console.log("No rules, commands, hooks, skills, or agents installed");
}
else if (result.packagesCount > 1) {
console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
}
else {
console.log(`Successfully installed ${countsMessage}`);
}
}
}