From 4e00f5f1e638ec7c2c82ac6ee90a3afa67060033 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Sat, 13 Jun 2026 19:32:48 +0300 Subject: [PATCH] add interactive microservice-selection checklist to install wizard Co-Authored-By: Claude --- java_codebase_rag/installer.py | 76 ++++++++++++++++++++++++++++++- tests/test_installer.py | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/java_codebase_rag/installer.py b/java_codebase_rag/installer.py index ba413931..9c9ac8d2 100644 --- a/java_codebase_rag/installer.py +++ b/java_codebase_rag/installer.py @@ -325,6 +325,66 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list return [HOSTS[name] for name in selected] +def select_microservices( + java_dirs: list[Path], + *, + non_interactive: bool, + preselected: list[str] | None = None, +) -> list[str] | None: + """Show an interactive checklist of detected microservices, all pre-checked. + + Returns None when all are selected (-> microservice_roots omitted, index + everything) or a non-empty subset list. Never returns []. + + Args: + java_dirs: Detected module roots (relative Path names) from + detect_java_directories. Caller must pass len >= 2. + non_interactive: If True, return None (all) without prompting. + preselected: On re-run, the prior microservice_roots subset to pre-check. + """ + # Defensive guard: caller gates on len >= 2, but stay safe if called directly. + if len(java_dirs) < 2: + return None + + dir_names = [str(d) for d in java_dirs] + + if non_interactive: + return None + + preselected_set = set(preselected) if preselected else None + choices = [ + { + "name": name, + "value": name, + "checked": (name in preselected_set) if preselected_set is not None else True, + } + for name in dir_names + ] + + print("Note: Select which modules to index. Toggle with Space, confirm with Enter.") + selected = prompt( + "checkbox", + "Select microservices to index:", + choices=choices, + default=dir_names, # non-TTY fallback returns all -> caller omits key + ) + + if not selected: + retry = prompt( + "confirm", + "At least one module is required. Re-select?", + ) + if retry: + return select_microservices(java_dirs, non_interactive=False, preselected=preselected) + raise SystemExit(2) + + selected_set = set(selected) + if selected_set == set(dir_names): + return None + # Preserve detection order for deterministic YAML output. + return [name for name in dir_names if name in selected_set] + + def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope: """Select 'project' or 'user' scope. @@ -1285,6 +1345,20 @@ def run_install( except SystemExit as e: return e.code + # Stage 1 (Case B): interactive microservice selection (only when 2+ detected) + try: + selected_roots = ( + select_microservices( + java_dirs, + non_interactive=non_interactive, + preselected=existing_config.get("microservice_roots") if existing_config else None, + ) + if len(java_dirs) >= 2 + else None + ) + except SystemExit as e: + return e.code + # Stage 2: Embedding model resolved_model = resolve_model(model, non_interactive=non_interactive) @@ -1327,7 +1401,7 @@ def run_install( yaml_content = generate_yaml_config( source_root, resolved_model, - microservice_roots=[str(d) for d in java_dirs] if len(java_dirs) > 1 else None, + microservice_roots=selected_roots, existing_yaml=existing_config, ) diff --git a/tests/test_installer.py b/tests/test_installer.py index ffcdac5c..ed7dd053 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -153,6 +153,89 @@ def test_detect_java_no_root_no_services_exit_2(self, tmp_path, capsys): assert "Error:" in captured.out and "No Java build files" in captured.out +class TestSelectMicroservices: + """Test select_microservices function.""" + + def test_select_microservices_non_interactive_returns_none(self): + """non_interactive=True with 3 dirs → returns None (all)""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + result = select_microservices(dirs, non_interactive=True) + assert result is None + + def test_select_microservices_non_tty_returns_none_all_selected(self, monkeypatch): + """non-TTY → prompt returns default (all) → returns None""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + result = select_microservices(dirs, non_interactive=False) + assert result is None + + def test_select_microservices_subset_returns_list(self, monkeypatch): + """prompt checkbox returns ['service-a'] of 3 → returns ['service-a']""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + + def fake_prompt(ptype, message, **kw): + return ["service-a"] if ptype == "checkbox" else True + + monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt) + result = select_microservices(dirs, non_interactive=False) + assert result == ["service-a"] + + def test_select_microservices_all_selected_returns_none(self, monkeypatch): + """prompt returns all 3 → returns None""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + all_names = ["service-a", "service-b", "service-c"] + + def fake_prompt(ptype, message, **kw): + return all_names if ptype == "checkbox" else True + + monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt) + result = select_microservices(dirs, non_interactive=False) + assert result is None + + def test_select_microservices_empty_then_decline_exit_2(self, monkeypatch): + """prompt checkbox [] + confirm False → SystemExit(2)""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + + def fake_prompt(ptype, message, **kw): + return [] if ptype == "checkbox" else False + + monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt) + with pytest.raises(SystemExit) as exc_info: + select_microservices(dirs, non_interactive=False) + assert exc_info.value.code == 2 + + def test_select_microservices_preselected_marks_choices(self, monkeypatch): + """preselected=['service-a'] → only service-a has checked=True, result == ['service-a']""" + from java_codebase_rag.installer import select_microservices + dirs = [Path("service-a"), Path("service-b"), Path("service-c")] + captured = {} + + def fake_prompt(ptype, message, **kw): + if ptype == "checkbox": + captured["choices"] = kw["choices"] + return ["service-a"] + return True + + monkeypatch.setattr("java_codebase_rag.installer.prompt", fake_prompt) + result = select_microservices(dirs, non_interactive=False, preselected=["service-a"]) + + checked_names = [c["name"] for c in captured["choices"] if c["checked"]] + assert checked_names == ["service-a"] + assert result == ["service-a"] + + def test_select_microservices_single_dir_returns_none(self): + """len(java_dirs) < 2 → returns None""" + from java_codebase_rag.installer import select_microservices + dirs = [Path(".")] + result = select_microservices(dirs, non_interactive=False) + assert result is None + + class TestConfirmSourceRoot: """Test confirm_source_root function."""