@@ -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+
3153export 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
0 commit comments