@@ -12,7 +12,6 @@ import { logger } from '../utils/logger.js';
1212async function readSecretInput ( rl : readline . Interface , prompt : string ) : Promise < string > {
1313 const rlAny = rl as any ;
1414 const originalWrite = rlAny . _writeToOutput ;
15- // Suppress character echoing: only allow the initial prompt to be written
1615 let promptWritten = false ;
1716 rlAny . _writeToOutput = function _writeToOutput ( str : string ) {
1817 if ( ! promptWritten ) {
@@ -29,6 +28,28 @@ async function readSecretInput(rl: readline.Interface, prompt: string): Promise<
2928 }
3029}
3130
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+ try {
41+ fs . writeFileSync ( fd , content ) ;
42+ fs . fsyncSync ( fd ) ;
43+ } finally {
44+ fs . closeSync ( fd ) ;
45+ }
46+ fs . renameSync ( tempPath , filePath ) ;
47+ } catch ( error ) {
48+ try { fs . unlinkSync ( tempPath ) ; } catch { /* best-effort cleanup */ }
49+ throw error ;
50+ }
51+ }
52+
3253export async function initCommand ( ) {
3354 const rl = readline . createInterface ( {
3455 input : process . stdin ,
@@ -38,6 +59,7 @@ export async function initCommand() {
3859 console . log ( '🚀 Welcome to AI-Git-Terminal Setup\n' ) ;
3960
4061 try {
62+ // --- Step 1: Read API Key ---
4163 let apiKey = '' ;
4264 while ( ! apiKey ) {
4365 const apiKeyInput = await readSecretInput ( rl , '🔑 Enter your Gemini API Key: ' ) ;
@@ -47,56 +69,65 @@ export async function initCommand() {
4769 }
4870 }
4971
72+ // --- Step 2: Read model name ---
5073 const modelInput = await rl . question ( '🤖 Enter model name (default: gemini-1.5-flash): ' ) ;
5174 const model = modelInput . trim ( ) || 'gemini-1.5-flash' ;
5275
76+ // --- Step 3: Build config object ---
5377 const newConfig : Config = {
54- ai : {
55- provider : 'gemini' ,
56- apiKey,
57- model : model ,
58- } ,
59- git : {
60- autoStage : false ,
61- } ,
62- ui : {
63- theme : 'dark' ,
64- showIcons : true ,
65- } ,
78+ ai : { provider : 'gemini' , apiKey, model } ,
79+ git : { autoStage : false } ,
80+ ui : { theme : 'dark' , showIcons : true } ,
6681 } ;
6782
68- // Validate with Zod and persist with restricted permissions (mode 0o600)
83+ // Validate with Zod
6984 ConfigSchema . parse ( newConfig ) ;
7085
7186 const configPath = path . join ( os . homedir ( ) , '.aigitrc' ) ;
7287
73- if ( fs . existsSync ( configPath ) ) {
74- const overwriteChoice = ( await rl . question (
75- '⚠️ Existing config found. Choose [o]verwrite, [b]ackup then replace, or [c]ancel: '
76- ) ) . trim ( ) . toLowerCase ( ) ;
77-
78- if ( overwriteChoice === 'b' || overwriteChoice === 'backup' ) {
79- const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) ;
80- const backupPath = `${ configPath } .bak-${ timestamp } ` ;
81- fs . renameSync ( configPath , backupPath ) ;
82- console . log ( `📦 Existing config backed up to ${ backupPath } ` ) ;
83- } else if ( overwriteChoice === 'o' || overwriteChoice === 'overwrite' ) {
84- console . log ( '📝 Overwriting existing config file.' ) ;
85- } else {
86- console . log ( '🚫 Initialization canceled. Existing config left unchanged.' ) ;
87- return ;
88+ // --- Step 4: Attempt atomic creation ---
89+ try {
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 ) ;
8896 }
97+ console . log ( `\n✅ Configuration saved to ${ configPath } ` ) ;
98+ console . log ( 'Try running: ai-git commit' ) ;
99+ return ;
100+ } catch ( err : any ) {
101+ if ( err . code !== 'EEXIST' ) throw err ;
102+ // File already exists, proceed to backup/overwrite prompt
89103 }
90104
91- fs . writeFileSync ( configPath , JSON . stringify ( newConfig , null , 2 ) , { mode : 0o600 } ) ;
92- fs . chmodSync ( configPath , 0o600 ) ;
105+ // --- Step 5: Handle existing file ---
106+ const overwriteChoice = ( await rl . question (
107+ '⚠️ Existing config found. Choose [o]verwrite, [b]ackup then replace, or [c]ancel: '
108+ ) ) . trim ( ) . toLowerCase ( ) ;
109+
110+ if ( overwriteChoice === 'b' || overwriteChoice === 'backup' ) {
111+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) ;
112+ const backupPath = `${ configPath } .bak-${ timestamp } ` ;
113+ fs . renameSync ( configPath , backupPath ) ;
114+ console . log ( `📦 Existing config backed up to ${ backupPath } ` ) ;
115+ atomicWriteFileSync ( configPath , JSON . stringify ( newConfig , null , 2 ) ) ;
116+ } else if ( overwriteChoice === 'o' || overwriteChoice === 'overwrite' ) {
117+ atomicWriteFileSync ( configPath , JSON . stringify ( newConfig , null , 2 ) ) ;
118+ console . log ( '📝 Overwriting existing config file.' ) ;
119+ } else {
120+ console . log ( '🚫 Initialization canceled. Existing config left unchanged.' ) ;
121+ return ;
122+ }
93123
94124 console . log ( `\n✅ Configuration saved to ${ configPath } ` ) ;
95125 console . log ( 'Try running: ai-git commit' ) ;
126+
96127 } catch ( error ) {
97128 logger . error ( 'Failed to save configuration: ' + ( error instanceof Error ? error . message : String ( error ) ) ) ;
98129 console . error ( '\n❌ Invalid input or failed to write config file.' ) ;
99130 } finally {
100131 rl . close ( ) ;
101132 }
102- }
133+ }
0 commit comments