Skip to content

Commit 1b0832f

Browse files
authored
Merge pull request #19 from BeyteFlow/copilot/sub-pr-18
Fix atomic writes and fs.constants flags in InitCommand and ConfigService
2 parents 72d423b + a65b3a6 commit 1b0832f

2 files changed

Lines changed: 46 additions & 8 deletions

File tree

git-ai/src/commands/InitCommand.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ async function readSecretInput(rl: readline.Interface, prompt: string): Promise<
2828
}
2929
}
3030

31+
/**
32+
* Atomically writes content to filePath using a temp file + fsync + rename,
33+
* ensuring the file has permissions 0o600.
34+
*/
35+
function atomicWriteFileSync(filePath: string, content: string): void {
36+
const dir = path.dirname(filePath);
37+
const tempPath = path.join(dir, `.tmp-${process.pid}-${Date.now()}`);
38+
const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
39+
try {
40+
fs.writeFileSync(fd, content);
41+
fs.fsyncSync(fd);
42+
} finally {
43+
fs.closeSync(fd);
44+
}
45+
try {
46+
fs.renameSync(tempPath, filePath);
47+
} catch (error) {
48+
try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ }
49+
throw error;
50+
}
51+
}
52+
3153
export async function initCommand() {
3254
const rl = readline.createInterface({
3355
input: process.stdin,
@@ -65,9 +87,13 @@ export async function initCommand() {
6587

6688
// --- Step 4: Attempt atomic creation ---
6789
try {
68-
const fd = fs.openSync(configPath, fs.O_CREAT | fs.O_EXCL | fs.O_RDWR, 0o600);
69-
fs.writeFileSync(fd, JSON.stringify(newConfig, null, 2));
70-
fs.closeSync(fd);
90+
const fd = fs.openSync(configPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR, 0o600);
91+
try {
92+
fs.writeFileSync(fd, JSON.stringify(newConfig, null, 2));
93+
fs.fsyncSync(fd);
94+
} finally {
95+
fs.closeSync(fd);
96+
}
7197
console.log(`\n✅ Configuration saved to ${configPath}`);
7298
console.log('Try running: ai-git commit');
7399
return;
@@ -86,17 +112,15 @@ export async function initCommand() {
86112
const backupPath = `${configPath}.bak-${timestamp}`;
87113
fs.renameSync(configPath, backupPath);
88114
console.log(`📦 Existing config backed up to ${backupPath}`);
89-
// Now write new config
90-
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
115+
atomicWriteFileSync(configPath, JSON.stringify(newConfig, null, 2));
91116
} else if (overwriteChoice === 'o' || overwriteChoice === 'overwrite') {
92-
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
117+
atomicWriteFileSync(configPath, JSON.stringify(newConfig, null, 2));
93118
console.log('📝 Overwriting existing config file.');
94119
} else {
95120
console.log('🚫 Initialization canceled. Existing config left unchanged.');
96121
return;
97122
}
98123

99-
fs.chmodSync(configPath, 0o600);
100124
console.log(`\n✅ Configuration saved to ${configPath}`);
101125
console.log('Try running: ai-git commit');
102126

git-ai/src/services/ConfigService.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,21 @@ export class ConfigService {
5555

5656
public saveConfig(newConfig: Config): void {
5757
const validated = ConfigSchema.parse(newConfig);
58-
fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 });
58+
const configPath = ConfigService.CONFIG_PATH;
59+
const tempPath = path.join(path.dirname(configPath), `.tmp-${process.pid}-${Date.now()}`);
60+
const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
61+
try {
62+
fs.writeFileSync(fd, JSON.stringify(validated, null, 2));
63+
fs.fsyncSync(fd);
64+
} finally {
65+
fs.closeSync(fd);
66+
}
67+
try {
68+
fs.renameSync(tempPath, configPath);
69+
} catch (error) {
70+
try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ }
71+
throw error;
72+
}
5973
this.config = validated;
6074
}
6175
}

0 commit comments

Comments
 (0)