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
JavaScript
#!/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