1+ import fs from 'fs' ;
2+ import path from 'path' ;
3+ import os from 'os' ;
4+ import readline from 'readline/promises' ;
5+ import { ConfigSchema , Config } from '../services/ConfigService.js' ;
6+ import { logger } from '../utils/logger.js' ;
7+
8+ /**
9+ * Reads a secret/password from the terminal without echoing characters.
10+ * Falls back to normal readline if stdin is not a TTY (e.g. piped input).
11+ */
12+ async function readSecretInput ( rl : readline . Interface , prompt : string ) : Promise < string > {
13+ const rlAny = rl as any ;
14+ const originalWrite = rlAny . _writeToOutput ;
15+ // Suppress character echoing: only allow the initial prompt to be written
16+ let promptWritten = false ;
17+ rlAny . _writeToOutput = function _writeToOutput ( str : string ) {
18+ if ( ! promptWritten ) {
19+ promptWritten = true ;
20+ process . stdout . write ( str ) ;
21+ }
22+ // Suppress all subsequent echoed characters
23+ } ;
24+ try {
25+ return await rl . question ( prompt ) ;
26+ } finally {
27+ process . stdout . write ( '\n' ) ;
28+ rlAny . _writeToOutput = originalWrite ;
29+ }
30+ }
31+
32+ export async function initCommand ( ) {
33+ const rl = readline . createInterface ( {
34+ input : process . stdin ,
35+ output : process . stdout ,
36+ } ) ;
37+
38+ console . log ( '🚀 Welcome to AI-Git-Terminal Setup\n' ) ;
39+
40+ try {
41+ let apiKey = '' ;
42+ while ( ! apiKey ) {
43+ const apiKeyInput = await readSecretInput ( rl , '🔑 Enter your Gemini API Key: ' ) ;
44+ apiKey = apiKeyInput . trim ( ) ;
45+ if ( ! apiKey ) {
46+ console . error ( '❌ API key cannot be empty. Please enter a valid key.' ) ;
47+ }
48+ }
49+
50+ const modelInput = await rl . question ( '🤖 Enter model name (default: gemini-1.5-flash): ' ) ;
51+ const model = modelInput . trim ( ) || 'gemini-1.5-flash' ;
52+
53+ 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+ } ,
66+ } ;
67+
68+ // Validate with Zod and persist with restricted permissions (mode 0o600)
69+ ConfigSchema . parse ( newConfig ) ;
70+
71+ const configPath = path . join ( os . homedir ( ) , '.aigitrc' ) ;
72+
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+ }
89+ }
90+
91+ fs . writeFileSync ( configPath , JSON . stringify ( newConfig , null , 2 ) , { mode : 0o600 } ) ;
92+ fs . chmodSync ( configPath , 0o600 ) ;
93+
94+ console . log ( `\n✅ Configuration saved to ${ configPath } ` ) ;
95+ console . log ( 'Try running: ai-git commit' ) ;
96+ } catch ( error ) {
97+ logger . error ( 'Failed to save configuration: ' + ( error instanceof Error ? error . message : String ( error ) ) ) ;
98+ console . error ( '\n❌ Invalid input or failed to write config file.' ) ;
99+ } finally {
100+ rl . close ( ) ;
101+ }
102+ }
0 commit comments