UNPKG

rulesync

Version:

Unified AI rules management CLI tool that generates configuration files for various AI development tools

1,651 lines (1,608 loc) 126 kB
#!/usr/bin/env node import { generateJunieMcp } from "./chunk-TJKD6LEW.js"; import { generateKiroMcp } from "./chunk-QVPD6ENS.js"; import { generateRooMcp } from "./chunk-BEPSWIZC.js"; import { generateAugmentcodeMcp } from "./chunk-ORNO5MOO.js"; import { generateClaudeMcp } from "./chunk-OY6BYYIX.js"; import { generateClineMcp } from "./chunk-UHANRG2O.js"; import { generateCodexMcp } from "./chunk-D7XQ4OHK.js"; import "./chunk-PPAQWVXX.js"; import { generateCopilotMcp } from "./chunk-OXKDEZJK.js"; import { generateCursorMcp } from "./chunk-3PHMFVXP.js"; import { generateGeminiCliMcp } from "./chunk-UZCJNUXO.js"; import { ALL_TOOL_TARGETS, RulesyncTargetsSchema, ToolTargetSchema, ToolTargetsSchema, isToolTarget } from "./chunk-VI6SBYFB.js"; // src/cli/index.ts import { Command } from "commander"; // src/cli/commands/add.ts import { mkdir, writeFile } from "fs/promises"; import * as path from "path"; // src/utils/config.ts function getDefaultConfig() { return { aiRulesDir: ".rulesync", outputPaths: { augmentcode: ".", "augmentcode-legacy": ".", copilot: ".github/instructions", cursor: ".cursor/rules", cline: ".clinerules", claudecode: ".", codexcli: ".", roo: ".roo/rules", geminicli: ".gemini/memories", kiro: ".kiro/steering", junie: "." }, watchEnabled: false, defaultTargets: ALL_TOOL_TARGETS.filter((tool) => tool !== "augmentcode-legacy") }; } function resolveTargets(targets, config) { if (targets.length === 1 && targets[0] === "*") { return config.defaultTargets; } const validatedTargets = ToolTargetsSchema.parse(targets); return validatedTargets; } // src/cli/commands/add.ts function sanitizeFilename(filename) { return filename.endsWith(".md") ? filename.slice(0, -3) : filename; } function generateRuleTemplate(filename) { return `--- root: false targets: ["*"] description: "Rules for ${filename}" globs: [] --- # ${filename.charAt(0).toUpperCase() + filename.slice(1)} Rules Add your rules here. `; } async function addCommand(filename) { try { const config = getDefaultConfig(); const sanitizedFilename = sanitizeFilename(filename); const rulesDir = config.aiRulesDir; const filePath = path.join(rulesDir, `${sanitizedFilename}.md`); await mkdir(rulesDir, { recursive: true }); const template = generateRuleTemplate(sanitizedFilename); await writeFile(filePath, template, "utf8"); console.log(`\u2705 Created rule file: ${filePath}`); console.log(`\u{1F4DD} Edit the file to customize your rules.`); } catch (error) { console.error( `\u274C Failed to create rule file: ${error instanceof Error ? error.message : String(error)}` ); process.exit(3); } } // src/cli/commands/config.ts import { writeFileSync } from "fs"; import path2 from "path"; // src/types/claudecode.ts import { z } from "zod/mini"; var ClaudeSettingsSchema = z.looseObject({ permissions: z._default( z.looseObject({ deny: z._default(z.array(z.string()), []) }), { deny: [] } ) }); // src/types/config.ts import { z as z2 } from "zod/mini"; var ConfigSchema = z2.object({ aiRulesDir: z2.string(), outputPaths: z2.record(ToolTargetSchema, z2.string()), watchEnabled: z2.boolean(), defaultTargets: ToolTargetsSchema }); // src/types/config-options.ts import { z as z3 } from "zod/mini"; var OutputPathsSchema = z3.object({ augmentcode: z3.optional(z3.string()), "augmentcode-legacy": z3.optional(z3.string()), copilot: z3.optional(z3.string()), cursor: z3.optional(z3.string()), cline: z3.optional(z3.string()), claudecode: z3.optional(z3.string()), codexcli: z3.optional(z3.string()), roo: z3.optional(z3.string()), geminicli: z3.optional(z3.string()), kiro: z3.optional(z3.string()), junie: z3.optional(z3.string()) }); var ConfigOptionsSchema = z3.object({ aiRulesDir: z3.optional(z3.string()), outputPaths: z3.optional(OutputPathsSchema), watchEnabled: z3.optional(z3.boolean()), defaultTargets: z3.optional(ToolTargetsSchema), targets: z3.optional(z3.array(ToolTargetSchema)), exclude: z3.optional(z3.array(ToolTargetSchema)), verbose: z3.optional(z3.boolean()), delete: z3.optional(z3.boolean()), baseDir: z3.optional(z3.union([z3.string(), z3.array(z3.string())])), watch: z3.optional( z3.object({ enabled: z3.optional(z3.boolean()), interval: z3.optional(z3.number()), ignore: z3.optional(z3.array(z3.string())) }) ) }); var MergedConfigSchema = z3.object({ aiRulesDir: z3.string(), outputPaths: z3.record(ToolTargetSchema, z3.string()), watchEnabled: z3.boolean(), defaultTargets: ToolTargetsSchema, targets: z3.optional(z3.array(ToolTargetSchema)), exclude: z3.optional(z3.array(ToolTargetSchema)), verbose: z3.optional(z3.boolean()), delete: z3.optional(z3.boolean()), baseDir: z3.optional(z3.union([z3.string(), z3.array(z3.string())])), configPath: z3.optional(z3.string()), watch: z3.optional( z3.object({ enabled: z3.optional(z3.boolean()), interval: z3.optional(z3.number()), ignore: z3.optional(z3.array(z3.string())) }) ) }); // src/types/mcp.ts import { z as z4 } from "zod/mini"; var McpTransportTypeSchema = z4.enum(["stdio", "sse", "http"]); var McpServerBaseSchema = z4.object({ command: z4.optional(z4.string()), args: z4.optional(z4.array(z4.string())), url: z4.optional(z4.string()), httpUrl: z4.optional(z4.string()), env: z4.optional(z4.record(z4.string(), z4.string())), disabled: z4.optional(z4.boolean()), networkTimeout: z4.optional(z4.number()), timeout: z4.optional(z4.number()), trust: z4.optional(z4.boolean()), cwd: z4.optional(z4.string()), transport: z4.optional(McpTransportTypeSchema), type: z4.optional(z4.enum(["sse", "streamable-http"])), alwaysAllow: z4.optional(z4.array(z4.string())), tools: z4.optional(z4.array(z4.string())), kiroAutoApprove: z4.optional(z4.array(z4.string())), kiroAutoBlock: z4.optional(z4.array(z4.string())), headers: z4.optional(z4.record(z4.string(), z4.string())) }); var RulesyncMcpServerSchema = z4.extend(McpServerBaseSchema, { targets: z4.optional(RulesyncTargetsSchema) }); var McpConfigSchema = z4.object({ mcpServers: z4.record(z4.string(), McpServerBaseSchema) }); var RulesyncMcpConfigSchema = z4.object({ mcpServers: z4.record(z4.string(), RulesyncMcpServerSchema) }); // src/types/rules.ts import { z as z5 } from "zod/mini"; var RuleFrontmatterSchema = z5.object({ root: z5.boolean(), targets: RulesyncTargetsSchema, description: z5.string(), globs: z5.array(z5.string()), cursorRuleType: z5.optional(z5.enum(["always", "manual", "specificFiles", "intelligently"])), tags: z5.optional(z5.array(z5.string())) }); var ParsedRuleSchema = z5.object({ frontmatter: RuleFrontmatterSchema, content: z5.string(), filename: z5.string(), filepath: z5.string() }); var GeneratedOutputSchema = z5.object({ tool: ToolTargetSchema, filepath: z5.string(), content: z5.string() }); var GenerateOptionsSchema = z5.object({ targetTools: z5.optional(ToolTargetsSchema), outputDir: z5.optional(z5.string()), watch: z5.optional(z5.boolean()) }); // src/utils/config-loader.ts import { loadConfig as loadC12Config } from "c12"; import { $ZodError } from "zod/v4/core"; var MODULE_NAME = "rulesync"; async function loadConfig(options = {}) { const defaultConfig = getDefaultConfig(); if (options.noConfig) { return { config: defaultConfig, isEmpty: true }; } try { const loadOptions = { name: MODULE_NAME, cwd: options.cwd || process.cwd(), rcFile: false, // Disable rc file lookup configFile: "rulesync", // Will look for rulesync.jsonc, rulesync.ts, etc. defaults: defaultConfig }; if (options.configPath) { loadOptions.configFile = options.configPath; } const { config, configFile } = await loadC12Config(loadOptions); if (!config || Object.keys(config).length === 0) { return { config: defaultConfig, isEmpty: true }; } try { ConfigOptionsSchema.parse(config); } catch (error) { if (error instanceof $ZodError) { const issues = error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"); throw new Error(`Invalid configuration in ${configFile}: ${issues}`); } throw error; } const processedConfig = postProcessConfig(config); const result = { config: processedConfig, isEmpty: false }; if (configFile) { result.filepath = configFile; } return result; } catch (error) { throw new Error( `Failed to load configuration: ${error instanceof Error ? error.message : String(error)}` ); } } function postProcessConfig(config) { const processed = { ...config }; if (processed.baseDir && !Array.isArray(processed.baseDir)) { processed.baseDir = [processed.baseDir]; } if (config.targets || config.exclude) { const baseTargets = config.targets || processed.defaultTargets; if (config.exclude && config.exclude.length > 0) { processed.defaultTargets = baseTargets.filter( (target) => config.exclude && !config.exclude.includes(target) ); } else { processed.defaultTargets = baseTargets; } } return processed; } function generateMinimalConfig(options) { if (!options || Object.keys(options).length === 0) { return generateSampleConfig(); } const lines = ["{"]; if (options.targets || options.exclude) { lines.push(` // Available tools: ${ALL_TOOL_TARGETS.join(", ")}`); } if (options.targets) { lines.push(` "targets": ${JSON.stringify(options.targets)}`); } if (options.exclude) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push(` "exclude": ${JSON.stringify(options.exclude)}`); } if (options.aiRulesDir) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push(` "aiRulesDir": "${options.aiRulesDir}"`); } if (options.outputPaths) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push( ` "outputPaths": ${JSON.stringify(options.outputPaths, null, 4).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n")}` ); } if (options.baseDir) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push(` "baseDir": ${JSON.stringify(options.baseDir)}`); } if (options.delete !== void 0) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push(` "delete": ${options.delete}`); } if (options.verbose !== void 0) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push(` "verbose": ${options.verbose}`); } if (options.watch) { const comma = lines.length > 1 ? "," : ""; if (comma) lines[lines.length - 1] += comma; lines.push( ` "watch": ${JSON.stringify(options.watch, null, 4).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n")}` ); } lines.push("}"); return lines.join("\n"); } function generateSampleConfig(options) { const targets = options?.targets || ALL_TOOL_TARGETS; const excludeValue = options?.exclude ? JSON.stringify(options.exclude) : null; const aiRulesDir = options?.aiRulesDir || null; const baseDir = options?.baseDir || null; const deleteFlag = options?.delete || false; const verbose = options?.verbose !== void 0 ? options.verbose : true; return `{ // List of tools to generate configurations for // Available: ${ALL_TOOL_TARGETS.join(", ")} "targets": ${JSON.stringify(targets)}, // Tools to exclude from generation (overrides targets) ${excludeValue ? `"exclude": ${excludeValue},` : '// "exclude": ["roo"],'} ${aiRulesDir ? ` // Directory containing AI rule files "aiRulesDir": "${aiRulesDir}",` : ""} // Custom output paths for specific tools "outputPaths": { "copilot": ".github/copilot-instructions.md" }, ${baseDir ? ` // Base directory for generation "baseDir": "${baseDir}",` : ` // Base directory or directories for generation // "baseDir": "./packages", // "baseDir": ["./packages/frontend", "./packages/backend"],`} // Delete existing files before generating "delete": ${deleteFlag}, // Enable verbose output "verbose": ${verbose}, // Watch configuration "watch": { "enabled": false, "interval": 1000, "ignore": ["node_modules/**", "dist/**"] } } `; } function mergeWithCliOptions(config, cliOptions) { const merged = { ...config }; if (cliOptions.verbose !== void 0) { merged.verbose = cliOptions.verbose; } if (cliOptions.delete !== void 0) { merged.delete = cliOptions.delete; } if (cliOptions.baseDirs && cliOptions.baseDirs.length > 0) { merged.baseDir = cliOptions.baseDirs; } if (cliOptions.tools && cliOptions.tools.length > 0) { merged.defaultTargets = cliOptions.tools; merged.exclude = void 0; } return merged; } // src/utils/error.ts function getErrorMessage(error) { return error instanceof Error ? error.message : String(error); } function formatErrorWithContext(error, context) { const errorMessage = getErrorMessage(error); return `${context}: ${errorMessage}`; } function createErrorResult(error, context) { const errorMessage = context ? formatErrorWithContext(error, context) : getErrorMessage(error); return { success: false, error: errorMessage }; } function createSuccessResult(result) { return { success: true, result }; } async function safeAsyncOperation(operation, errorContext) { try { const result = await operation(); return createSuccessResult(result); } catch (error) { return createErrorResult(error, errorContext); } } // src/utils/file.ts import { mkdir as mkdir2, readdir, readFile, rm, stat, writeFile as writeFile2 } from "fs/promises"; import { dirname, join as join2 } from "path"; async function ensureDir(dirPath) { try { await stat(dirPath); } catch { await mkdir2(dirPath, { recursive: true }); } } function resolvePath(relativePath, baseDir) { return baseDir ? join2(baseDir, relativePath) : relativePath; } async function readFileContent(filepath) { return readFile(filepath, "utf-8"); } async function writeFileContent(filepath, content) { await ensureDir(dirname(filepath)); await writeFile2(filepath, content, "utf-8"); } async function fileExists(filepath) { try { await stat(filepath); return true; } catch { return false; } } async function findFiles(dir, extension = ".md") { try { const files = await readdir(dir); return files.filter((file) => file.endsWith(extension)).map((file) => join2(dir, file)); } catch { return []; } } async function removeDirectory(dirPath) { const dangerousPaths = [".", "/", "~", "src", "node_modules"]; if (dangerousPaths.includes(dirPath) || dirPath === "") { console.warn(`Skipping deletion of dangerous path: ${dirPath}`); return; } try { if (await fileExists(dirPath)) { await rm(dirPath, { recursive: true, force: true }); } } catch (error) { console.warn(`Failed to remove directory ${dirPath}:`, error); } } async function removeFile(filepath) { try { if (await fileExists(filepath)) { await rm(filepath); } } catch (error) { console.warn(`Failed to remove file ${filepath}:`, error); } } async function removeClaudeGeneratedFiles() { const filesToRemove = ["CLAUDE.md", ".claude/memories"]; for (const fileOrDir of filesToRemove) { if (fileOrDir.endsWith("/memories")) { await removeDirectory(fileOrDir); } else { await removeFile(fileOrDir); } } } // src/utils/rules.ts function isToolSpecificRule(rule, targetTool) { const filename = rule.filename; const toolPatterns = { "augmentcode-legacy": /^specification-augmentcode-legacy-/i, augmentcode: /^specification-augmentcode-/i, copilot: /^specification-copilot-/i, cursor: /^specification-cursor-/i, cline: /^specification-cline-/i, claudecode: /^specification-claudecode-/i, roo: /^specification-roo-/i, geminicli: /^specification-geminicli-/i, kiro: /^specification-kiro-/i }; for (const [tool, pattern] of Object.entries(toolPatterns)) { if (pattern.test(filename)) { return tool === targetTool; } } return true; } // src/cli/commands/config.ts async function configCommand(options = {}) { if (options.init) { await initConfig(options); return; } await showConfig(); } async function showConfig() { console.log("Loading configuration...\n"); try { const result = await loadConfig(); if (result.isEmpty) { console.log("No configuration file found. Using default configuration.\n"); } else { console.log(`Configuration loaded from: ${result.filepath} `); } console.log("Current configuration:"); console.log("====================="); const config = result.config; console.log(` AI Rules Directory: ${config.aiRulesDir}`); console.log(` Default Targets: ${config.defaultTargets.join(", ")}`); if (config.exclude && config.exclude.length > 0) { console.log(`Excluded Targets: ${config.exclude.join(", ")}`); } console.log("\nOutput Paths:"); for (const [tool, outputPath] of Object.entries(config.outputPaths)) { console.log(` ${tool}: ${outputPath}`); } if (config.baseDir) { const dirs = Array.isArray(config.baseDir) ? config.baseDir : [config.baseDir]; console.log(` Base Directories: ${dirs.join(", ")}`); } console.log(` Verbose: ${config.verbose || false}`); console.log(`Delete before generate: ${config.delete || false}`); if (config.watch) { console.log("\nWatch Configuration:"); console.log(` Enabled: ${config.watch.enabled || false}`); if (config.watch.interval) { console.log(` Interval: ${config.watch.interval}ms`); } if (config.watch.ignore && config.watch.ignore.length > 0) { console.log(` Ignore patterns: ${config.watch.ignore.join(", ")}`); } } console.log("\nTip: Use 'rulesync config init' to create a configuration file."); } catch (error) { console.error( "\u274C Failed to load configuration:", error instanceof Error ? error.message : String(error) ); process.exit(1); } } var FORMAT_CONFIG = { jsonc: { filename: "rulesync.jsonc", generator: generateJsoncConfig }, ts: { filename: "rulesync.ts", generator: generateTsConfig } }; async function initConfig(options) { const validFormats = Object.keys(FORMAT_CONFIG); const selectedFormat = options.format || "jsonc"; if (!validFormats.includes(selectedFormat)) { console.error( `\u274C Invalid format: ${selectedFormat}. Valid formats are: ${validFormats.join(", ")}` ); process.exit(1); } const formatConfig = FORMAT_CONFIG[selectedFormat]; const filename = formatConfig.filename; const configOptions = {}; if (options.targets) { const targets = options.targets.split(",").map((t) => t.trim()); const validTargets = []; for (const target of targets) { const result = ToolTargetSchema.safeParse(target); if (result.success) { validTargets.push(result.data); } else { console.error(`\u274C Invalid target: ${target}`); process.exit(1); } } configOptions.targets = validTargets; } if (options.exclude) { const excludes = options.exclude.split(",").map((t) => t.trim()); const validExcludes = []; for (const exclude of excludes) { const result = ToolTargetSchema.safeParse(exclude); if (result.success) { validExcludes.push(result.data); } else { console.error(`\u274C Invalid exclude target: ${exclude}`); process.exit(1); } } configOptions.exclude = validExcludes; } if (options.aiRulesDir) { configOptions.aiRulesDir = options.aiRulesDir; } if (options.baseDir) { configOptions.baseDir = options.baseDir; } if (options.verbose !== void 0) { configOptions.verbose = options.verbose; } if (options.delete !== void 0) { configOptions.delete = options.delete; } const content = formatConfig.generator(configOptions); const filepath = path2.join(process.cwd(), filename); try { const fs2 = await import("fs/promises"); await fs2.access(filepath); console.error(`\u274C Configuration file already exists: ${filepath}`); console.log("Remove the existing file or choose a different format."); process.exit(1); } catch { } try { writeFileSync(filepath, content, "utf-8"); console.log(`\u2705 Created configuration file: ${filepath}`); console.log("\nYou can now customize the configuration to fit your needs."); console.log("Run 'rulesync generate' to use the new configuration."); } catch (error) { console.error( `\u274C Failed to create configuration file: ${error instanceof Error ? error.message : String(error)}` ); process.exit(1); } } function generateJsoncConfig(options) { if (options && Object.keys(options).length > 0) { return generateMinimalConfig(options); } return generateSampleConfig(options); } function generateTsConfig(options) { if (!options || Object.keys(options).length === 0) { return `import type { ConfigOptions } from "rulesync"; const config: ConfigOptions = { // List of tools to generate configurations for // Available: ${ALL_TOOL_TARGETS.join(", ")} targets: ${JSON.stringify(ALL_TOOL_TARGETS)}, // Custom output paths for specific tools // outputPaths: { // copilot: ".github/copilot-instructions.md", // }, // Delete existing files before generating // delete: false, // Enable verbose output verbose: true, }; export default config;`; } const configLines = []; if (options.targets) { configLines.push(` targets: ${JSON.stringify(options.targets)}`); } if (options.exclude) { configLines.push(` exclude: ${JSON.stringify(options.exclude)}`); } if (options.aiRulesDir) { configLines.push(` aiRulesDir: "${options.aiRulesDir}"`); } if (options.outputPaths) { const pathsStr = JSON.stringify(options.outputPaths, null, 4).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n"); configLines.push(` outputPaths: ${pathsStr}`); } if (options.baseDir) { configLines.push(` baseDir: ${JSON.stringify(options.baseDir)}`); } if (options.delete !== void 0) { configLines.push(` delete: ${options.delete}`); } if (options.verbose !== void 0) { configLines.push(` verbose: ${options.verbose}`); } if (options.watch) { const watchStr = JSON.stringify(options.watch, null, 4).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n"); configLines.push(` watch: ${watchStr}`); } const configContent = `import type { ConfigOptions } from "rulesync"; const config: ConfigOptions = { ${configLines.join(",\n")}, }; export default config; `; return configContent; } // src/cli/commands/generate.ts import { join as join13 } from "path"; // src/generators/ignore/shared-factory.ts import { join as join3 } from "path"; // src/generators/ignore/shared-helpers.ts function extractIgnorePatternsFromRules(rules) { const patterns = []; for (const rule of rules) { if (rule.frontmatter.globs && rule.frontmatter.globs.length > 0) { for (const glob of rule.frontmatter.globs) { if (shouldExcludeFromAI(glob)) { patterns.push(`# Exclude: ${rule.frontmatter.description}`); patterns.push(glob); } } } const contentPatterns = extractIgnorePatternsFromContent(rule.content); patterns.push(...contentPatterns); } return patterns; } function shouldExcludeFromAI(glob) { const excludePatterns = [ // Large generated files that slow indexing "**/assets/generated/**", "**/public/build/**", // Test fixtures with potentially sensitive data "**/tests/fixtures/**", "**/test/fixtures/**", "**/*.fixture.*", // Build outputs that provide little value for AI context "**/dist/**", "**/build/**", "**/coverage/**", // Configuration that might contain sensitive data "**/config/production/**", "**/config/secrets/**", "**/config/prod/**", "**/deploy/prod/**", "**/*.prod.*", // Internal documentation that might be sensitive "**/internal/**", "**/internal-docs/**", "**/proprietary/**", "**/personal-notes/**", "**/private/**", "**/confidential/**" ]; return excludePatterns.some((pattern) => { const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")); return regex.test(glob); }); } function extractIgnorePatternsFromContent(content) { const patterns = []; const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith("# IGNORE:") || trimmed.startsWith("# aiignore:")) { const pattern = trimmed.replace(/^# (IGNORE|aiignore):\s*/, "").trim(); if (pattern) { patterns.push(pattern); } } if (trimmed.startsWith("# AUGMENT_IGNORE:") || trimmed.startsWith("# augmentignore:")) { const pattern = trimmed.replace(/^# (AUGMENT_IGNORE|augmentignore):\s*/, "").trim(); if (pattern) { patterns.push(pattern); } } if (trimmed.startsWith("# AUGMENT_INCLUDE:") || trimmed.startsWith("# augmentinclude:")) { const pattern = trimmed.replace(/^# (AUGMENT_INCLUDE|augmentinclude):\s*/, "").trim(); if (pattern) { patterns.push(`!${pattern}`); } } if (trimmed.includes("exclude") || trimmed.includes("ignore")) { const matches = trimmed.match(/['"`]([^'"`]+\.(log|tmp|cache|temp))['"`]/g); if (matches) { patterns.push(...matches.map((m) => m.replace(/['"`]/g, ""))); } } } return patterns; } function extractAugmentCodeIgnorePatternsFromContent(content) { const patterns = []; const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith("# AUGMENT_IGNORE:") || trimmed.startsWith("# augmentignore:")) { const pattern = trimmed.replace(/^# (AUGMENT_IGNORE|augmentignore):\s*/, "").trim(); if (pattern) { patterns.push(pattern); } } if (trimmed.startsWith("# AUGMENT_INCLUDE:") || trimmed.startsWith("# augmentinclude:")) { const pattern = trimmed.replace(/^# (AUGMENT_INCLUDE|augmentinclude):\s*/, "").trim(); if (pattern) { patterns.push(`!${pattern}`); } } if (trimmed.includes("large file") || trimmed.includes("binary") || trimmed.includes("media")) { const regex = /['"`]([^'"`]+\.(mp4|avi|zip|tar\.gz|rar|pdf|doc|xlsx))['"`]/g; let match; while ((match = regex.exec(trimmed)) !== null) { if (match[1]) { patterns.push(match[1]); } } } } return patterns; } // src/generators/ignore/shared-factory.ts function generateIgnoreFile(rules, config, ignoreConfig, baseDir) { const outputs = []; const content = generateIgnoreContent(rules, ignoreConfig); const outputPath = baseDir || process.cwd(); const filepath = join3(outputPath, ignoreConfig.filename); outputs.push({ tool: ignoreConfig.tool, filepath, content }); return outputs; } function generateIgnoreContent(rules, config) { const lines = []; lines.push(...config.header); lines.push(""); if (config.includeCommonPatterns) { lines.push(...getCommonIgnorePatterns()); } if (config.corePatterns.length > 0) { lines.push(...config.corePatterns); lines.push(""); } const rulePatterns = extractIgnorePatternsFromRules(rules); const customPatterns = config.customPatternProcessor ? config.customPatternProcessor(rules) : []; const allPatterns = [...rulePatterns, ...customPatterns]; if (allPatterns.length > 0) { const headerText = config.projectPatternsHeader || "# \u2500\u2500\u2500\u2500\u2500 Project-specific exclusions from rulesync rules \u2500\u2500\u2500\u2500\u2500"; lines.push(headerText); lines.push(...allPatterns); lines.push(""); } return lines.join("\n"); } function getCommonIgnorePatterns() { return [ "# \u2500\u2500\u2500\u2500\u2500 Source Control Metadata \u2500\u2500\u2500\u2500\u2500", ".git/", ".svn/", ".hg/", ".idea/", "*.iml", ".vscode/settings.json", "", "# \u2500\u2500\u2500\u2500\u2500 Build Artifacts \u2500\u2500\u2500\u2500\u2500", "/out/", "/dist/", "/target/", "/build/", "*.class", "*.jar", "*.war", "", "# \u2500\u2500\u2500\u2500\u2500 Secrets & Credentials \u2500\u2500\u2500\u2500\u2500", "# Environment files", ".env", ".env.*", "!.env.example", "", "# Key material", "*.pem", "*.key", "*.crt", "*.p12", "*.pfx", "*.der", "id_rsa*", "id_dsa*", "*.ppk", "", "# Cloud and service configs", "aws-credentials.json", "gcp-service-account*.json", "azure-credentials.json", "secrets/**", "config/secrets/", "**/secrets/", "", "# Database credentials", "database.yml", "**/database/config.*", "", "# API keys and tokens", "**/apikeys/", "**/*_token*", "**/*_secret*", "**/*api_key*", "", "# \u2500\u2500\u2500\u2500\u2500 Infrastructure & Deployment \u2500\u2500\u2500\u2500\u2500", "# Terraform state", "*.tfstate", "*.tfstate.*", ".terraform/", "", "# Kubernetes secrets", "**/k8s/**/secret*.yaml", "**/kubernetes/**/secret*.yaml", "", "# Docker secrets", "docker-compose.override.yml", "**/docker/secrets/", "", "# \u2500\u2500\u2500\u2500\u2500 Logs & Runtime Data \u2500\u2500\u2500\u2500\u2500", "*.log", "*.tmp", "*.cache", "logs/", "/var/log/", "coverage/", ".nyc_output/", "", "# \u2500\u2500\u2500\u2500\u2500 Large Data Files \u2500\u2500\u2500\u2500\u2500", "*.csv", "*.xlsx", "*.sqlite", "*.db", "*.dump", "data/", "datasets/", "", "# \u2500\u2500\u2500\u2500\u2500 Node.js Specific \u2500\u2500\u2500\u2500\u2500", "node_modules/", ".pnpm-store/", ".yarn/", ".next/", ".nuxt/", ".cache/", ".parcel-cache/", "", "# \u2500\u2500\u2500\u2500\u2500 Python Specific \u2500\u2500\u2500\u2500\u2500", "__pycache__/", "*.pyc", "*.pyo", "*.pyd", ".Python", "venv/", ".venv/", "env/", ".env/", "", "# \u2500\u2500\u2500\u2500\u2500 Java Specific \u2500\u2500\u2500\u2500\u2500", "*.class", "*.jar", "*.war", "target/", "" ]; } var ignoreConfigs = { junie: { tool: "junie", filename: ".aiignore", header: [ "# Generated by rulesync - JetBrains Junie AI ignore file", "# This file controls which files the AI can access automatically", "# AI must ask before reading or editing matched files/directories" ], corePatterns: [ "# \u2500\u2500\u2500\u2500\u2500 Allow specific source files (uncomment as needed) \u2500\u2500\u2500\u2500\u2500", "# !src/**/*.ts", "# !src/**/*.js", "# !lib/**/*.py", "# !src/main/**/*.java" ], includeCommonPatterns: true }, kiro: { tool: "kiro", filename: ".aiignore", header: [ "# Generated by rulesync - Kiro AI-specific exclusions", "# This file excludes files that can be in Git but shouldn't be read by the AI" ], corePatterns: [ "# Data files AI shouldn't process", "*.csv", "*.tsv", "*.sqlite", "*.db", "", "# Large binary files", "*.zip", "*.tar.gz", "*.rar", "", "# Sensitive documentation", "internal-docs/", "confidential/", "", "# Test data that might confuse AI", "test/fixtures/large-*.json", "benchmark-results/", "", "# Reinforce critical exclusions from .gitignore", "*.pem", "*.key", ".env*" ], includeCommonPatterns: false, projectPatternsHeader: "# Project-specific exclusions from rulesync rules" }, augmentcode: { tool: "augmentcode", filename: ".augmentignore", header: [ "# Generated by rulesync - AugmentCode ignore patterns", "# AugmentCode uses a two-tier approach: .gitignore first, then .augmentignore", "# This file provides Augment-specific exclusions and re-inclusions" ], corePatterns: [ "# Security and Secrets (critical exclusions)", "# Environment files", ".env*", "", "# Private keys and certificates", "*.pem", "*.key", "*.p12", "*.crt", "*.der", "", "# SSH keys", "id_rsa*", "id_dsa*", "", "# AWS credentials", ".aws/", "aws-exports.js", "", "# API keys and tokens", "**/apikeys/", "**/*_token*", "**/*_secret*", "", "# Build Artifacts and Dependencies", "# Build outputs", "dist/", "build/", "out/", "target/", "", "# Dependencies", "node_modules/", "venv/", "*.egg-info/", "", "# Logs", "*.log", "logs/", "", "# Temporary files", "*.tmp", "*.swp", "*.swo", "*~", "", "# Large Files and Media", "# Binary files", "*.jar", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.mp4", "*.avi", "*.zip", "*.tar.gz", "*.rar", "", "# Database files", "*.sqlite", "*.db", "*.mdb", "", "# Data files", "*.csv", "*.tsv", "*.xlsx", "", "# Performance Optimization", "# Exclude files that are too large for effective AI processing", "**/*.{mp4,avi,mov,mkv}", "**/*.{zip,tar,gz,rar}", "**/*.{pdf,doc,docx}", "**/logs/**/*.log", "", "# But include small configuration files", "!**/config.{json,yaml,yml}", "", "# Team Collaboration", "# Exclude personal IDE settings", ".vscode/settings.json", ".idea/workspace.xml", "", "# But include shared team settings", "!.vscode/extensions.json", "!.idea/codeStyles/", "", "# Exclude test fixtures with sensitive data", "tests/fixtures/real-data/**", "", "# Re-include important documentation", "!vendor/*/README.md", "!third-party/*/LICENSE" ], includeCommonPatterns: false, projectPatternsHeader: "# Project-specific patterns from rulesync rules", customPatternProcessor: (rules) => { const augmentPatterns = []; for (const rule of rules) { augmentPatterns.push(...extractAugmentCodeIgnorePatternsFromContent(rule.content)); } return augmentPatterns; } }, codexcli: { tool: "codexcli", filename: ".codexignore", header: [ "# Generated by rulesync - OpenAI Codex CLI ignore file", "# This file controls which files are excluded from Codex CLI's AI context", "# Note: .codexignore is a community-requested feature (GitHub Issue #205)", "# Currently using proposed syntax based on .gitignore patterns" ], corePatterns: [ "# \u2500\u2500\u2500\u2500\u2500 Security & Credentials (Critical) \u2500\u2500\u2500\u2500\u2500", "# Environment files", ".env", ".env.*", "!.env.example", "", "# Private keys and certificates", "*.key", "*.pem", "*.p12", "*.pfx", "*.der", "*.crt", "", "# SSH keys", "id_rsa*", "id_dsa*", "*.ppk", "", "# API keys and secrets", "api-keys.json", "credentials.json", "secrets.json", "**/apikeys/", "**/*_token*", "**/*_secret*", "**/*api_key*", "", "# Cloud provider credentials", "aws-credentials.json", "gcp-service-account*.json", "azure-credentials.json", ".aws/", "", "# \u2500\u2500\u2500\u2500\u2500 Database & Infrastructure Secrets \u2500\u2500\u2500\u2500\u2500", "# Database configuration", "*.db", "*.sqlite", "*.sqlite3", "database.yml", "**/database/config.*", "", "# Infrastructure as Code secrets", "*.tfstate", "*.tfstate.*", "terraform.tfvars", "secrets.auto.tfvars", ".terraform/", "", "# Kubernetes secrets", "**/k8s/**/secret*.yaml", "**/kubernetes/**/secret*.yaml", "", "# \u2500\u2500\u2500\u2500\u2500 Business Sensitive Data \u2500\u2500\u2500\u2500\u2500", "# Sensitive directories", "secrets/", "private/", "confidential/", "internal-docs/", "company-secrets/", "", "# Customer and personal data", "customer-data/", "pii/", "personal-data/", "**/*customer*.csv", "**/*personal*.json", "", "# \u2500\u2500\u2500\u2500\u2500 Build Artifacts & Large Files \u2500\u2500\u2500\u2500\u2500", "# Build outputs (may contain embedded secrets)", "dist/", "build/", "out/", ".next/", ".nuxt/", "", "# Large files that slow down AI processing", "*.zip", "*.tar.gz", "*.rar", "**/*.{mp4,avi,mov,mkv}", "**/*.{pdf,doc,docx}", "", "# Data files", "*.csv", "*.tsv", "*.xlsx", "data/", "datasets/", "", "# \u2500\u2500\u2500\u2500\u2500 Development Environment \u2500\u2500\u2500\u2500\u2500", "# Personal IDE settings", ".vscode/settings.json", ".idea/workspace.xml", "", "# Temporary files", "*.swp", "*.swo", "*~", "*.tmp", "", "# Test data with sensitive content", "test-data/sensitive/", "tests/fixtures/real-data/**", "", "# \u2500\u2500\u2500\u2500\u2500 Logs & Runtime Data \u2500\u2500\u2500\u2500\u2500", "*.log", "logs/", ".cache/", "", "# \u2500\u2500\u2500\u2500\u2500 Re-include Important Files \u2500\u2500\u2500\u2500\u2500", "# Allow configuration examples and documentation", "!secrets/README.md", "!config/*.example.*", "!docs/**/*.md" ], includeCommonPatterns: false, projectPatternsHeader: "# \u2500\u2500\u2500\u2500\u2500 Project-specific patterns from rulesync rules \u2500\u2500\u2500\u2500\u2500" } }; // src/generators/ignore/augmentcode.ts async function generateAugmentCodeIgnoreFiles(rules, config, baseDir) { return generateIgnoreFile(rules, config, ignoreConfigs.augmentcode, baseDir); } // src/generators/ignore/junie.ts async function generateJunieIgnoreFiles(rules, config, baseDir) { return generateIgnoreFile(rules, config, ignoreConfigs.junie, baseDir); } // src/generators/ignore/kiro.ts async function generateKiroIgnoreFiles(rules, config, baseDir) { return generateIgnoreFile(rules, config, ignoreConfigs.kiro, baseDir); } // src/generators/rules/augmentcode.ts import { join as join6 } from "path"; // src/generators/rules/shared-helpers.ts import { join as join5 } from "path"; // src/utils/ignore.ts import { join as join4 } from "path"; import micromatch from "micromatch"; var cachedIgnorePatterns = null; async function loadIgnorePatterns(baseDir = process.cwd()) { if (cachedIgnorePatterns) { return cachedIgnorePatterns; } const ignorePath = join4(baseDir, ".rulesyncignore"); if (!await fileExists(ignorePath)) { cachedIgnorePatterns = { patterns: [] }; return cachedIgnorePatterns; } try { const content = await readFileContent(ignorePath); const patterns = parseIgnoreFile(content); cachedIgnorePatterns = { patterns }; return cachedIgnorePatterns; } catch (error) { console.warn(`Failed to read .rulesyncignore: ${error}`); cachedIgnorePatterns = { patterns: [] }; return cachedIgnorePatterns; } } function parseIgnoreFile(content) { return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")); } function isFileIgnored(filepath, ignorePatterns) { if (ignorePatterns.length === 0) { return false; } const negationPatterns = ignorePatterns.filter((p) => p.startsWith("!")); const positivePatterns = ignorePatterns.filter((p) => !p.startsWith("!")); const isIgnored = positivePatterns.length > 0 && micromatch.isMatch(filepath, positivePatterns, { dot: true }); if (isIgnored && negationPatterns.length > 0) { const negationPatternsWithoutPrefix = negationPatterns.map((p) => p.substring(1)); return !micromatch.isMatch(filepath, negationPatternsWithoutPrefix, { dot: true }); } return isIgnored; } function filterIgnoredFiles(files, ignorePatterns) { if (ignorePatterns.length === 0) { return files; } return files.filter((file) => !isFileIgnored(file, ignorePatterns)); } // src/generators/rules/shared-helpers.ts function resolveOutputDir(config, tool, baseDir) { return resolvePath(config.outputPaths[tool], baseDir); } function createOutputsArray() { return []; } function addOutput(outputs, tool, config, baseDir, relativePath, content) { const outputDir = resolveOutputDir(config, tool, baseDir); outputs.push({ tool, filepath: join5(outputDir, relativePath), content }); } async function generateRulesConfig(rules, config, generatorConfig, baseDir) { const outputs = []; for (const rule of rules) { const content = generatorConfig.generateContent(rule); const outputDir = resolveOutputDir(config, generatorConfig.tool, baseDir); const filepath = generatorConfig.pathResolver ? generatorConfig.pathResolver(rule, outputDir) : join5(outputDir, `${rule.filename}${generatorConfig.fileExtension}`); outputs.push({ tool: generatorConfig.tool, filepath, content }); } const ignorePatterns = await loadIgnorePatterns(baseDir); if (ignorePatterns.patterns.length > 0) { const ignorePath = resolvePath(generatorConfig.ignoreFileName, baseDir); const ignoreContent = generateIgnoreFile2(ignorePatterns.patterns, generatorConfig.tool); outputs.push({ tool: generatorConfig.tool, filepath: ignorePath, content: ignoreContent }); } return outputs; } async function generateComplexRules(rules, config, generatorConfig, baseDir) { const outputs = []; const rootRules = rules.filter((r) => r.frontmatter.root === true); const detailRules = rules.filter((r) => r.frontmatter.root === false); const rootRule = rootRules[0]; if (generatorConfig.generateDetailContent && generatorConfig.detailSubDir) { for (const rule of detailRules) { const content = generatorConfig.generateDetailContent(rule); const filepath = resolvePath( join5(generatorConfig.detailSubDir, `${rule.filename}.md`), baseDir ); outputs.push({ tool: generatorConfig.tool, filepath, content }); } } if (generatorConfig.generateRootContent && generatorConfig.rootFilePath) { const rootContent = generatorConfig.generateRootContent(rootRule, detailRules, baseDir); const rootFilepath = resolvePath(generatorConfig.rootFilePath, baseDir); outputs.push({ tool: generatorConfig.tool, filepath: rootFilepath, content: rootContent }); } const ignorePatterns = await loadIgnorePatterns(baseDir); if (ignorePatterns.patterns.length > 0) { const ignorePath = resolvePath(generatorConfig.ignoreFileName, baseDir); const ignoreContent = generateIgnoreFile2(ignorePatterns.patterns, generatorConfig.tool); outputs.push({ tool: generatorConfig.tool, filepath: ignorePath, content: ignoreContent }); if (generatorConfig.updateAdditionalConfig) { const additionalOutputs = await generatorConfig.updateAdditionalConfig( ignorePatterns.patterns, baseDir ); outputs.push(...additionalOutputs); } } return outputs; } function generateIgnoreFile2(patterns, tool) { const lines = [ "# Generated by rulesync from .rulesyncignore", "# This file is automatically generated. Do not edit manually." ]; if (tool === "copilot") { lines.push("# Note: .copilotignore is not officially supported by GitHub Copilot."); lines.push("# This file is for use with community tools like copilotignore-vscode extension."); } lines.push(""); lines.push(...patterns); return lines.join("\n"); } async function generateComplexRulesConfig(rules, config, generatorConfig, baseDir) { const unifiedConfig = { tool: generatorConfig.tool, fileExtension: generatorConfig.fileExtension, ignoreFileName: generatorConfig.ignoreFileName, generateContent: generatorConfig.generateContent, pathResolver: generatorConfig.getOutputPath }; return generateRulesConfig(rules, config, unifiedConfig, baseDir); } // src/generators/rules/augmentcode.ts async function generateAugmentcodeConfig(rules, config, baseDir) { const outputs = createOutputsArray(); rules.forEach((rule) => { addOutput( outputs, "augmentcode", config, baseDir, join6(".augment", "rules", `${rule.filename}.md`), generateRuleFile(rule) ); }); return outputs; } function generateRuleFile(rule) { const lines = []; lines.push("---"); let ruleType = "manual"; let description = rule.frontmatter.description; if (rule.filename.endsWith("-always")) { ruleType = "always"; description = ""; } else if (rule.filename.endsWith("-auto")) { ruleType = "auto"; } lines.push(`type: ${ruleType}`); lines.push(`description: "${description}"`); if (rule.frontmatter.tags && Array.isArray(rule.frontmatter.tags) && rule.frontmatter.tags.length > 0) { lines.push(`tags: [${rule.frontmatter.tags.map((tag) => `"${tag}"`).join(", ")}]`); } lines.push("---"); lines.push(""); lines.push(rule.content.trim()); return lines.join("\n"); } // src/generators/rules/augmentcode-legacy.ts async function generateAugmentcodeLegacyConfig(rules, config, baseDir) { const outputs = createOutputsArray(); if (rules.length > 0) { addOutput( outputs, "augmentcode-legacy", config, baseDir, ".augment-guidelines", generateLegacyGuidelinesFile(rules) ); } return outputs; } function generateLegacyGuidelinesFile(allRules) { const lines = []; for (const rule of allRules) { lines.push(rule.content.trim()); lines.push(""); } return lines.join("\n").trim(); } // src/generators/rules/claudecode.ts import { join as join7 } from "path"; async function generateClaudecodeConfig(rules, config, baseDir) { const generatorConfig = { tool: "claudecode", fileExtension: ".md", ignoreFileName: ".aiignore", generateContent: generateMemoryFile, generateRootContent: (rootRule, detailRules) => generateClaudeMarkdown(rootRule ? [rootRule] : [], detailRules), rootFilePath: "CLAUDE.md", generateDetailContent: generateMemoryFile, detailSubDir: ".claude/memories", updateAdditionalConfig: async (ignorePatterns, baseDir2) => { const settingsPath = resolvePath(join7(".claude", "settings.json"), baseDir2); await updateClaudeSettings(settingsPath, ignorePatterns); return []; } }; return generateComplexRules(rules, config, generatorConfig, baseDir); } function generateClaudeMarkdown(rootRules, detailRules) { const lines = []; if (detailRules.length > 0) { lines.push("Please also reference the following documents as needed:"); lines.push(""); for (const rule of detailRules) { const escapedDescription = rule.frontmatter.description.replace(/"/g, '\\"'); const globsText = rule.frontmatter.globs.join(","); lines.push( `@.claude/memories/${rule.filename}.md description: "${escapedDescription}" globs: "${globsText}"` ); } lines.push(""); } if (rootRules.length > 0) { for (const rule of rootRules) { lines.push(rule.content); lines.push(""); } } return lines.join("\n"); } function generateMemoryFile(rule) { return rule.content.trim(); } async function updateClaudeSettings(settingsPath, ignorePatterns) { let rawSettings = {}; if (await fileExists(settingsPath)) { try { const content = await readFileContent(settingsPath); rawSettings = JSON.parse(content); } catch { console.warn(`Failed to parse existing ${settingsPath}, creating new settings`); rawSettings = {}; } } const parseResult = ClaudeSettingsSchema.safeParse(rawSettings); const settings = parseResult.success ? parseResult.data : ClaudeSettingsSchema.parse({}); const readDenyRules = ignorePatterns.map((pattern) => `Read(${pattern})`); if (!settings.permissions) { settings.permissions = { deny: [] }; } if (!Array.isArray(settings.permissions.deny)) { settings.permissions.deny = []; } const filteredDeny = settings.permissions.deny.filter((rule) => { if (!rule.startsWith("Read(")) return true; const match = rule.match(/^Read\((.*)\)$/); if (!match) return true; return !ignorePatterns.includes(match[1] ?? ""); }); filteredDeny.push(...readDenyRules); settings.permissions.deny = Array.from(new Set(filteredDeny)); const jsonContent = JSON.stringify(settings, null, 2); await wr