Skip to content

Commit a9ceba1

Browse files
committed
[JIRA-61] Add global disable backup configuration and update related functionality
1 parent b407f1d commit a9ceba1

File tree

9 files changed

+159
-7
lines changed

9 files changed

+159
-7
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
160160
| `--no-gitignore` | Disable automatic .gitignore updates |
161161
| `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` |
162162
| `--verbose` / `-v` | Display detailed output during execution |
163-
| `--disable-backup` | Disable creation of `.bak` backup files when applying rules (default: false) |
163+
| `--disable-backup` | Disable creation of `.bak` backup files when applying rules (default: false, configurable via `disable_backup` in `ruler.toml`) |
164164

165165
### Common Examples
166166

@@ -282,6 +282,10 @@ Defaults to `.ruler/ruler.toml` in the project root. Override with `--config` CL
282282
# Uses case-insensitive substring matching
283283
default_agents = ["copilot", "claude", "aider"]
284284

285+
# Global backup setting - disable creation of .bak backup files
286+
# (default: false, meaning backups are enabled by default)
287+
disable_backup = false
288+
285289
# --- Global MCP Server Configuration ---
286290
[mcp]
287291
# Enable/disable MCP propagation globally (default: true)

src/agents/GooseAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class GooseAgent implements IAgent {
2727
this.getDefaultOutputPath(projectRoot);
2828

2929
// Write rules to .goosehints
30-
await backupFile(hintsPath);
30+
await backupFile(hintsPath, agentConfig?.disableBackup);
3131
await writeGeneratedFile(hintsPath, concatenatedRules);
3232
}
3333

src/agents/OpenCodeAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class OpenCodeAgent implements IAgent {
2020
const outputPath =
2121
agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
2222
const absolutePath = path.resolve(projectRoot, outputPath);
23-
await backupFile(absolutePath);
23+
await backupFile(absolutePath, agentConfig?.disableBackup);
2424
await writeGeneratedFile(absolutePath, concatenatedRules);
2525
}
2626

src/cli/commands.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ export function run(): void {
8585
const verbose = argv.verbose as boolean;
8686
const dryRun = argv['dry-run'] as boolean;
8787
const localOnly = argv['local-only'] as boolean;
88-
const disableBackup = argv['disable-backup'] as boolean;
88+
// Determine backup disable preference: CLI > TOML > Default (false)
89+
let backupDisablePreference: boolean | undefined;
90+
if (argv['disable-backup'] !== undefined) {
91+
backupDisablePreference = argv['disable-backup'] as boolean;
92+
} else {
93+
backupDisablePreference = undefined; // Let TOML/default decide
94+
}
8995

9096
// Determine gitignore preference: CLI > TOML > Default (enabled)
9197
// yargs handles --no-gitignore by setting gitignore to false
@@ -106,7 +112,7 @@ export function run(): void {
106112
verbose,
107113
dryRun,
108114
localOnly,
109-
disableBackup,
115+
backupDisablePreference,
110116
);
111117
console.log('Ruler apply completed successfully.');
112118
} catch (err: unknown) {

src/core/ConfigLoader.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const rulerConfigSchema = z.object({
4141
enabled: z.boolean().optional(),
4242
})
4343
.optional(),
44+
disable_backup: z.boolean().optional(),
4445
});
4546

4647
/**
@@ -69,6 +70,8 @@ export interface LoadedConfig {
6970
mcp?: GlobalMcpConfig;
7071
/** Gitignore configuration section. */
7172
gitignore?: GitignoreConfig;
73+
/** Global disable backup setting. */
74+
disableBackup?: boolean;
7275
}
7376

