From 72d423b35effe962a60fc26e6c46d6511e1dc4d6 Mon Sep 17 00:00:00 2001 From: Naheel Muhammed Date: Mon, 23 Mar 2026 12:16:11 +0530 Subject: [PATCH 1/5] Refactor config file handling in InitCommand --- git-ai/src/commands/InitCommand.ts | 71 ++++++++++++++++-------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/git-ai/src/commands/InitCommand.ts b/git-ai/src/commands/InitCommand.ts index 655831b..886cd3d 100644 --- a/git-ai/src/commands/InitCommand.ts +++ b/git-ai/src/commands/InitCommand.ts @@ -12,7 +12,6 @@ import { logger } from '../utils/logger.js'; async function readSecretInput(rl: readline.Interface, prompt: string): Promise { const rlAny = rl as any; const originalWrite = rlAny._writeToOutput; - // Suppress character echoing: only allow the initial prompt to be written let promptWritten = false; rlAny._writeToOutput = function _writeToOutput(str: string) { if (!promptWritten) { @@ -38,6 +37,7 @@ export async function initCommand() { console.log('šŸš€ Welcome to AI-Git-Terminal Setup\n'); try { + // --- Step 1: Read API Key --- let apiKey = ''; while (!apiKey) { const apiKeyInput = await readSecretInput(rl, 'šŸ”‘ Enter your Gemini API Key: '); @@ -47,56 +47,63 @@ export async function initCommand() { } } + // --- Step 2: Read model name --- const modelInput = await rl.question('šŸ¤– Enter model name (default: gemini-1.5-flash): '); const model = modelInput.trim() || 'gemini-1.5-flash'; + // --- Step 3: Build config object --- const newConfig: Config = { - ai: { - provider: 'gemini', - apiKey, - model: model, - }, - git: { - autoStage: false, - }, - ui: { - theme: 'dark', - showIcons: true, - }, + ai: { provider: 'gemini', apiKey, model }, + git: { autoStage: false }, + ui: { theme: 'dark', showIcons: true }, }; - // Validate with Zod and persist with restricted permissions (mode 0o600) + // Validate with Zod ConfigSchema.parse(newConfig); const configPath = path.join(os.homedir(), '.aigitrc'); - if (fs.existsSync(configPath)) { - const overwriteChoice = (await rl.question( - 'āš ļø Existing config found. Choose [o]verwrite, [b]ackup then replace, or [c]ancel: ' - )).trim().toLowerCase(); + // --- Step 4: Attempt atomic creation --- + try { + const fd = fs.openSync(configPath, fs.O_CREAT | fs.O_EXCL | fs.O_RDWR, 0o600); + fs.writeFileSync(fd, JSON.stringify(newConfig, null, 2)); + fs.closeSync(fd); + console.log(`\nāœ… Configuration saved to ${configPath}`); + console.log('Try running: ai-git commit'); + return; + } catch (err: any) { + if (err.code !== 'EEXIST') throw err; + // File already exists, proceed to backup/overwrite prompt + } - if (overwriteChoice === 'b' || overwriteChoice === 'backup') { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupPath = `${configPath}.bak-${timestamp}`; - fs.renameSync(configPath, backupPath); - console.log(`šŸ“¦ Existing config backed up to ${backupPath}`); - } else if (overwriteChoice === 'o' || overwriteChoice === 'overwrite') { - console.log('šŸ“ Overwriting existing config file.'); - } else { - console.log('🚫 Initialization canceled. Existing config left unchanged.'); - return; - } + // --- Step 5: Handle existing file --- + const overwriteChoice = (await rl.question( + 'āš ļø Existing config found. Choose [o]verwrite, [b]ackup then replace, or [c]ancel: ' + )).trim().toLowerCase(); + + if (overwriteChoice === 'b' || overwriteChoice === 'backup') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${configPath}.bak-${timestamp}`; + fs.renameSync(configPath, backupPath); + console.log(`šŸ“¦ Existing config backed up to ${backupPath}`); + // Now write new config + fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 }); + } else if (overwriteChoice === 'o' || overwriteChoice === 'overwrite') { + fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 }); + console.log('šŸ“ Overwriting existing config file.'); + } else { + console.log('🚫 Initialization canceled. Existing config left unchanged.'); + return; } - fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 }); fs.chmodSync(configPath, 0o600); - console.log(`\nāœ… Configuration saved to ${configPath}`); console.log('Try running: ai-git commit'); + } catch (error) { logger.error('Failed to save configuration: ' + (error instanceof Error ? error.message : String(error))); console.error('\nāŒ Invalid input or failed to write config file.'); } finally { rl.close(); } -} \ No newline at end of file +} From 9b8f8c93243c0a7f41d9ad0d62eb7e2958138be4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:56:50 +0000 Subject: [PATCH 2/5] Initial plan From a65b3a61f5b97c436ee3c2041860cb573c0183d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:00:43 +0000 Subject: [PATCH 3/5] Fix fs.constants flags, add try/finally for fd, and use atomic writes in InitCommand and ConfigService Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> Agent-Logs-Url: https://github.com/BeyteFlow/git-ai/sessions/53acba39-daa6-4e19-91d9-07b21288984a --- git-ai/src/commands/InitCommand.ts | 38 +++++++++++++++++++++++----- git-ai/src/services/ConfigService.ts | 16 +++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/git-ai/src/commands/InitCommand.ts b/git-ai/src/commands/InitCommand.ts index 886cd3d..a382f83 100644 --- a/git-ai/src/commands/InitCommand.ts +++ b/git-ai/src/commands/InitCommand.ts @@ -28,6 +28,28 @@ async function readSecretInput(rl: readline.Interface, prompt: string): Promise< } } +/** + * Atomically writes content to filePath using a temp file + fsync + rename, + * ensuring the file has permissions 0o600. + */ +function atomicWriteFileSync(filePath: string, content: string): void { + const dir = path.dirname(filePath); + const tempPath = path.join(dir, `.tmp-${process.pid}-${Date.now()}`); + const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600); + try { + fs.writeFileSync(fd, content); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + try { + fs.renameSync(tempPath, filePath); + } catch (error) { + try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ } + throw error; + } +} + export async function initCommand() { const rl = readline.createInterface({ input: process.stdin, @@ -65,9 +87,13 @@ export async function initCommand() { // --- Step 4: Attempt atomic creation --- try { - const fd = fs.openSync(configPath, fs.O_CREAT | fs.O_EXCL | fs.O_RDWR, 0o600); - fs.writeFileSync(fd, JSON.stringify(newConfig, null, 2)); - fs.closeSync(fd); + const fd = fs.openSync(configPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR, 0o600); + try { + fs.writeFileSync(fd, JSON.stringify(newConfig, null, 2)); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } console.log(`\nāœ… Configuration saved to ${configPath}`); console.log('Try running: ai-git commit'); return; @@ -86,17 +112,15 @@ export async function initCommand() { const backupPath = `${configPath}.bak-${timestamp}`; fs.renameSync(configPath, backupPath); console.log(`šŸ“¦ Existing config backed up to ${backupPath}`); - // Now write new config - fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 }); + atomicWriteFileSync(configPath, JSON.stringify(newConfig, null, 2)); } else if (overwriteChoice === 'o' || overwriteChoice === 'overwrite') { - fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), { mode: 0o600 }); + atomicWriteFileSync(configPath, JSON.stringify(newConfig, null, 2)); console.log('šŸ“ Overwriting existing config file.'); } else { console.log('🚫 Initialization canceled. Existing config left unchanged.'); return; } - fs.chmodSync(configPath, 0o600); console.log(`\nāœ… Configuration saved to ${configPath}`); console.log('Try running: ai-git commit'); diff --git a/git-ai/src/services/ConfigService.ts b/git-ai/src/services/ConfigService.ts index 16d7453..62095cd 100644 --- a/git-ai/src/services/ConfigService.ts +++ b/git-ai/src/services/ConfigService.ts @@ -55,7 +55,21 @@ export class ConfigService { public saveConfig(newConfig: Config): void { const validated = ConfigSchema.parse(newConfig); - fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 }); + const configPath = ConfigService.CONFIG_PATH; + const tempPath = path.join(path.dirname(configPath), `.tmp-${process.pid}-${Date.now()}`); + const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600); + try { + fs.writeFileSync(fd, JSON.stringify(validated, null, 2)); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + try { + fs.renameSync(tempPath, configPath); + } catch (error) { + try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ } + throw error; + } this.config = validated; } } \ No newline at end of file From 2c8ac734b866dcb859fea942e7b5fb484d791368 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:11:29 +0000 Subject: [PATCH 4/5] Initial plan From ad25fa1e07ad007e26535c07f233dfedf46ef0e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:15:15 +0000 Subject: [PATCH 5/5] Fix tempPath leak: unlink on write/fsync failure in atomic write helpers Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> Agent-Logs-Url: https://github.com/BeyteFlow/git-ai/sessions/41d3f316-2d69-4a6b-a512-943d22a1408b --- git-ai/src/commands/InitCommand.ts | 12 ++++++------ git-ai/src/services/ConfigService.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/git-ai/src/commands/InitCommand.ts b/git-ai/src/commands/InitCommand.ts index a382f83..23398e1 100644 --- a/git-ai/src/commands/InitCommand.ts +++ b/git-ai/src/commands/InitCommand.ts @@ -37,12 +37,12 @@ function atomicWriteFileSync(filePath: string, content: string): void { const tempPath = path.join(dir, `.tmp-${process.pid}-${Date.now()}`); const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600); try { - fs.writeFileSync(fd, content); - fs.fsyncSync(fd); - } finally { - fs.closeSync(fd); - } - try { + try { + fs.writeFileSync(fd, content); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } fs.renameSync(tempPath, filePath); } catch (error) { try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ } diff --git a/git-ai/src/services/ConfigService.ts b/git-ai/src/services/ConfigService.ts index 62095cd..a2de320 100644 --- a/git-ai/src/services/ConfigService.ts +++ b/git-ai/src/services/ConfigService.ts @@ -59,12 +59,12 @@ export class ConfigService { const tempPath = path.join(path.dirname(configPath), `.tmp-${process.pid}-${Date.now()}`); const fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600); try { - fs.writeFileSync(fd, JSON.stringify(validated, null, 2)); - fs.fsyncSync(fd); - } finally { - fs.closeSync(fd); - } - try { + try { + fs.writeFileSync(fd, JSON.stringify(validated, null, 2)); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } fs.renameSync(tempPath, configPath); } catch (error) { try { fs.unlinkSync(tempPath); } catch { /* best-effort cleanup */ }