1010from semantic_code_intelligence .config .settings import (
1111 AppConfig ,
1212 init_project ,
13+ load_config ,
1314 save_config ,
1415)
1516from semantic_code_intelligence .embeddings .model_registry import (
1617 CLI_PROFILE_CHOICES ,
18+ CORE_PROFILES ,
19+ MODEL_PROFILES ,
20+ ModelProfile ,
21+ PROFILE_ALIASES ,
1722 recommend_profile_for_ram ,
1823 resolve_profile ,
1924)
3035 print_success ,
3136 print_warning ,
3237)
38+ from rich .console import Console
39+ from rich .panel import Panel
40+ from rich .table import Table
3341
3442logger = get_logger ("cli.init" )
3543
@@ -102,8 +110,14 @@ def _generate_vscode_mcp_config(root: Path) -> bool:
102110 "Size aliases (small/base/large) and named aliases (default/quality/code) are supported."
103111 ),
104112)
113+ @click .option (
114+ "--interactive/--no-interactive" ,
115+ "interactive" ,
116+ default = False ,
117+ help = "Launch the interactive installer to choose the embedding model and batch size." ,
118+ )
105119@click .pass_context
106- def init_cmd (ctx : click .Context , path : str , auto_index : bool , setup_vscode : bool , profile_name : str | None ) -> None :
120+ def init_cmd (ctx : click .Context , path : str , auto_index : bool , setup_vscode : bool , profile_name : str | None , interactive : bool ) -> None :
107121 """Initialize a project for semantic code indexing.
108122
109123 Creates a .codexa/ directory with default configuration and an empty index.
@@ -117,27 +131,37 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool
117131 """
118132 root = Path (path ).resolve ()
119133
120- # Check if already initialized
121134 config_dir = AppConfig .config_dir (root )
122- if config_dir .exists ():
123- print_info (f"Project already initialized at { root } " )
124- print_info (f"Config directory: { config_dir } " )
125- # Still allow --vscode and --index on existing projects
126- if setup_vscode :
127- if _generate_vscode_mcp_config (root ):
128- print_success ("VS Code MCP config written to .vscode/settings.json" )
129- else :
130- print_info ("VS Code MCP config already exists" )
131- if auto_index :
132- _run_index (root )
133- return
134-
135135 try :
136- config , config_path = init_project (root )
137- print_success (f"Initialized project at { root } " )
138- print_info (f"Config file: { config_path } " )
139- print_info (f"Index directory: { AppConfig .index_dir (root )} " )
140- logger .debug ("Default config: %s" , config .model_dump ())
136+ if config_dir .exists ():
137+ if not interactive :
138+ print_info (f"Project already initialized at { root } " )
139+ print_info (f"Config directory: { config_dir } " )
140+ # Still allow --vscode and --index on existing projects
141+ if setup_vscode :
142+ if _generate_vscode_mcp_config (root ):
143+ print_success ("VS Code MCP config written to .vscode/settings.json" )
144+ else :
145+ print_info ("VS Code MCP config already exists" )
146+ if auto_index :
147+ _run_index (root )
148+ return
149+
150+ try :
151+ config = load_config (root )
152+ except (json .JSONDecodeError , ValueError , OSError ) as e :
153+ print_error ("Failed to read existing .codexa/config.json. Please fix or delete it and rerun 'codexa init'." )
154+ print_error (f"Details: { e } " )
155+ ctx .exit (1 )
156+ return
157+ print_info (f"Project already initialized at { root } " )
158+ print_info ("Launching interactive installer to update configuration." )
159+ else :
160+ config , config_path = init_project (root )
161+ print_success (f"Initialized project at { root } " )
162+ print_info (f"Config file: { config_path } " )
163+ print_info (f"Index directory: { AppConfig .index_dir (root )} " )
164+ logger .debug ("Default config: %s" , config .model_dump ())
141165 except OSError as e :
142166 print_error (f"Failed to initialize project: { e } " )
143167 ctx .exit (1 )
@@ -149,48 +173,63 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool
149173 available_memory / BYTES_PER_GB if available_memory is not None else None
150174 )
151175
152- # Apply model profile (explicit or RAM-auto-detected)
153- profile = None
176+ recommended_profile = None
154177 if profile_name :
155- profile = resolve_profile (profile_name )
178+ recommended_profile = resolve_profile (profile_name )
156179 elif available_gb is not None :
157- profile = recommend_profile_for_ram (available_gb )
158- print_info (f"Detected { available_gb :.1f} GB available RAM → using '{ profile .name } ' profile ({ profile .label } )" )
159-
160- profile_changed = False
161- if profile :
162- if config .embedding .model_name != profile .model_name :
163- config .embedding .model_name = profile .model_name
164- profile_changed = True
165- print_success (f"Model profile: { profile .label } → { profile .model_name } " )
166- print_info (f" { profile .description } " )
180+ recommended_profile = recommend_profile_for_ram (available_gb )
167181
168182 recommended_batch_size = recommend_batch_size (available_memory , logical_cpu_count )
169- batch_changed = recommended_batch_size != config .embedding .batch_size
170- if batch_changed :
171- config .embedding .batch_size = recommended_batch_size
172183
173- resource_parts : list [str ] = []
174- if available_gb is not None :
175- resource_parts .append (f"{ available_gb :.1f} GB RAM" )
176- if logical_cpu_count is not None :
177- core_label = "CPU core" if logical_cpu_count == 1 else "CPU cores"
178- resource_parts .append (f"{ logical_cpu_count } { core_label } " )
179-
180- batch_message_prefix = (
181- f"Embedding batch size { 'updated' if batch_changed else 'kept' } "
182- f"at { config .embedding .batch_size } "
183- )
184- if resource_parts :
185- print_info (
186- f"{ batch_message_prefix } (based on { ', ' .join (resource_parts )} )"
184+ if interactive :
185+ profile_changed , batch_changed = _run_interactive_installer (
186+ config = config ,
187+ available_gb = available_gb ,
188+ cpu_count = logical_cpu_count ,
189+ default_profile = recommended_profile or MODEL_PROFILES ["balanced" ],
190+ recommended_batch_size = recommended_batch_size ,
187191 )
192+ should_save = profile_changed or batch_changed
188193 else :
189- print_info (
190- f"{ batch_message_prefix } (using default recommendation)"
194+ # Apply model profile (explicit or RAM-auto-detected)
195+ profile = recommended_profile
196+ profile_changed = False
197+ if profile :
198+ if profile_name is None and available_gb is not None :
199+ print_info (f"Detected { available_gb :.1f} GB available RAM → using '{ profile .name } ' profile ({ profile .label } )" )
200+
201+ if config .embedding .model_name != profile .model_name :
202+ config .embedding .model_name = profile .model_name
203+ profile_changed = True
204+ print_success (f"Model profile: { profile .label } → { profile .model_name } " )
205+ print_info (f" { profile .description } " )
206+
207+ batch_changed = recommended_batch_size != config .embedding .batch_size
208+ if batch_changed :
209+ config .embedding .batch_size = recommended_batch_size
210+
211+ resource_parts : list [str ] = []
212+ if available_gb is not None :
213+ resource_parts .append (f"{ available_gb :.1f} GB RAM" )
214+ if logical_cpu_count is not None :
215+ core_label = "CPU core" if logical_cpu_count == 1 else "CPU cores"
216+ resource_parts .append (f"{ logical_cpu_count } { core_label } " )
217+
218+ batch_message_prefix = (
219+ f"Embedding batch size { 'updated' if batch_changed else 'kept' } "
220+ f"at { config .embedding .batch_size } "
191221 )
222+ if resource_parts :
223+ print_info (
224+ f"{ batch_message_prefix } (based on { ', ' .join (resource_parts )} )"
225+ )
226+ else :
227+ print_info (
228+ f"{ batch_message_prefix } (using default recommendation)"
229+ )
230+
231+ should_save = profile_changed or batch_changed
192232
193- should_save = profile_changed or batch_changed
194233 if should_save :
195234 save_config (config , root )
196235
@@ -210,6 +249,98 @@ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool
210249 print_info (" .codexaignore — Exclude secrets or generated files from indexing" )
211250
212251
252+ def _run_interactive_installer (
253+ config : AppConfig ,
254+ available_gb : float | None ,
255+ cpu_count : int | None ,
256+ default_profile : ModelProfile ,
257+ recommended_batch_size : int ,
258+ ) -> tuple [bool , bool ]:
259+ """Launch a text-based interactive installer for model and batch settings."""
260+ console = Console ()
261+ console .print ()
262+ console .print (Panel .fit ("[bold cyan]CodexA Interactive Installer[/bold cyan]\n Configure embedding defaults for your project." , border_style = "cyan" ))
263+
264+ # Resource summary and suggestions
265+ resource_lines : list [str ] = []
266+ if available_gb is not None :
267+ resource_lines .append (f"[green]{ available_gb :.1f} GB[/green] available RAM detected" )
268+ if cpu_count is not None :
269+ resource_lines .append (f"[green]{ cpu_count } CPU cores[/green] detected" )
270+ if resource_lines :
271+ console .print (" • " .join (resource_lines ))
272+ console .print (f"Suggested profile: [bold]{ default_profile .label } [/bold]" )
273+ console .print (f"Suggested batch size: [bold]{ recommended_batch_size } [/bold]" )
274+ else :
275+ console .print ("System resources could not be detected; keeping safe defaults." )
276+
277+ # Show model options
278+ table = Table (title = "Embedding Profiles" , show_lines = True )
279+ table .add_column ("Key" , justify = "center" , style = "cyan" , no_wrap = True )
280+ table .add_column ("Label" )
281+ table .add_column ("Model" )
282+ table .add_column ("Description" )
283+ table .add_column ("Min RAM (GB)" , justify = "right" )
284+ for key in CORE_PROFILES :
285+ profile = MODEL_PROFILES [key ]
286+ table .add_row (
287+ profile .name ,
288+ profile .label ,
289+ profile .model_name ,
290+ profile .description ,
291+ f"{ profile .min_ram_gb :.1f} " ,
292+ )
293+ console .print (table )
294+
295+ chosen_profile_key = click .prompt (
296+ "Select embedding profile" ,
297+ type = click .Choice (CLI_PROFILE_CHOICES , case_sensitive = False ),
298+ default = default_profile .name ,
299+ show_choices = False ,
300+ )
301+ chosen_profile = resolve_profile (chosen_profile_key )
302+ if chosen_profile is None :
303+ valid_profiles = sorted (set (MODEL_PROFILES .keys ()) | set (PROFILE_ALIASES .keys ()))
304+ raise click .ClickException (
305+ f"Profile '{ chosen_profile_key } ' could not be resolved. "
306+ f"Valid profiles are: { ', ' .join (valid_profiles )} ."
307+ )
308+
309+ profile_changed = False
310+ if config .embedding .model_name != chosen_profile .model_name :
311+ config .embedding .model_name = chosen_profile .model_name
312+ profile_changed = True
313+
314+ console .print ()
315+ console .print (
316+ Panel .fit (
317+ f"[bold]Batch size[/bold] controls how many chunks are embedded at once.\n "
318+ f"Recommended: [cyan]{ recommended_batch_size } [/cyan] (based on detection)." ,
319+ border_style = "cyan" ,
320+ )
321+ )
322+
323+ batch_input = click .prompt (
324+ "Embedding batch size" ,
325+ default = recommended_batch_size ,
326+ type = click .IntRange (1 , 1024 ),
327+ show_default = True ,
328+ )
329+ batch_changed = batch_input != config .embedding .batch_size
330+ config .embedding .batch_size = batch_input
331+
332+ console .print ()
333+ console .print (
334+ Panel .fit (
335+ f"Using profile [green]{ chosen_profile .label } [/green] ({ chosen_profile .model_name } ) "
336+ f"with batch size [green]{ config .embedding .batch_size } [/green]." ,
337+ border_style = "green" ,
338+ )
339+ )
340+
341+ return profile_changed , batch_changed
342+
343+
213344def _run_index (root : Path ) -> None :
214345 """Run indexing as part of init."""
215346 from semantic_code_intelligence .services .indexing_service import index_project
0 commit comments