7477
/**
@@ -207,11 +210,16 @@ export async function loadConfig(
207210
gitignoreConfig.enabled = rawGitignoreSection.enabled;
208211
}
209212

213+
// Parse global disable_backup setting
214+
const disableBackup =
215+
typeof raw.disable_backup === 'boolean' ? raw.disable_backup : undefined;
216+
210217
return {
211218
defaultAgents,
212219
agentConfigs,
213220
cliAgents,
214221
mcp: globalMcpConfig,
215222
gitignore: gitignoreConfig,
223+
disableBackup,
216224
};
217225
}

src/core/FileSystemUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ export async function writeGeneratedFile(
9797
* @param filePath The file to backup
9898
* @param disableBackup If true, skip creating the backup
9999
*/
100-
export async function backupFile(filePath: string, disableBackup: boolean = false): Promise<void> {
100+
export async function backupFile(
101+
filePath: string,
102+
disableBackup: boolean = false,
103+
): Promise<void> {
101104
if (disableBackup) {
102105
return; // Skip backup if disabled
103106
}

src/lib.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export async function applyAllAgentConfigs(
116116
verbose = false,
117117
dryRun = false,
118118
localOnly = false,
119-
disableBackup = false,
119+
cliDisableBackup?: boolean,
120120
): Promise<void> {
121121
// Load configuration (default_agents, per-agent overrides, CLI filters)
122122
logVerbose(
@@ -276,6 +276,17 @@ export async function applyAllAgentConfigs(
276276
);
277277
}
278278

279+
// Handle backup disable setting
280+
// Configuration precedence: CLI > TOML > Default (false)
281+
let disableBackup: boolean;
282+
if (cliDisableBackup !== undefined) {
283+
disableBackup = cliDisableBackup;
284+
} else if (config.disableBackup !== undefined) {
285+
disableBackup = config.disableBackup;
286+
} else {
287+
disableBackup = false; // Default disabled (backups enabled)
288+
}
289+
279290
// Collect all generated file paths for .gitignore
280291
const generatedPaths: string[] = [];
281292
let agentsMdWritten = false;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as fs from 'fs/promises';
2+
import * as path from 'path';
3+
import os from 'os';
4+
import { execSync } from 'child_process';
5+
6+
describe('apply-disable-backup.toml', () => {
7+
let tmpDir: string;
8+
let rulerDir: string;
9+
10+
beforeEach(async () => {
11+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ruler-backup-'));
12+
rulerDir = path.join(tmpDir, '.ruler');
13+
await fs.mkdir(rulerDir, { recursive: true });
14+
15+
// Create a simple instruction file
16+
await fs.writeFile(
17+
path.join(rulerDir, 'instructions.md'),
18+
'# Test Instructions\n\nThis is a test.',
19+
);
20+
});
21+
22+
afterEach(async () => {
23+
await fs.rm(tmpDir, { recursive: true, force: true });
24+
});
25+
26+
it('does not create backup files when disable_backup=true in TOML', async () => {
27+
const toml = `disable_backup = true
28+
default_agents = ["Claude"]
29+
`;
30+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml);
31+
32+
execSync('npm run build', { stdio: 'inherit' });
33+
execSync(`node dist/cli/index.js apply --project-root ${tmpDir}`, {
34+
stdio: 'inherit',
35+
});
36+
37+
// Check that no backup files were created
38+
const claudeFile = path.join(tmpDir, 'CLAUDE.md');
39+
const backupFile = path.join(tmpDir, 'CLAUDE.md.bak');
40+
41+
expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true);
42+
expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(false);
43+
});
44+
45+
it('creates backup files when disable_backup=false in TOML', async () => {
46+
const toml = `disable_backup = false
47+
default_agents = ["Claude"]
48+
`;
49+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml);
50+
51+
// Create a pre-existing file to back up
52+
const claudeFile = path.join(tmpDir, 'CLAUDE.md');
53+
await fs.writeFile(claudeFile, '# Existing content\n');
54+
55+
execSync('npm run build', { stdio: 'inherit' });
56+
execSync(`node dist/cli/index.js apply --project-root ${tmpDir}`, {
57+
stdio: 'inherit',
58+
});
59+
60+
// Check that backup file was created
61+
const backupFile = path.join(tmpDir, 'CLAUDE.md.bak');
62+
63+
expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true);
64+
expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(true);
65+
66+
const backupContent = await fs.readFile(backupFile, 'utf8');
67+
expect(backupContent).toBe('# Existing content\n');
68+
});
69+
70+
it('CLI --disable-backup overrides TOML disable_backup=false', async () => {
71+
const toml = `disable_backup = false
72+
default_agents = ["Claude"]
73+
`;
74+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), toml);
75+
76+
// Create a pre-existing file to back up
77+
const claudeFile = path.join(tmpDir, 'CLAUDE.md');
78+
await fs.writeFile(claudeFile, '# Existing content\n');
79+
80+
execSync('npm run build', { stdio: 'inherit' });
81+
execSync(`node dist/cli/index.js apply --disable-backup --project-root ${tmpDir}`, {
82+
stdio: 'inherit',
83+
});
84+
85+
// Check that no backup file was created despite TOML setting
86+
const backupFile = path.join(tmpDir, 'CLAUDE.md.bak');
87+
88+
expect(await fs.access(claudeFile).then(() => true).catch(() => false)).toBe(true);
89+
expect(await fs.access(backupFile).then(() => true).catch(() => false)).toBe(false);
90+
});
91+
});

tests/unit/core/ConfigLoader.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,33 @@ it('loads config from custom path via configPath option', async () => {
154154
expect(config.gitignore?.enabled).toBeUndefined();
155155
});
156156
});
157+
158+
describe('disable_backup configuration', () => {
159+
it('parses disable_backup = true', async () => {
160+
const content = `disable_backup = true`;
161+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content);
162+
const config = await loadConfig({ projectRoot: tmpDir });
163+
expect(config.disableBackup).toBe(true);
164+
});
165+
166+
it('parses disable_backup = false', async () => {
167+
const content = `disable_backup = false`;
168+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content);
169+
const config = await loadConfig({ projectRoot: tmpDir });
170+
expect(config.disableBackup).toBe(false);
171+
});
172+
173+
it('handles missing disable_backup key', async () => {
174+
const content = `default_agents = ["A"]`;
175+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), content);
176+
const config = await loadConfig({ projectRoot: tmpDir });
177+
expect(config.disableBackup).toBeUndefined();
178+
});
179+
180+
it('handles empty config file for disable_backup', async () => {
181+
await fs.writeFile(path.join(rulerDir, 'ruler.toml'), '');
182+
const config = await loadConfig({ projectRoot: tmpDir });
183+
expect(config.disableBackup).toBeUndefined();
184+
});
185+
});
157186
});

0 commit comments

Comments
 (0)