From 46ab89bc4bbe3ea8db2bf57d27c8c123734fefa6 Mon Sep 17 00:00:00 2001 From: Fraser Killip Date: Tue, 22 Jul 2025 01:58:43 +0000 Subject: [PATCH 1/5] feat: Add option to disable backup file creation in agents and CLI --- README.md | 7 + src/agents/AiderAgent.ts | 8 +- src/agents/AugmentCodeAgent.ts | 8 +- src/agents/ClaudeAgent.ts | 4 +- src/agents/ClineAgent.ts | 4 +- src/agents/CodexCliAgent.ts | 4 +- src/agents/CopilotAgent.ts | 4 +- src/agents/CursorAgent.ts | 4 +- src/agents/FirebaseAgent.ts | 4 +- src/agents/IAgent.ts | 1 + src/agents/JunieAgent.ts | 4 +- src/agents/KiloCodeAgent.ts | 4 +- src/agents/OpenHandsAgent.ts | 4 +- src/agents/WindsurfAgent.ts | 4 +- src/cli/commands.ts | 7 + src/lib.ts | 6 +- tests/unit/agents/AgentAdapters.test.ts | 253 ++++++++++++++++-------- 17 files changed, 234 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 88e505a..e73cb8a 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t | `--no-gitignore` | Disable automatic .gitignore updates | | `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` | | `--verbose` / `-v` | Display detailed output during execution | +| `--disable-backup` | Disable creation of `.bak` backup files when applying rules (default: false) | ### Common Examples @@ -194,6 +195,12 @@ ruler apply --verbose ruler apply --no-mcp --no-gitignore ``` +**Apply rules without creating backup files:** + +```bash +ruler apply --disable-backup +``` + ## Usage: The `revert` Command The `revert` command safely undoes all changes made by `ruler apply`, restoring your project to its pre-ruler state. It intelligently restores files from backups (`.bak` files) when available, or removes generated files that didn't exist before. diff --git a/src/agents/AiderAgent.ts b/src/agents/AiderAgent.ts index 006f3af..198bed6 100644 --- a/src/agents/AiderAgent.ts +++ b/src/agents/AiderAgent.ts @@ -25,7 +25,9 @@ export class AiderAgent implements IAgent { const mdPath = agentConfig?.outputPathInstructions ?? this.getDefaultOutputPath(projectRoot).instructions; - await backupFile(mdPath); + if (!agentConfig?.disableBackup) { + await backupFile(mdPath); + } await writeGeneratedFile(mdPath, concatenatedRules); const cfgPath = @@ -38,7 +40,9 @@ export class AiderAgent implements IAgent { let doc: AiderConfig = {} as AiderConfig; try { await fs.access(cfgPath); - await backupFile(cfgPath); + if (!agentConfig?.disableBackup) { + await backupFile(cfgPath); + } const raw = await fs.readFile(cfgPath, 'utf8'); doc = (yaml.load(raw) || {}) as AiderConfig; } catch { diff --git a/src/agents/AugmentCodeAgent.ts b/src/agents/AugmentCodeAgent.ts index 09c41fb..2a08ef0 100644 --- a/src/agents/AugmentCodeAgent.ts +++ b/src/agents/AugmentCodeAgent.ts @@ -30,12 +30,16 @@ export class AugmentCodeAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); if (rulerMcpJson) { const settingsPath = getVSCodeSettingsPath(projectRoot); - await backupFile(settingsPath); + if (!agentConfig?.disableBackup) { + await backupFile(settingsPath); + } const existingSettings = await readVSCodeSettings(settingsPath); const augmentServers = transformRulerToAugmentMcp(rulerMcpJson); diff --git a/src/agents/ClaudeAgent.ts b/src/agents/ClaudeAgent.ts index eeac6ce..05b3f46 100644 --- a/src/agents/ClaudeAgent.ts +++ b/src/agents/ClaudeAgent.ts @@ -22,7 +22,9 @@ export class ClaudeAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/ClineAgent.ts b/src/agents/ClineAgent.ts index 912f928..8b4aada 100644 --- a/src/agents/ClineAgent.ts +++ b/src/agents/ClineAgent.ts @@ -22,7 +22,9 @@ export class ClineAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/CodexCliAgent.ts b/src/agents/CodexCliAgent.ts index f1df034..03827c0 100644 --- a/src/agents/CodexCliAgent.ts +++ b/src/agents/CodexCliAgent.ts @@ -22,7 +22,9 @@ export class CodexCliAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/CopilotAgent.ts b/src/agents/CopilotAgent.ts index a05f968..e08ea8b 100644 --- a/src/agents/CopilotAgent.ts +++ b/src/agents/CopilotAgent.ts @@ -27,7 +27,9 @@ export class CopilotAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/CursorAgent.ts b/src/agents/CursorAgent.ts index 369b8fd..f7d3eb9 100644 --- a/src/agents/CursorAgent.ts +++ b/src/agents/CursorAgent.ts @@ -27,7 +27,9 @@ export class CursorAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/FirebaseAgent.ts b/src/agents/FirebaseAgent.ts index a04d883..3288e11 100644 --- a/src/agents/FirebaseAgent.ts +++ b/src/agents/FirebaseAgent.ts @@ -22,7 +22,9 @@ export class FirebaseAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } diff --git a/src/agents/IAgent.ts b/src/agents/IAgent.ts index 3a197dd..8f27dff 100644 --- a/src/agents/IAgent.ts +++ b/src/agents/IAgent.ts @@ -14,6 +14,7 @@ export interface IAgentConfig { enabled?: boolean; strategy?: 'merge' | 'overwrite'; }; + disableBackup?: boolean; } export interface IAgent { diff --git a/src/agents/JunieAgent.ts b/src/agents/JunieAgent.ts index 2dd5ccc..7eb5215 100644 --- a/src/agents/JunieAgent.ts +++ b/src/agents/JunieAgent.ts @@ -27,7 +27,9 @@ export class JunieAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/KiloCodeAgent.ts b/src/agents/KiloCodeAgent.ts index 2c8c7f5..52bc2be 100644 --- a/src/agents/KiloCodeAgent.ts +++ b/src/agents/KiloCodeAgent.ts @@ -28,7 +28,9 @@ export class KiloCodeAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } diff --git a/src/agents/OpenHandsAgent.ts b/src/agents/OpenHandsAgent.ts index 03be5b5..0ee6065 100644 --- a/src/agents/OpenHandsAgent.ts +++ b/src/agents/OpenHandsAgent.ts @@ -22,7 +22,9 @@ export class OpenHandsAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/WindsurfAgent.ts b/src/agents/WindsurfAgent.ts index ab21e7f..ced6c8b 100644 --- a/src/agents/WindsurfAgent.ts +++ b/src/agents/WindsurfAgent.ts @@ -27,7 +27,9 @@ export class WindsurfAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - await backupFile(output); + if (!agentConfig?.disableBackup) { + await backupFile(output); + } await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/cli/commands.ts b/src/cli/commands.ts index f7c6eff..8be29b6 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -65,6 +65,11 @@ export function run(): void { 'Only search for local .ruler directories, ignore global config', default: false, }); + y.option('disable-backup', { + type: 'boolean', + description: 'Disable creation of backup files before applying changes', + default: false, + }); }, async (argv) => { const projectRoot = argv['project-root'] as string; @@ -79,6 +84,7 @@ export function run(): void { const verbose = argv.verbose as boolean; const dryRun = argv['dry-run'] as boolean; const localOnly = argv['local-only'] as boolean; + const disableBackup = argv['disable-backup'] as boolean; // Determine gitignore preference: CLI > TOML > Default (enabled) // yargs handles --no-gitignore by setting gitignore to false @@ -99,6 +105,7 @@ export function run(): void { verbose, dryRun, localOnly, + disableBackup, ); console.log('Ruler apply completed successfully.'); } catch (err: unknown) { diff --git a/src/lib.ts b/src/lib.ts index 324be13..4850f2b 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -107,6 +107,7 @@ export async function applyAllAgentConfigs( verbose = false, dryRun = false, localOnly = false, + disableBackup = false, ): Promise { // Load configuration (default_agents, per-agent overrides, CLI filters) logVerbose( @@ -259,7 +260,8 @@ export async function applyAllAgentConfigs( } agentsMdWritten = true; } - let finalAgentConfig = agentConfig; + // Propagate disableBackup to agent config + let finalAgentConfig = { ...agentConfig, disableBackup }; if (agent.getIdentifier() === 'augmentcode' && rulerMcpJson) { const resolvedStrategy = cliMcpStrategy ?? @@ -268,7 +270,7 @@ export async function applyAllAgentConfigs( 'merge'; finalAgentConfig = { - ...agentConfig, + ...finalAgentConfig, mcp: { ...agentConfig?.mcp, strategy: resolvedStrategy, diff --git a/tests/unit/agents/AgentAdapters.test.ts b/tests/unit/agents/AgentAdapters.test.ts index b50e39e..0ef052e 100644 --- a/tests/unit/agents/AgentAdapters.test.ts +++ b/tests/unit/agents/AgentAdapters.test.ts @@ -24,7 +24,7 @@ describe('Agent Adapters', () => { }); describe('CopilotAgent', () => { - it('backs up and writes copilot-instructions.md', async () => { + it('backs up and writes copilot-instructions.md', async () => { const agent = new CopilotAgent(); const githubDir = path.join(tmpDir, '.github'); await fs.mkdir(githubDir, { recursive: true }); @@ -36,17 +36,29 @@ describe('Agent Adapters', () => { expect(backup).toBe('old copilot'); expect(content).toBe('new copilot'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new CopilotAgent(); - const custom = path.join(tmpDir, 'custom_copilot.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('custom data', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('custom data'); + it('writes copilot-instructions.md without backup when cli flag is used', async () => { + const agent = new CopilotAgent(); + const githubDir = path.join(tmpDir, '.github'); + await fs.mkdir(githubDir, { recursive: true }); + const target = path.join(githubDir, 'copilot-instructions.md'); + await fs.writeFile(target, 'old copilot'); + await agent.applyRulerConfig('new copilot', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new copilot'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new CopilotAgent(); + const custom = path.join(tmpDir, 'custom_copilot.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('custom data', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('custom data'); + }); }); + describe('ClaudeAgent', () => { - it('backs up and writes CLAUDE.md', async () => { + it('backs up and writes CLAUDE.md', async () => { const agent = new ClaudeAgent(); const target = path.join(tmpDir, 'CLAUDE.md'); await fs.writeFile(target, 'old claude'); @@ -54,17 +66,26 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old claude'); expect(await fs.readFile(target, 'utf8')).toBe('new claude'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new ClaudeAgent(); - const custom = path.join(tmpDir, 'CUSTOM_CLAUDE.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('x', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('x'); + it('writes CLAUDE.md without backup when cli flag is used', async () => { + const agent = new ClaudeAgent(); + const target = path.join(tmpDir, 'CLAUDE.md'); + await fs.writeFile(target, 'old claude'); + await agent.applyRulerConfig('new claude', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new claude'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new ClaudeAgent(); + const custom = path.join(tmpDir, 'CUSTOM_CLAUDE.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('x', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('x'); + }); }); describe('CodexCliAgent', () => { - it('backs up and writes AGENTS.md', async () => { + it('backs up and writes AGENTS.md', async () => { const agent = new CodexCliAgent(); const target = path.join(tmpDir, 'AGENTS.md'); await fs.writeFile(target, 'old codex'); @@ -72,17 +93,26 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old codex'); expect(await fs.readFile(target, 'utf8')).toBe('new codex'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new CodexCliAgent(); - const custom = path.join(tmpDir, 'CUSTOM_AGENTS.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('y', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('y'); + it('writes AGENTS.md without backup when cli flag is used', async () => { + const agent = new CodexCliAgent(); + const target = path.join(tmpDir, 'AGENTS.md'); + await fs.writeFile(target, 'old codex'); + await agent.applyRulerConfig('new codex', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new codex'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new CodexCliAgent(); + const custom = path.join(tmpDir, 'CUSTOM_AGENTS.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('y', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('y'); + }); }); describe('CursorAgent', () => { - it('backs up and writes ruler_cursor_instructions.mdc', async () => { + it('backs up and writes ruler_cursor_instructions.mdc', async () => { const agent = new CursorAgent(); const rulesDir = path.join(tmpDir, '.cursor', 'rules'); await fs.mkdir(rulesDir, { recursive: true }); @@ -92,19 +122,30 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old cursor'); expect(await fs.readFile(target, 'utf8')).toBe('new cursor'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new CursorAgent(); - const customDir = path.join(tmpDir, '.cursor', 'rules'); - await fs.mkdir(customDir, { recursive: true }); - const custom = path.join(tmpDir, 'custom_cursor.mdc'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('z', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('z'); + it('writes ruler_cursor_instructions.mdc without backup when cli flag is used', async () => { + const agent = new CursorAgent(); + const rulesDir = path.join(tmpDir, '.cursor', 'rules'); + await fs.mkdir(rulesDir, { recursive: true }); + const target = path.join(rulesDir, 'ruler_cursor_instructions.mdc'); + await fs.writeFile(target, 'old cursor'); + await agent.applyRulerConfig('new cursor', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new cursor'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new CursorAgent(); + const customDir = path.join(tmpDir, '.cursor', 'rules'); + await fs.mkdir(customDir, { recursive: true }); + const custom = path.join(tmpDir, 'custom_cursor.mdc'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('z', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('z'); + }); }); describe('WindsurfAgent', () => { - it('backs up and writes ruler_windsurf_instructions.md', async () => { + it('backs up and writes ruler_windsurf_instructions.md', async () => { const agent = new WindsurfAgent(); const rulesDir = path.join(tmpDir, '.windsurf', 'rules'); await fs.mkdir(rulesDir, { recursive: true }); @@ -114,19 +155,30 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old windsurf'); expect(await fs.readFile(target, 'utf8')).toBe('new windsurf'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new WindsurfAgent(); - const customDir = path.join(tmpDir, '.windsurf', 'rules'); - await fs.mkdir(customDir, { recursive: true }); - const custom = path.join(tmpDir, 'custom_windsurf.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('w', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('w'); + it('writes ruler_windsurf_instructions.md without backup when cli flag is used', async () => { + const agent = new WindsurfAgent(); + const rulesDir = path.join(tmpDir, '.windsurf', 'rules'); + await fs.mkdir(rulesDir, { recursive: true }); + const target = path.join(rulesDir, 'ruler_windsurf_instructions.md'); + await fs.writeFile(target, 'old windsurf'); + await agent.applyRulerConfig('new windsurf', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new windsurf'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new WindsurfAgent(); + const customDir = path.join(tmpDir, '.windsurf', 'rules'); + await fs.mkdir(customDir, { recursive: true }); + const custom = path.join(tmpDir, 'custom_windsurf.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('w', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('w'); + }); }); describe('ClineAgent', () => { - it('backs up and writes .clinerules', async () => { + it('backs up and writes .clinerules', async () => { const agent = new ClineAgent(); const target = path.join(tmpDir, '.clinerules'); await fs.writeFile(target, 'old cline'); @@ -134,17 +186,26 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old cline'); expect(await fs.readFile(target, 'utf8')).toBe('new cline'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new ClineAgent(); - const custom = path.join(tmpDir, 'custom_cline'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('c', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('c'); + it('writes .clinerules without backup when cli flag is used', async () => { + const agent = new ClineAgent(); + const target = path.join(tmpDir, '.clinerules'); + await fs.writeFile(target, 'old cline'); + await agent.applyRulerConfig('new cline', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new cline'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new ClineAgent(); + const custom = path.join(tmpDir, 'custom_cline'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('c', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('c'); + }); }); describe('AiderAgent', () => { - it('creates and updates .aider.conf.yml', async () => { + it('creates and updates .aider.conf.yml', async () => { const agent = new AiderAgent(); // No existing config await agent.applyRulerConfig('aider rules', tmpDir, null); @@ -161,21 +222,21 @@ describe('Agent Adapters', () => { expect(Array.isArray(updated.read)).toBe(true); expect(updated.read).toContain('ruler_aider_instructions.md'); }); - }); - it('uses custom outputPathInstructions when provided', async () => { - const agent = new AiderAgent(); - const customMd = path.join(tmpDir, 'custom_aider.md'); - await fs.mkdir(path.dirname(customMd), { recursive: true }); - await agent.applyRulerConfig('aider data', tmpDir, null, { outputPathInstructions: customMd }); - expect(await fs.readFile(customMd, 'utf8')).toBe('aider data'); - const cfg = yaml.load( - await fs.readFile(path.join(tmpDir, '.aider.conf.yml'), 'utf8'), - ) as any; - expect(cfg.read).toContain('custom_aider.md'); + it('uses custom outputPathInstructions when provided', async () => { + const agent = new AiderAgent(); + const customMd = path.join(tmpDir, 'custom_aider.md'); + await fs.mkdir(path.dirname(customMd), { recursive: true }); + await agent.applyRulerConfig('aider data', tmpDir, null, { outputPathInstructions: customMd }); + expect(await fs.readFile(customMd, 'utf8')).toBe('aider data'); + const cfg = yaml.load( + await fs.readFile(path.join(tmpDir, '.aider.conf.yml'), 'utf8'), + ) as any; + expect(cfg.read).toContain('custom_aider.md'); + }); }); describe('FirebaseAgent', () => { - it('backs up and writes .idx/airules.md', async () => { + it('backs up and writes .idx/airules.md', async () => { const agent = new FirebaseAgent(); const idxDir = path.join(tmpDir, '.idx'); await fs.mkdir(idxDir, { recursive: true }); @@ -185,17 +246,28 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old firebase'); expect(await fs.readFile(target, 'utf8')).toBe('new firebase'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new FirebaseAgent(); - const custom = path.join(tmpDir, 'custom_firebase.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('firebase rules', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('firebase rules'); + it('writes .idx/airules.md without backup when cli flag is used', async () => { + const agent = new FirebaseAgent(); + const idxDir = path.join(tmpDir, '.idx'); + await fs.mkdir(idxDir, { recursive: true }); + const target = path.join(idxDir, 'airules.md'); + await fs.writeFile(target, 'old firebase'); + await agent.applyRulerConfig('new firebase', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new firebase'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new FirebaseAgent(); + const custom = path.join(tmpDir, 'custom_firebase.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('firebase rules', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('firebase rules'); + }); }); describe('JunieAgent', () => { - it('backs up and writes .junie/guidelines.md', async () => { + it('backs up and writes .junie/guidelines.md', async () => { const agent = new JunieAgent(); const junieDir = path.join(tmpDir, '.junie'); await fs.mkdir(junieDir, { recursive: true }); @@ -205,13 +277,24 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old junie'); expect(await fs.readFile(target, 'utf8')).toBe('new junie'); }); - }); - it('uses custom outputPath when provided', async () => { - const agent = new JunieAgent(); - const custom = path.join(tmpDir, 'custom_junie.md'); - await fs.mkdir(path.dirname(custom), { recursive: true }); - await agent.applyRulerConfig('junie rules', tmpDir, null, { outputPath: custom }); - expect(await fs.readFile(custom, 'utf8')).toBe('junie rules'); + it('writes .junie/guidelines.md without backup when cli flag is used', async () => { + const agent = new JunieAgent(); + const junieDir = path.join(tmpDir, '.junie'); + await fs.mkdir(junieDir, { recursive: true }); + const target = path.join(junieDir, 'guidelines.md'); + await fs.writeFile(target, 'old junie'); + await agent.applyRulerConfig('new junie', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new junie'); + }); + it('uses custom outputPath when provided', async () => { + const agent = new JunieAgent(); + const custom = path.join(tmpDir, 'custom_junie.md'); + await fs.mkdir(path.dirname(custom), { recursive: true }); + await agent.applyRulerConfig('junie rules', tmpDir, null, { outputPath: custom }); + expect(await fs.readFile(custom, 'utf8')).toBe('junie rules'); + }); }); describe('AugmentCodeAgent', () => { @@ -224,6 +307,16 @@ describe('Agent Adapters', () => { expect(await fs.readFile(`${target}.bak`, 'utf8')).toBe('old augment'); expect(await fs.readFile(target, 'utf8')).toBe('new augment'); }); + it('writes ruler_augment_instructions.md without backup when cli flag is used', async () => { + const agent = new AugmentCodeAgent(); + const target = path.join(tmpDir, '.augment', 'rules', 'ruler_augment_instructions.md'); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, 'old augment'); + await agent.applyRulerConfig('new augment', tmpDir, null, { disableBackup: true }); + await expect(fs.readFile(`${target}.bak`, 'utf8')).rejects.toThrow(); + const content = await fs.readFile(target, 'utf8'); + expect(content).toBe('new augment'); + }); it('uses custom outputPath when provided', async () => { const agent = new AugmentCodeAgent(); From a7201b39580c52bc390f58fbb1599dace68a380c Mon Sep 17 00:00:00 2001 From: Fraser Killip Date: Wed, 6 Aug 2025 09:55:25 +1200 Subject: [PATCH 2/5] fix: Update line endings to LF and format code --- .claude/settings.local.json | 11 +++++++++++ package-lock.json | 4 ++-- src/cli/commands.ts | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ccc4b9f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(gh pr view:*)", + "Bash(npm run lint)", + "Bash(npm install)", + "Bash(git add:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 41ccc3f..483467a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@intellectronica/ruler", - "version": "0.2.6", + "version": "0.2.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@intellectronica/ruler", - "version": "0.2.6", + "version": "0.2.11", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 8be29b6..c258576 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -67,7 +67,8 @@ export function run(): void { }); y.option('disable-backup', { type: 'boolean', - description: 'Disable creation of backup files before applying changes', + description: + 'Disable creation of backup files before applying changes', default: false, }); }, From 81719464e6d2f6c855673df3924ad60dbf5e8c16 Mon Sep 17 00:00:00 2001 From: Fraser Killip Date: Wed, 6 Aug 2025 10:15:08 +1200 Subject: [PATCH 3/5] fix: Remove unused settings.local.json file and clean up test file --- .claude/settings.local.json | 11 ----------- tests/unit/agents/AgentAdapters.test.ts | 1 - 2 files changed, 12 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ccc4b9f..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr view:*)", - "Bash(npm run lint)", - "Bash(npm install)", - "Bash(git add:*)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/tests/unit/agents/AgentAdapters.test.ts b/tests/unit/agents/AgentAdapters.test.ts index 9dcee3a..4e60367 100644 --- a/tests/unit/agents/AgentAdapters.test.ts +++ b/tests/unit/agents/AgentAdapters.test.ts @@ -145,7 +145,6 @@ describe('Agent Adapters', () => { expect(content).toContain('z'); }); }); - }); describe('WindsurfAgent', () => { it('backs up and writes ruler_windsurf_instructions.md', async () => { From b407f1d0e29766405abe9d4f49933828422f99d0 Mon Sep 17 00:00:00 2001 From: Fraser Killip Date: Fri, 8 Aug 2025 14:49:09 +1200 Subject: [PATCH 4/5] Refactor backup logic to simplify backupFile function and streamline agent implementations --- src/agents/AiderAgent.ts | 8 ++------ src/agents/AugmentCodeAgent.ts | 8 ++------ src/agents/ClaudeAgent.ts | 4 +--- src/agents/ClineAgent.ts | 4 +--- src/agents/CodexCliAgent.ts | 4 +--- src/agents/CopilotAgent.ts | 4 +--- src/agents/CursorAgent.ts | 4 +--- src/agents/FirebaseAgent.ts | 4 +--- src/agents/JunieAgent.ts | 4 +--- src/agents/KiloCodeAgent.ts | 4 +--- src/agents/OpenHandsAgent.ts | 4 +--- src/agents/WindsurfAgent.ts | 4 +--- src/core/FileSystemUtils.ts | 7 ++++++- 13 files changed, 20 insertions(+), 43 deletions(-) diff --git a/src/agents/AiderAgent.ts b/src/agents/AiderAgent.ts index 198bed6..b857770 100644 --- a/src/agents/AiderAgent.ts +++ b/src/agents/AiderAgent.ts @@ -25,9 +25,7 @@ export class AiderAgent implements IAgent { const mdPath = agentConfig?.outputPathInstructions ?? this.getDefaultOutputPath(projectRoot).instructions; - if (!agentConfig?.disableBackup) { - await backupFile(mdPath); - } + await backupFile(mdPath, agentConfig?.disableBackup); await writeGeneratedFile(mdPath, concatenatedRules); const cfgPath = @@ -40,9 +38,7 @@ export class AiderAgent implements IAgent { let doc: AiderConfig = {} as AiderConfig; try { await fs.access(cfgPath); - if (!agentConfig?.disableBackup) { - await backupFile(cfgPath); - } + await backupFile(cfgPath, agentConfig?.disableBackup); const raw = await fs.readFile(cfgPath, 'utf8'); doc = (yaml.load(raw) || {}) as AiderConfig; } catch { diff --git a/src/agents/AugmentCodeAgent.ts b/src/agents/AugmentCodeAgent.ts index 2a08ef0..9e61b67 100644 --- a/src/agents/AugmentCodeAgent.ts +++ b/src/agents/AugmentCodeAgent.ts @@ -30,16 +30,12 @@ export class AugmentCodeAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); if (rulerMcpJson) { const settingsPath = getVSCodeSettingsPath(projectRoot); - if (!agentConfig?.disableBackup) { - await backupFile(settingsPath); - } + await backupFile(settingsPath, agentConfig?.disableBackup); const existingSettings = await readVSCodeSettings(settingsPath); const augmentServers = transformRulerToAugmentMcp(rulerMcpJson); diff --git a/src/agents/ClaudeAgent.ts b/src/agents/ClaudeAgent.ts index 05b3f46..903501b 100644 --- a/src/agents/ClaudeAgent.ts +++ b/src/agents/ClaudeAgent.ts @@ -22,9 +22,7 @@ export class ClaudeAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/ClineAgent.ts b/src/agents/ClineAgent.ts index 8b4aada..1782d9f 100644 --- a/src/agents/ClineAgent.ts +++ b/src/agents/ClineAgent.ts @@ -22,9 +22,7 @@ export class ClineAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/CodexCliAgent.ts b/src/agents/CodexCliAgent.ts index 28296cd..d67e00e 100644 --- a/src/agents/CodexCliAgent.ts +++ b/src/agents/CodexCliAgent.ts @@ -34,9 +34,7 @@ export class CodexCliAgent implements IAgent { defaults.instructions; // Write the instructions file - if (!agentConfig?.disableBackup) { - await backupFile(instructionsPath); - } + await backupFile(instructionsPath, agentConfig?.disableBackup); await writeGeneratedFile(instructionsPath, concatenatedRules); // Handle MCP configuration if enabled diff --git a/src/agents/CopilotAgent.ts b/src/agents/CopilotAgent.ts index e08ea8b..8c878bb 100644 --- a/src/agents/CopilotAgent.ts +++ b/src/agents/CopilotAgent.ts @@ -27,9 +27,7 @@ export class CopilotAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/CursorAgent.ts b/src/agents/CursorAgent.ts index 2bcf119..9af3f49 100644 --- a/src/agents/CursorAgent.ts +++ b/src/agents/CursorAgent.ts @@ -33,9 +33,7 @@ export class CursorAgent implements IAgent { const content = `${frontMatter}${concatenatedRules.trimStart()}`; await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, content); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/FirebaseAgent.ts b/src/agents/FirebaseAgent.ts index 3288e11..bfadb28 100644 --- a/src/agents/FirebaseAgent.ts +++ b/src/agents/FirebaseAgent.ts @@ -22,9 +22,7 @@ export class FirebaseAgent implements IAgent { ): Promise { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } diff --git a/src/agents/JunieAgent.ts b/src/agents/JunieAgent.ts index 7eb5215..5d43fac 100644 --- a/src/agents/JunieAgent.ts +++ b/src/agents/JunieAgent.ts @@ -27,9 +27,7 @@ export class JunieAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/KiloCodeAgent.ts b/src/agents/KiloCodeAgent.ts index 52bc2be..920110f 100644 --- a/src/agents/KiloCodeAgent.ts +++ b/src/agents/KiloCodeAgent.ts @@ -28,9 +28,7 @@ export class KiloCodeAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } diff --git a/src/agents/OpenHandsAgent.ts b/src/agents/OpenHandsAgent.ts index 0ee6065..3cf8c0b 100644 --- a/src/agents/OpenHandsAgent.ts +++ b/src/agents/OpenHandsAgent.ts @@ -22,9 +22,7 @@ export class OpenHandsAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/agents/WindsurfAgent.ts b/src/agents/WindsurfAgent.ts index ced6c8b..5525788 100644 --- a/src/agents/WindsurfAgent.ts +++ b/src/agents/WindsurfAgent.ts @@ -27,9 +27,7 @@ export class WindsurfAgent implements IAgent { const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); await ensureDirExists(path.dirname(output)); - if (!agentConfig?.disableBackup) { - await backupFile(output); - } + await backupFile(output, agentConfig?.disableBackup); await writeGeneratedFile(output, concatenatedRules); } getDefaultOutputPath(projectRoot: string): string { diff --git a/src/core/FileSystemUtils.ts b/src/core/FileSystemUtils.ts index f55b56b..16ee05c 100644 --- a/src/core/FileSystemUtils.ts +++ b/src/core/FileSystemUtils.ts @@ -94,8 +94,13 @@ export async function writeGeneratedFile( /** * Creates a backup of the given filePath by copying it to filePath.bak if it exists. + * @param filePath The file to backup + * @param disableBackup If true, skip creating the backup */ -export async function backupFile(filePath: string): Promise { +export async function backupFile(filePath: string, disableBackup: boolean = false): Promise { + if (disableBackup) { + return; // Skip backup if disabled + } try { await fs.access(filePath); await fs.copyFile(filePath, `${filePath}.bak`); From 80d39e396de8bed97be5d085b81b0377771e06d8 Mon Sep 17 00:00:00 2001 From: Fraser Killip Date: Fri, 8 Aug 2025 20:11:39 +1200 Subject: [PATCH 5/5] Add global disable backup configuration and update related functionality --- README.md | 6 +- src/agents/GooseAgent.ts | 2 +- src/agents/OpenCodeAgent.ts | 2 +- src/cli/commands.ts | 10 ++- src/core/ConfigLoader.ts | 8 +++ src/core/FileSystemUtils.ts | 5 +- src/lib.ts | 13 +++- tests/apply-disable-backup.toml.test.ts | 91 +++++++++++++++++++++++++ tests/unit/core/ConfigLoader.test.ts | 29 ++++++++ 9 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 tests/apply-disable-backup.toml.test.ts diff --git a/README.md b/README.md index 7f93c63..4106ff7 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t | `--no-gitignore` | Disable automatic .gitignore updates | | `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` | | `--verbose` / `-v` | Display detailed output during execution | -| `--disable-backup` | Disable creation of `.bak` backup files when applying rules (default: false) | +| `--disable-backup` | Disable creation of `.bak` backup files when applying rules (default: false, configurable via `disable_backup` in `ruler.toml`) | ### Common Examples @@ -282,6 +282,10 @@ Defaults to `.ruler/ruler.toml` in the project root. Override with `--config` CL # Uses case-insensitive substring matching default_agents = ["copilot", "claude", "aider"] +# Global backup setting - disable creation of .bak backup files +# (default: false, meaning backups are enabled by default) +disable_backup = false + # --- Global MCP Server Configuration --- [mcp] # Enable/disable MCP propagation globally (default: true) diff --git a/src/agents/GooseAgent.ts b/src/agents/GooseAgent.ts index e2bed17..8a44cdf 100644 --- a/src/agents/GooseAgent.ts +++ b/src/agents/GooseAgent.ts @@ -27,7 +27,7 @@ export class GooseAgent implements IAgent { this.getDefaultOutputPath(projectRoot); // Write rules to .goosehints - await backupFile(hintsPath); + await backupFile(hintsPath, agentConfig?.disableBackup); await writeGeneratedFile(hintsPath, concatenatedRules); } diff --git a/src/agents/OpenCodeAgent.ts b/src/agents/OpenCodeAgent.ts index 62259cb..29d0e6a 100644 --- a/src/agents/OpenCodeAgent.ts +++ b/src/agents/OpenCodeAgent.ts @@ -20,7 +20,7 @@ export class OpenCodeAgent implements IAgent { const outputPath = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot); const absolutePath = path.resolve(projectRoot, outputPath); - await backupFile(absolutePath); + await backupFile(absolutePath, agentConfig?.disableBackup); await writeGeneratedFile(absolutePath, concatenatedRules); } diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 92ec24d..cfa5682 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -85,7 +85,13 @@ export function run(): void { const verbose = argv.verbose as boolean; const dryRun = argv['dry-run'] as boolean; const localOnly = argv['local-only'] as boolean; - const disableBackup = argv['disable-backup'] as boolean; + // Determine backup disable preference: CLI > TOML > Default (false) + let backupDisablePreference: boolean | undefined; + if (argv['disable-backup'] !== undefined) { + backupDisablePreference = argv['disable-backup'] as boolean; + } else { + backupDisablePreference = undefined; // Let TOML/default decide + } // Determine gitignore preference: CLI > TOML > Default (enabled) // yargs handles --no-gitignore by setting gitignore to false @@ -106,7 +112,7 @@ export function run(): void { verbose, dryRun, localOnly, - disableBackup, + backupDisablePreference, ); console.log('Ruler apply completed successfully.'); } catch (err: unknown) { diff --git a/src/core/ConfigLoader.ts b/src/core/ConfigLoader.ts index 8eae7cd..971738c 100644 --- a/src/core/ConfigLoader.ts +++ b/src/core/ConfigLoader.ts @@ -41,6 +41,7 @@ const rulerConfigSchema = z.object({ enabled: z.boolean().optional(), }) .optional(), + disable_backup: z.boolean().optional(), }); /** @@ -69,6 +70,8 @@ export interface LoadedConfig { mcp?: GlobalMcpConfig; /** Gitignore configuration section. */ gitignore?: GitignoreConfig; + /** Global disable backup setting. */ + disableBackup?: boolean; } /** @@ -207,11 +210,16 @@ export async function loadConfig( gitignoreConfig.enabled = rawGitignoreSection.enabled; } + // Parse global disable_backup setting + const disableBackup = + typeof raw.disable_backup === 'boolean' ? raw.disable_backup : undefined; + return { defaultAgents, agentConfigs, cliAgents, mcp: globalMcpConfig, gitignore: gitignoreConfig, + disableBackup, }; } diff --git a/src/core/FileSystemUtils.ts b/src/core/FileSystemUtils.ts index 16ee05c..27b38d2 100644 --- a/src/core/FileSystemUtils.ts +++ b/src/core/FileSystemUtils.ts @@ -97,7 +97,10 @@ export async function writeGeneratedFile( * @param filePath The file to backup * @param disableBackup If true, skip creating the backup */ -export async function backupFile(filePath: string, disableBackup: boolean = false): Promise { +export async function backupFile( + filePath: string, + disableBackup: boolean = false, +): Promise { if (disableBackup) { return; // Skip backup if disabled } diff --git a/src/lib.ts b/src/lib.ts index 38fe056..285a076 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -116,7 +116,7 @@ export async function applyAllAgentConfigs( verbose = false, dryRun = false, localOnly = false, - disableBackup = false, + cliDisableBackup?: boolean, ): Promise { // Load configuration (default_agents, per-agent overrides, CLI filters) logVerbose( @@ -276,6 +276,17 @@ export async function applyAllAgentConfigs( ); } + // Handle backup disable setting + // Configuration precedence: CLI > TOML > Default (false) + let disableBackup: boolean; + if (cliDisableBackup !== undefined) { + disableBackup = cliDisableBackup; + } else if (config.disableBackup !== undefined) { + disableBackup = config.disableBackup; + } else { + disableBackup = false; // Default disabled (backups enabled) + } + // Collect all generated file paths for .gitignore const generatedPaths: string[] = []; let agentsMdWritten = false; diff --git a/tests/apply-disable-backup.toml.test.ts b/tests/apply-disable-backup.toml.test.ts new file mode 100644 index 0000000..3ee78fd --- /dev/null +++ b/tests/apply-disable-backup.toml.test.ts @@ -0,0 +1,91 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import os from 'os'; +import { execSync } from 'child_process'; + +describe('apply-disable-backup.toml', () => { + let tmpDir: string; + let rulerDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ruler-backup-')); + rulerDir = path.join(tmpDir, '.ruler'); + await fs.mkdir(rulerDir, { recursive: true }); + + // Create a simple instruction file + await fs.writeFile( + path.join(rulerDir, 'instructions.md'), + '# Test Instructions\n\nThis is a test.', + ); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it('does not create backup files when disable_backup=true in TOML', async () => { + const toml = `disable_backup = true +default_agents = ["Claude"] +`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml); + + execSync('npm run build', { stdio: 'inherit' }); + execSync(`node dist/cli/index.js apply --project-root ${tmpDir}`, { + stdio: 'inherit', + }); + + // Check that no backup files were created + const claudeFile = path.join(tmpDir, 'CLAUDE.md'); + const backupFile = path.join(tmpDir, 'CLAUDE.md.bak'); + + expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true); + expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(false); + }); + + it('creates backup files when disable_backup=false in TOML', async () => { + const toml = `disable_backup = false +default_agents = ["Claude"] +`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml); + + // Create a pre-existing file to back up + const claudeFile = path.join(tmpDir, 'CLAUDE.md'); + await fs.writeFile(claudeFile, '# Existing content\n'); + + execSync('npm run build', { stdio: 'inherit' }); + execSync(`node dist/cli/index.js apply --project-root ${tmpDir}`, { + stdio: 'inherit', + }); + + // Check that backup file was created + const backupFile = path.join(tmpDir, 'CLAUDE.md.bak'); + + expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true); + expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(true); + + const backupContent = await fs.readFile(backupFile, 'utf8'); + expect(backupContent).toBe('# Existing content\n'); + }); + + it('CLI --disable-backup overrides TOML disable_backup=false', async () => { + const toml = `disable_backup = false +default_agents = ["Claude"] +`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml); + + // Create a pre-existing file to back up + const claudeFile = path.join(tmpDir, 'CLAUDE.md'); + await fs.writeFile(claudeFile, '# Existing content\n'); + + execSync('npm run build', { stdio: 'inherit' }); + execSync(`node dist/cli/index.js apply --disable-backup --project-root ${tmpDir}`, { + stdio: 'inherit', + }); + + // Check that no backup file was created despite TOML setting + const backupFile = path.join(tmpDir, 'CLAUDE.md.bak'); + + expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true); + expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(false); + }); +}); \ No newline at end of file diff --git a/tests/unit/core/ConfigLoader.test.ts b/tests/unit/core/ConfigLoader.test.ts index 1d45d42..9eb90a4 100644 --- a/tests/unit/core/ConfigLoader.test.ts +++ b/tests/unit/core/ConfigLoader.test.ts @@ -154,4 +154,33 @@ it('loads config from custom path via configPath option', async () => { expect(config.gitignore?.enabled).toBeUndefined(); }); }); + + describe('disable_backup configuration', () => { + it('parses disable_backup = true', async () => { + const content = `disable_backup = true`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content); + const config = await loadConfig({ projectRoot: tmpDir }); + expect(config.disableBackup).toBe(true); + }); + + it('parses disable_backup = false', async () => { + const content = `disable_backup = false`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content); + const config = await loadConfig({ projectRoot: tmpDir }); + expect(config.disableBackup).toBe(false); + }); + + it('handles missing disable_backup key', async () => { + const content = `default_agents = ["A"]`; + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content); + const config = await loadConfig({ projectRoot: tmpDir }); + expect(config.disableBackup).toBeUndefined(); + }); + + it('handles empty config file for disable_backup', async () => { + await fs.writeFile(path.join(rulerDir, 'ruler.toml'), ''); + const config = await loadConfig({ projectRoot: tmpDir }); + expect(config.disableBackup).toBeUndefined(); + }); + }); }); \ No newline at end of file