Skip to content

Commit 7bab1f1

Browse files
authored
Merge pull request #18 from BeyteFlow/InitCommand
Refactor config file handling in InitCommand
2 parents a186481 + caf677c commit 7bab1f1

2 files changed

Lines changed: 78 additions & 33 deletions

File tree

git-ai/src/commands/InitCommand.ts

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { logger } from '../utils/logger.js';
1212
async 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+
3253
export 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+
}

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+
try {
63+
fs.writeFileSync(fd, JSON.stringify(validated, null, 2));
64+
fs.fsyncSync(fd);
65+
} finally {
66+
fs.closeSync(fd);
67+
}
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)