java-codebase-rag 0.5.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {java_codebase_rag-0.5.0/java_codebase_rag.egg-info → java_codebase_rag-0.5.1}/PKG-INFO +1 -1
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/cli.py +31 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/installer.py +418 -9
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1/java_codebase_rag.egg-info}/PKG-INFO +1 -1
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/pyproject.toml +1 -1
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_installer.py +413 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/LICENSE +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/README.md +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/ast_java.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/brownfield_events.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/build_ast_graph.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/chunk_heuristics.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/graph_enrich.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/index_common.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/__init__.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/cli_format.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/cli_progress.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/config.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag/pipeline.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/SOURCES.txt +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/entry_points.txt +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/requires.txt +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/top_level.txt +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_index_flow_lancedb.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_index_v1_common.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_ontology.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/kuzu_queries.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/mcp_hints.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/mcp_v2.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/path_filtering.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/pr_analysis.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/search_lancedb.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/server.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/setup.cfg +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_agent_skills_static.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_assign_endpoint_client_extraction.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_ast_graph_build.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_ast_java_calls.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_ast_java_capabilities.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_bank_chat_brownfield_integration.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_brownfield_clients.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_brownfield_events.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_brownfield_overrides.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_brownfield_routes.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_edge_matching.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_edges_e2e.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_graph_receiver_resolution.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_graph_smoke_roundtrip.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_invariant.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_cli_progress_stdout_invariant.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_cli_quiet_parity.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_client_hint_recovery.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_client_node_extraction.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_client_role_rename.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_config.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_cross_service_resolution_flag.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_edge_navigation_doc.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_feign_not_exposer.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_graph_enrich.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_incremental_graph.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_installer_integration.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_java_codebase_rag_cli.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_kuzu_queries.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_lancedb_e2e.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_mcp_hints.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_mcp_server_project_root.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_mcp_tools.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_mcp_v2.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_mcp_v2_compose.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_meta_chain_core.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_microservice_scope.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_outgoing_call_extraction.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_packaging_metadata.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_path_filtering.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_pr_analysis.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_route_extraction.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_schema_consistency.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_search_lancedb.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_search_lancedb_capability.py +0 -0
- {java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_string_value_atoms.py +0 -0
|
@@ -496,6 +496,15 @@ def _cmd_install(args: argparse.Namespace) -> int:
|
|
|
496
496
|
)
|
|
497
497
|
|
|
498
498
|
|
|
499
|
+
def _cmd_update(args: argparse.Namespace) -> int:
|
|
500
|
+
from java_codebase_rag.installer import run_update
|
|
501
|
+
|
|
502
|
+
return run_update(
|
|
503
|
+
force=bool(args.force),
|
|
504
|
+
dry_run=bool(args.dry_run),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
499
508
|
def _cmd_erase(args: argparse.Namespace) -> int:
|
|
500
509
|
cfg = _resolved_from_ns(args)
|
|
501
510
|
_startup_hints(cfg)
|
|
@@ -760,6 +769,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
760
769
|
_add_verbosity_flags(install)
|
|
761
770
|
install.set_defaults(handler=_cmd_install)
|
|
762
771
|
|
|
772
|
+
update = subparsers.add_parser(
|
|
773
|
+
"update",
|
|
774
|
+
help="Refresh shipped artifacts (skill, agent, MCP entry) after pip upgrade.",
|
|
775
|
+
description=(
|
|
776
|
+
"Post-upgrade refresh: overwrites skill and agent files with the latest "
|
|
777
|
+
"shipped versions and updates the MCP command path. Use --dry-run to "
|
|
778
|
+
"preview changes without writing. Requires a prior `install` run."
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
update.add_argument(
|
|
782
|
+
"--force",
|
|
783
|
+
action="store_true",
|
|
784
|
+
help="Overwrite all artifacts even if content matches.",
|
|
785
|
+
)
|
|
786
|
+
update.add_argument(
|
|
787
|
+
"--dry-run",
|
|
788
|
+
action="store_true",
|
|
789
|
+
help="Print changes without writing files.",
|
|
790
|
+
)
|
|
791
|
+
_add_verbosity_flags(update)
|
|
792
|
+
update.set_defaults(handler=_cmd_update)
|
|
793
|
+
|
|
763
794
|
increment = subparsers.add_parser(
|
|
764
795
|
"increment",
|
|
765
796
|
help="Pick up changes since the last index update.",
|
|
@@ -22,6 +22,14 @@ import yaml
|
|
|
22
22
|
|
|
23
23
|
Scope = Literal["project", "user"]
|
|
24
24
|
|
|
25
|
+
# MCP server name constant
|
|
26
|
+
_MCP_SERVER_NAME = "java-codebase-rag"
|
|
27
|
+
|
|
28
|
+
# Exit code constants
|
|
29
|
+
EXIT_SUCCESS = 0
|
|
30
|
+
EXIT_PARTIAL = 1
|
|
31
|
+
EXIT_FATAL = 2
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
class ArtifactResult(NamedTuple):
|
|
27
35
|
"""Result of deploying a single artifact."""
|
|
@@ -111,16 +119,27 @@ def prompt(
|
|
|
111
119
|
|
|
112
120
|
# Lazy import questionary only when needed (TTY)
|
|
113
121
|
import questionary
|
|
122
|
+
from prompt_toolkit.styles import Style
|
|
123
|
+
|
|
124
|
+
# Strip default ANSI colors — rely on ●/○ indicators only, no fg/bg highlights
|
|
125
|
+
# noinherit prevents prompt_toolkit from merging in questionary's default fg colors
|
|
126
|
+
no_color_style = Style(
|
|
127
|
+
[
|
|
128
|
+
("highlighted", "noinherit"),
|
|
129
|
+
("selected", "noinherit"),
|
|
130
|
+
("pointer", "noinherit bold"),
|
|
131
|
+
]
|
|
132
|
+
)
|
|
114
133
|
|
|
115
134
|
try:
|
|
116
135
|
if prompt_type == "checkbox":
|
|
117
|
-
return questionary.checkbox(message, choices=choices).ask()
|
|
136
|
+
return questionary.checkbox(message, choices=choices, style=no_color_style).ask()
|
|
118
137
|
elif prompt_type == "select":
|
|
119
|
-
return questionary.select(message, choices=choices).ask()
|
|
138
|
+
return questionary.select(message, choices=choices, style=no_color_style).ask()
|
|
120
139
|
elif prompt_type == "text":
|
|
121
|
-
return questionary.text(message, default=default).ask()
|
|
140
|
+
return questionary.text(message, default=default, style=no_color_style).ask()
|
|
122
141
|
elif prompt_type == "confirm":
|
|
123
|
-
return questionary.confirm(message).ask()
|
|
142
|
+
return questionary.confirm(message, style=no_color_style).ask()
|
|
124
143
|
else:
|
|
125
144
|
raise ValueError(f"Unknown prompt_type: {prompt_type}")
|
|
126
145
|
except KeyboardInterrupt:
|
|
@@ -279,10 +298,15 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list
|
|
|
279
298
|
print(f"Valid agents: {', '.join(HOSTS.keys())}")
|
|
280
299
|
raise SystemExit(2)
|
|
281
300
|
|
|
282
|
-
# Interactive: show checkbox with
|
|
301
|
+
# Interactive: show checkbox with claude-code pre-selected (most common)
|
|
302
|
+
# Changed from all pre-selected to avoid confusion
|
|
283
303
|
host_names = list(HOSTS.keys())
|
|
284
|
-
choices = [
|
|
304
|
+
choices = [
|
|
305
|
+
{"name": name, "value": name, "checked": (name == "claude-code")}
|
|
306
|
+
for name in host_names
|
|
307
|
+
]
|
|
285
308
|
|
|
309
|
+
print("Note: You can select multiple agent hosts with Space. Navigate with arrow keys.")
|
|
286
310
|
selected = prompt("checkbox", "Select agent hosts to configure:", choices=choices)
|
|
287
311
|
|
|
288
312
|
if not selected:
|
|
@@ -296,6 +320,8 @@ def select_hosts(*, non_interactive: bool, cli_agents: list[str] | None) -> list
|
|
|
296
320
|
else:
|
|
297
321
|
raise SystemExit(2)
|
|
298
322
|
|
|
323
|
+
# Show confirmation of what will be deployed
|
|
324
|
+
print(f"Will deploy to: {', '.join(selected)}")
|
|
299
325
|
return [HOSTS[name] for name in selected]
|
|
300
326
|
|
|
301
327
|
|
|
@@ -319,6 +345,8 @@ def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
|
|
|
319
345
|
return "project"
|
|
320
346
|
|
|
321
347
|
# Interactive: prompt for scope
|
|
348
|
+
print("Note: 'project' scope stores configs in the project directory.")
|
|
349
|
+
print(" 'user' scope stores configs in your home directory.")
|
|
322
350
|
selected = prompt(
|
|
323
351
|
"select",
|
|
324
352
|
"Select installation scope:",
|
|
@@ -328,6 +356,7 @@ def select_scope(*, non_interactive: bool, cli_scope: str | None) -> Scope:
|
|
|
328
356
|
if not selected:
|
|
329
357
|
return "project"
|
|
330
358
|
|
|
359
|
+
print(f"Selected scope: {selected}")
|
|
331
360
|
return selected # type: ignore
|
|
332
361
|
|
|
333
362
|
|
|
@@ -422,14 +451,14 @@ def merge_mcp_config(config_path: Path, host: HostConfig, *, mcp_command: str) -
|
|
|
422
451
|
|
|
423
452
|
# Prepare new entry
|
|
424
453
|
new_entry = {"command": mcp_command, "type": "stdio"}
|
|
425
|
-
existing_entry = config["mcpServers"].get(
|
|
454
|
+
existing_entry = config["mcpServers"].get(_MCP_SERVER_NAME)
|
|
426
455
|
|
|
427
456
|
# Check if entry already exists with same config
|
|
428
457
|
if existing_entry == new_entry:
|
|
429
458
|
return False
|
|
430
459
|
|
|
431
460
|
# Merge/update entry
|
|
432
|
-
config["mcpServers"][
|
|
461
|
+
config["mcpServers"][_MCP_SERVER_NAME] = new_entry
|
|
433
462
|
|
|
434
463
|
# Write atomically (write to tmp, then rename)
|
|
435
464
|
tmp_name = None
|
|
@@ -738,7 +767,8 @@ def run_init_if_needed(
|
|
|
738
767
|
)
|
|
739
768
|
from java_codebase_rag.pipeline import run_build_ast_graph, run_cocoindex_update
|
|
740
769
|
|
|
741
|
-
|
|
770
|
+
has_existing, _ = index_dir_has_existing_artifacts(index_dir)
|
|
771
|
+
if has_existing:
|
|
742
772
|
print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
|
|
743
773
|
return False
|
|
744
774
|
|
|
@@ -762,6 +792,8 @@ def run_init_if_needed(
|
|
|
762
792
|
g = run_build_ast_graph(
|
|
763
793
|
source_root=cfg.source_root,
|
|
764
794
|
kuzu_path=cfg.kuzu_path,
|
|
795
|
+
verbose=not quiet,
|
|
796
|
+
quiet=quiet,
|
|
765
797
|
env=env,
|
|
766
798
|
)
|
|
767
799
|
if g.returncode != 0:
|
|
@@ -823,6 +855,383 @@ def handle_rerun(cwd: Path, *, non_interactive: bool) -> dict | None:
|
|
|
823
855
|
return existing_config
|
|
824
856
|
|
|
825
857
|
|
|
858
|
+
def detect_configured_hosts(cwd: Path) -> list[tuple[HostConfig, str]]:
|
|
859
|
+
"""Scan project + user config files for java-codebase-rag MCP entries.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
cwd: Current working directory (for project-scope configs)
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
List of (host_config, scope) tuples where scope is "project" or "user"
|
|
866
|
+
"""
|
|
867
|
+
detected = []
|
|
868
|
+
|
|
869
|
+
# Check all hosts in both project and user scopes
|
|
870
|
+
for host_name, host_config in HOSTS.items():
|
|
871
|
+
# Check project scope
|
|
872
|
+
project_mcp_path = host_config.mcp_config_path("project", cwd)
|
|
873
|
+
if _has_java_codebase_rag_entry(project_mcp_path):
|
|
874
|
+
detected.append((host_config, "project"))
|
|
875
|
+
|
|
876
|
+
# Check user scope
|
|
877
|
+
user_mcp_path = host_config.mcp_config_path("user", cwd)
|
|
878
|
+
if _has_java_codebase_rag_entry(user_mcp_path):
|
|
879
|
+
detected.append((host_config, "user"))
|
|
880
|
+
|
|
881
|
+
return detected
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _has_java_codebase_rag_entry(config_path: Path) -> bool:
|
|
885
|
+
"""Check if MCP config file has a java-codebase-rag entry.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
config_path: Path to MCP config file
|
|
889
|
+
|
|
890
|
+
Returns:
|
|
891
|
+
True if file exists and contains java-codebase-rag in mcpServers
|
|
892
|
+
"""
|
|
893
|
+
if not config_path.is_file():
|
|
894
|
+
return False
|
|
895
|
+
|
|
896
|
+
try:
|
|
897
|
+
with open(config_path, "r") as f:
|
|
898
|
+
config = json.load(f)
|
|
899
|
+
except (json.JSONDecodeError, IOError, OSError):
|
|
900
|
+
return False
|
|
901
|
+
|
|
902
|
+
mcp_servers = config.get("mcpServers", {})
|
|
903
|
+
return _MCP_SERVER_NAME in mcp_servers
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def refresh_artifacts(
|
|
907
|
+
host: HostConfig,
|
|
908
|
+
scope: str,
|
|
909
|
+
cwd: Path,
|
|
910
|
+
*,
|
|
911
|
+
force: bool,
|
|
912
|
+
dry_run: bool,
|
|
913
|
+
) -> list[ArtifactResult]:
|
|
914
|
+
"""Overwrite skill and agent files from package data. Skip MCP if entry is correct.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
host: HostConfig for the agent host
|
|
918
|
+
scope: Installation scope ("project" or "user")
|
|
919
|
+
cwd: Current working directory
|
|
920
|
+
force: If True, overwrite all files even if matching
|
|
921
|
+
dry_run: If True, print changes without writing
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
List of ArtifactResult objects for each artifact
|
|
925
|
+
"""
|
|
926
|
+
results = []
|
|
927
|
+
|
|
928
|
+
# Refresh skill file
|
|
929
|
+
skills_dir = host.skills_dir(scope, cwd)
|
|
930
|
+
skill_dest = skills_dir / "explore-codebase" / "SKILL.md"
|
|
931
|
+
skill_result = _refresh_file(
|
|
932
|
+
skill_dest,
|
|
933
|
+
"skills/explore-codebase/SKILL.md",
|
|
934
|
+
artifact_type="skill",
|
|
935
|
+
force=force,
|
|
936
|
+
dry_run=dry_run,
|
|
937
|
+
)
|
|
938
|
+
results.append(skill_result)
|
|
939
|
+
|
|
940
|
+
# Refresh agent file
|
|
941
|
+
agents_dir = host.agents_dir(scope, cwd)
|
|
942
|
+
agent_dest = agents_dir / "explorer-rag-enhanced.md"
|
|
943
|
+
agent_result = _refresh_file(
|
|
944
|
+
agent_dest,
|
|
945
|
+
"agents/explorer-rag-enhanced.md",
|
|
946
|
+
artifact_type="agent",
|
|
947
|
+
force=force,
|
|
948
|
+
dry_run=dry_run,
|
|
949
|
+
)
|
|
950
|
+
results.append(agent_result)
|
|
951
|
+
|
|
952
|
+
# Refresh MCP config (update command path if needed)
|
|
953
|
+
mcp_config_path = host.mcp_config_path(scope, cwd)
|
|
954
|
+
mcp_result = _refresh_mcp_config(mcp_config_path, host, force=force, dry_run=dry_run)
|
|
955
|
+
results.append(mcp_result)
|
|
956
|
+
|
|
957
|
+
return results
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _refresh_file(
|
|
961
|
+
dest_path: Path,
|
|
962
|
+
package_relative_path: str,
|
|
963
|
+
*,
|
|
964
|
+
artifact_type: str,
|
|
965
|
+
force: bool,
|
|
966
|
+
dry_run: bool,
|
|
967
|
+
) -> ArtifactResult:
|
|
968
|
+
"""Refresh a single file from package data.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
dest_path: Destination file path
|
|
972
|
+
package_relative_path: Path relative to install_data
|
|
973
|
+
artifact_type: Type of artifact (for error messages)
|
|
974
|
+
force: If True, overwrite even if matching
|
|
975
|
+
dry_run: If True, print without writing
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
ArtifactResult with success status
|
|
979
|
+
"""
|
|
980
|
+
try:
|
|
981
|
+
# Read package data
|
|
982
|
+
package_content = _read_package_artifact(package_relative_path)
|
|
983
|
+
|
|
984
|
+
# Check if file exists
|
|
985
|
+
if dest_path.is_file():
|
|
986
|
+
existing_content = dest_path.read_text(encoding="utf-8")
|
|
987
|
+
|
|
988
|
+
# Skip if content matches and not forcing
|
|
989
|
+
if package_content == existing_content and not force:
|
|
990
|
+
return ArtifactResult(path=dest_path, success=True, error=None)
|
|
991
|
+
|
|
992
|
+
# Content differs or force mode
|
|
993
|
+
if dry_run:
|
|
994
|
+
print(f"Would update {artifact_type} file at {dest_path}")
|
|
995
|
+
return ArtifactResult(path=dest_path, success=True, error=None)
|
|
996
|
+
|
|
997
|
+
elif dry_run:
|
|
998
|
+
print(f"Would create {artifact_type} file at {dest_path}")
|
|
999
|
+
return ArtifactResult(path=dest_path, success=True, error=None)
|
|
1000
|
+
|
|
1001
|
+
# Ensure parent directory exists
|
|
1002
|
+
if not dry_run:
|
|
1003
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1004
|
+
|
|
1005
|
+
# Check writability
|
|
1006
|
+
if not _is_writable(dest_path.parent):
|
|
1007
|
+
return ArtifactResult(
|
|
1008
|
+
path=dest_path,
|
|
1009
|
+
success=False,
|
|
1010
|
+
error=f"Directory not writable: {dest_path.parent}",
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
# Write file (skip in dry_run mode)
|
|
1014
|
+
if not dry_run:
|
|
1015
|
+
dest_path.write_text(package_content, encoding="utf-8")
|
|
1016
|
+
print(f"Updated {artifact_type} file at {dest_path}")
|
|
1017
|
+
|
|
1018
|
+
return ArtifactResult(path=dest_path, success=True, error=None)
|
|
1019
|
+
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
return ArtifactResult(path=dest_path, success=False, error=str(e))
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _refresh_mcp_config(
|
|
1025
|
+
config_path: Path,
|
|
1026
|
+
host: HostConfig,
|
|
1027
|
+
*,
|
|
1028
|
+
force: bool,
|
|
1029
|
+
dry_run: bool,
|
|
1030
|
+
) -> ArtifactResult:
|
|
1031
|
+
"""Refresh MCP config entry (update command path if needed).
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
config_path: Path to MCP config file
|
|
1035
|
+
host: HostConfig for the agent host
|
|
1036
|
+
force: If True, update even if matching
|
|
1037
|
+
dry_run: If True, print without writing
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
ArtifactResult with success status
|
|
1041
|
+
"""
|
|
1042
|
+
try:
|
|
1043
|
+
# Resolve current MCP command path
|
|
1044
|
+
# Catch SystemExit because resolve_mcp_command raises it when binary not found
|
|
1045
|
+
try:
|
|
1046
|
+
mcp_command = resolve_mcp_command(non_interactive=True)
|
|
1047
|
+
except SystemExit:
|
|
1048
|
+
return ArtifactResult(
|
|
1049
|
+
path=config_path,
|
|
1050
|
+
success=False,
|
|
1051
|
+
error="java-codebase-rag-mcp not found on PATH",
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
# Prepare new entry
|
|
1055
|
+
new_entry = {"command": mcp_command, "type": "stdio"}
|
|
1056
|
+
|
|
1057
|
+
# Read existing config
|
|
1058
|
+
if config_path.is_file():
|
|
1059
|
+
try:
|
|
1060
|
+
with open(config_path, "r") as f:
|
|
1061
|
+
config = json.load(f)
|
|
1062
|
+
except json.JSONDecodeError as e:
|
|
1063
|
+
return ArtifactResult(
|
|
1064
|
+
path=config_path,
|
|
1065
|
+
success=False,
|
|
1066
|
+
error=f"Failed to parse {config_path}: {e}",
|
|
1067
|
+
)
|
|
1068
|
+
else:
|
|
1069
|
+
config = {}
|
|
1070
|
+
|
|
1071
|
+
# Ensure mcpServers key exists
|
|
1072
|
+
if "mcpServers" not in config:
|
|
1073
|
+
config["mcpServers"] = {}
|
|
1074
|
+
|
|
1075
|
+
existing_entry = config["mcpServers"].get(_MCP_SERVER_NAME)
|
|
1076
|
+
|
|
1077
|
+
# Check if entry already matches (skip unless force)
|
|
1078
|
+
if existing_entry == new_entry and not force:
|
|
1079
|
+
return ArtifactResult(path=config_path, success=True, error=None)
|
|
1080
|
+
|
|
1081
|
+
# Entry differs or force mode
|
|
1082
|
+
if dry_run:
|
|
1083
|
+
print(f"Would update MCP config at {config_path}")
|
|
1084
|
+
return ArtifactResult(path=config_path, success=True, error=None)
|
|
1085
|
+
|
|
1086
|
+
# Merge/update entry
|
|
1087
|
+
config["mcpServers"][_MCP_SERVER_NAME] = new_entry
|
|
1088
|
+
|
|
1089
|
+
# Ensure parent directory exists
|
|
1090
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1091
|
+
|
|
1092
|
+
# Check writability
|
|
1093
|
+
if not _is_writable(config_path.parent):
|
|
1094
|
+
return ArtifactResult(
|
|
1095
|
+
path=config_path,
|
|
1096
|
+
success=False,
|
|
1097
|
+
error=f"Directory not writable: {config_path.parent}",
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Write atomically
|
|
1101
|
+
tmp_name = None
|
|
1102
|
+
try:
|
|
1103
|
+
with tempfile.NamedTemporaryFile(
|
|
1104
|
+
mode="w",
|
|
1105
|
+
dir=config_path.parent,
|
|
1106
|
+
prefix=f".{config_path.name}.",
|
|
1107
|
+
delete=False,
|
|
1108
|
+
) as tmp:
|
|
1109
|
+
json.dump(config, tmp, indent=2)
|
|
1110
|
+
tmp.flush()
|
|
1111
|
+
os.fsync(tmp.fileno())
|
|
1112
|
+
tmp_name = tmp.name
|
|
1113
|
+
|
|
1114
|
+
# Atomic rename
|
|
1115
|
+
os.rename(tmp_name, config_path)
|
|
1116
|
+
print(f"Updated MCP config at {config_path}")
|
|
1117
|
+
return ArtifactResult(path=config_path, success=True, error=None)
|
|
1118
|
+
|
|
1119
|
+
except (IOError, OSError) as e:
|
|
1120
|
+
if tmp_name:
|
|
1121
|
+
try:
|
|
1122
|
+
os.unlink(tmp_name)
|
|
1123
|
+
except OSError:
|
|
1124
|
+
pass
|
|
1125
|
+
raise RuntimeError(f"Failed to write {config_path}: {e}") from e
|
|
1126
|
+
|
|
1127
|
+
except SystemExit as e:
|
|
1128
|
+
# Catch SystemExit from resolve_mcp_command and other exits
|
|
1129
|
+
return ArtifactResult(path=config_path, success=False, error=f"Command failed: {e.code}")
|
|
1130
|
+
except Exception as e:
|
|
1131
|
+
return ArtifactResult(path=config_path, success=False, error=str(e))
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def run_update(
|
|
1135
|
+
*,
|
|
1136
|
+
force: bool,
|
|
1137
|
+
dry_run: bool,
|
|
1138
|
+
cwd: Path | None = None,
|
|
1139
|
+
) -> int:
|
|
1140
|
+
"""Run the update pipeline. Returns exit code.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
force: If True, overwrite all artifacts even if matching
|
|
1144
|
+
dry_run: If True, print changes without writing
|
|
1145
|
+
cwd: Current working directory (defaults to Path.cwd())
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
Exit code (0=success, 1=partial, 2=fatal)
|
|
1149
|
+
"""
|
|
1150
|
+
if cwd is None:
|
|
1151
|
+
cwd = Path.cwd()
|
|
1152
|
+
cwd = cwd.resolve()
|
|
1153
|
+
|
|
1154
|
+
# Detect configured hosts
|
|
1155
|
+
configured_hosts = detect_configured_hosts(cwd)
|
|
1156
|
+
|
|
1157
|
+
if not configured_hosts:
|
|
1158
|
+
print("No configured agent hosts found.")
|
|
1159
|
+
print("Run `java-codebase-rag install` first.")
|
|
1160
|
+
return EXIT_FATAL
|
|
1161
|
+
|
|
1162
|
+
print(f"Found {len(configured_hosts)} configured host(s).")
|
|
1163
|
+
|
|
1164
|
+
# Refresh artifacts for each host
|
|
1165
|
+
all_results = []
|
|
1166
|
+
for host_config, scope in configured_hosts:
|
|
1167
|
+
print(f"\nRefreshing {host_config.name} ({scope} scope)...")
|
|
1168
|
+
results = refresh_artifacts(host_config, scope, cwd, force=force, dry_run=dry_run)
|
|
1169
|
+
all_results.extend(results)
|
|
1170
|
+
|
|
1171
|
+
# Check for partial failures
|
|
1172
|
+
partial_failures = [r for r in all_results if not r.success]
|
|
1173
|
+
has_artifact_failures = len(partial_failures) > 0
|
|
1174
|
+
if partial_failures:
|
|
1175
|
+
print("\nWarning: Some artifacts failed to update:")
|
|
1176
|
+
for r in partial_failures:
|
|
1177
|
+
print(f" {r.path}: {r.error}")
|
|
1178
|
+
|
|
1179
|
+
# Check if index exists
|
|
1180
|
+
from java_codebase_rag.config import (
|
|
1181
|
+
discover_project_root,
|
|
1182
|
+
index_dir_has_existing_artifacts,
|
|
1183
|
+
resolve_operator_config,
|
|
1184
|
+
)
|
|
1185
|
+
from java_codebase_rag.pipeline import run_cocoindex_update
|
|
1186
|
+
|
|
1187
|
+
project_root = discover_project_root(cwd)
|
|
1188
|
+
if project_root is None:
|
|
1189
|
+
print("\nNo project configuration found (.java-codebase-rag.yml).")
|
|
1190
|
+
print("Skipping index update.")
|
|
1191
|
+
return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
|
|
1192
|
+
|
|
1193
|
+
# Resolve configuration
|
|
1194
|
+
try:
|
|
1195
|
+
cfg = resolve_operator_config(source_root=project_root, cli_index_dir=None)
|
|
1196
|
+
index_dir = cfg.index_dir
|
|
1197
|
+
except Exception as e:
|
|
1198
|
+
print(f"\nWarning: Failed to resolve configuration: {e}")
|
|
1199
|
+
print("Skipping index update.")
|
|
1200
|
+
return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
|
|
1201
|
+
|
|
1202
|
+
# Check if index has existing artifacts
|
|
1203
|
+
index_exists, _ = index_dir_has_existing_artifacts(index_dir)
|
|
1204
|
+
|
|
1205
|
+
if not index_exists:
|
|
1206
|
+
print("\nNo index found.")
|
|
1207
|
+
print("Run `java-codebase-rag install` to create one.")
|
|
1208
|
+
return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
|
|
1209
|
+
|
|
1210
|
+
# Run increment (LanceDB catch-up)
|
|
1211
|
+
if not dry_run:
|
|
1212
|
+
print("\nUpdating index (incremental LanceDB update)...")
|
|
1213
|
+
cfg.apply_to_os_environ()
|
|
1214
|
+
env = cfg.subprocess_env()
|
|
1215
|
+
|
|
1216
|
+
coco = run_cocoindex_update(env, full_reprocess=False, quiet=True)
|
|
1217
|
+
if coco.returncode != 0:
|
|
1218
|
+
print(f"Error: Index update failed with code {coco.returncode}")
|
|
1219
|
+
return 1
|
|
1220
|
+
|
|
1221
|
+
# Print graph staleness warning
|
|
1222
|
+
from java_codebase_rag.cli import _INCREMENT_WARNING_LINES
|
|
1223
|
+
print("\n" + "\n".join(_INCREMENT_WARNING_LINES))
|
|
1224
|
+
else:
|
|
1225
|
+
print("\nWould run incremental index update.")
|
|
1226
|
+
|
|
1227
|
+
# Print summary
|
|
1228
|
+
print("\nUpdate complete.")
|
|
1229
|
+
successful = [r for r in all_results if r.success]
|
|
1230
|
+
print(f"Updated {len(successful)} artifact(s).")
|
|
1231
|
+
|
|
1232
|
+
return 1 if has_artifact_failures else 0
|
|
1233
|
+
|
|
1234
|
+
|
|
826
1235
|
def run_install(
|
|
827
1236
|
*,
|
|
828
1237
|
non_interactive: bool,
|
|
@@ -762,3 +762,416 @@ class TestInstallIntegration:
|
|
|
762
762
|
skill_qwen = cwd / ".qwen" / "skills" / "explore-codebase" / "SKILL.md"
|
|
763
763
|
assert skill_claude.is_file()
|
|
764
764
|
assert skill_qwen.is_file()
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class TestDetectConfiguredHosts:
|
|
768
|
+
"""Test detect_configured_hosts function for PR-I2."""
|
|
769
|
+
|
|
770
|
+
def test_detect_hosts_project_mcp_json(self, tmp_path):
|
|
771
|
+
""".mcp.json with entry → detects claude-code project scope"""
|
|
772
|
+
from java_codebase_rag.installer import detect_configured_hosts
|
|
773
|
+
|
|
774
|
+
# Create .mcp.json with java-codebase-rag entry
|
|
775
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
776
|
+
mcp_config.write_text(
|
|
777
|
+
json.dumps(
|
|
778
|
+
{
|
|
779
|
+
"mcpServers": {
|
|
780
|
+
"java-codebase-rag": {
|
|
781
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
782
|
+
"type": "stdio"
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
detected = detect_configured_hosts(tmp_path)
|
|
790
|
+
assert len(detected) == 1
|
|
791
|
+
host_config, scope = detected[0]
|
|
792
|
+
assert host_config.name == "claude-code"
|
|
793
|
+
assert scope == "project"
|
|
794
|
+
|
|
795
|
+
def test_detect_hosts_user_claude_json(self, tmp_path, monkeypatch):
|
|
796
|
+
"""~/.claude.json with entry → detects claude-code user scope"""
|
|
797
|
+
from java_codebase_rag.installer import detect_configured_hosts
|
|
798
|
+
|
|
799
|
+
# Create a fake home directory
|
|
800
|
+
fake_home = tmp_path / "home"
|
|
801
|
+
fake_home.mkdir()
|
|
802
|
+
monkeypatch.setattr(Path, "home", lambda: fake_home)
|
|
803
|
+
|
|
804
|
+
# Create ~/.claude.json with java-codebase-rag entry
|
|
805
|
+
claude_json = fake_home / ".claude.json"
|
|
806
|
+
claude_json.write_text(
|
|
807
|
+
json.dumps(
|
|
808
|
+
{
|
|
809
|
+
"mcpServers": {
|
|
810
|
+
"java-codebase-rag": {
|
|
811
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
812
|
+
"type": "stdio"
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
detected = detect_configured_hosts(tmp_path)
|
|
820
|
+
assert len(detected) == 1
|
|
821
|
+
host_config, scope = detected[0]
|
|
822
|
+
assert host_config.name == "claude-code"
|
|
823
|
+
assert scope == "user"
|
|
824
|
+
|
|
825
|
+
def test_detect_hosts_multiple_hosts(self, tmp_path, monkeypatch):
|
|
826
|
+
"""both .mcp.json and ~/.qwen/settings.json → returns both"""
|
|
827
|
+
from java_codebase_rag.installer import detect_configured_hosts
|
|
828
|
+
|
|
829
|
+
# Create a fake home directory
|
|
830
|
+
fake_home = tmp_path / "home"
|
|
831
|
+
fake_home.mkdir()
|
|
832
|
+
monkeypatch.setattr(Path, "home", lambda: fake_home)
|
|
833
|
+
|
|
834
|
+
# Create project-level .mcp.json
|
|
835
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
836
|
+
mcp_config.write_text(
|
|
837
|
+
json.dumps(
|
|
838
|
+
{
|
|
839
|
+
"mcpServers": {
|
|
840
|
+
"java-codebase-rag": {
|
|
841
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
842
|
+
"type": "stdio"
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
# Create user-level .qwen/settings.json
|
|
850
|
+
qwen_settings = fake_home / ".qwen" / "settings.json"
|
|
851
|
+
qwen_settings.parent.mkdir(parents=True, exist_ok=True)
|
|
852
|
+
qwen_settings.write_text(
|
|
853
|
+
json.dumps(
|
|
854
|
+
{
|
|
855
|
+
"mcpServers": {
|
|
856
|
+
"java-codebase-rag": {
|
|
857
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
858
|
+
"type": "stdio"
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
detected = detect_configured_hosts(tmp_path)
|
|
866
|
+
assert len(detected) == 2
|
|
867
|
+
|
|
868
|
+
# Sort by scope for consistent ordering
|
|
869
|
+
detected_sorted = sorted(detected, key=lambda x: x[1])
|
|
870
|
+
|
|
871
|
+
# First should be project scope claude-code
|
|
872
|
+
assert detected_sorted[0][0].name == "claude-code"
|
|
873
|
+
assert detected_sorted[0][1] == "project"
|
|
874
|
+
|
|
875
|
+
# Second should be user scope qwen-code
|
|
876
|
+
assert detected_sorted[1][0].name == "qwen-code"
|
|
877
|
+
assert detected_sorted[1][1] == "user"
|
|
878
|
+
|
|
879
|
+
def test_detect_hosts_no_config_returns_empty(self, tmp_path):
|
|
880
|
+
"""no MCP configs → empty list"""
|
|
881
|
+
from java_codebase_rag.installer import detect_configured_hosts
|
|
882
|
+
|
|
883
|
+
detected = detect_configured_hosts(tmp_path)
|
|
884
|
+
assert detected == []
|
|
885
|
+
|
|
886
|
+
def test_detect_hosts_ignores_unrelated_entries(self, tmp_path):
|
|
887
|
+
"""mcpServers with other tools but not java-codebase-rag → empty"""
|
|
888
|
+
from java_codebase_rag.installer import detect_configured_hosts
|
|
889
|
+
|
|
890
|
+
# Create .mcp.json with other MCP servers but not java-codebase-rag
|
|
891
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
892
|
+
mcp_config.write_text(
|
|
893
|
+
json.dumps(
|
|
894
|
+
{
|
|
895
|
+
"mcpServers": {
|
|
896
|
+
"filesystem": {"command": "/bin/fs", "type": "stdio"},
|
|
897
|
+
"brave-search": {"command": "/bin/search", "type": "stdio"},
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
detected = detect_configured_hosts(tmp_path)
|
|
904
|
+
assert detected == []
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class TestRefreshArtifacts:
|
|
908
|
+
"""Test refresh_artifacts function for PR-I2."""
|
|
909
|
+
|
|
910
|
+
def test_refresh_skill_overwrites_stale(self, tmp_path, monkeypatch):
|
|
911
|
+
"""skill file differs from package → overwritten"""
|
|
912
|
+
from java_codebase_rag.installer import refresh_artifacts, HOSTS
|
|
913
|
+
|
|
914
|
+
# Create skill file with stale content
|
|
915
|
+
skills_dir = tmp_path / ".claude" / "skills" / "explore-codebase"
|
|
916
|
+
skills_dir.mkdir(parents=True)
|
|
917
|
+
skill_file = skills_dir / "SKILL.md"
|
|
918
|
+
skill_file.write_text("STALE CONTENT")
|
|
919
|
+
|
|
920
|
+
# Mock _read_package_artifact to return new content
|
|
921
|
+
monkeypatch.setattr(
|
|
922
|
+
"java_codebase_rag.installer._read_package_artifact",
|
|
923
|
+
lambda path: "NEW CONTENT",
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
host = HOSTS["claude-code"]
|
|
927
|
+
results = refresh_artifacts(host, "project", tmp_path, force=False, dry_run=False)
|
|
928
|
+
|
|
929
|
+
# Should have updated the skill file
|
|
930
|
+
skill_results = [r for r in results if "SKILL.md" in str(r.path)]
|
|
931
|
+
assert len(skill_results) == 1
|
|
932
|
+
assert skill_results[0].success is True
|
|
933
|
+
assert skill_file.read_text() == "NEW CONTENT"
|
|
934
|
+
|
|
935
|
+
def test_refresh_skill_skips_if_matching(self, tmp_path, monkeypatch):
|
|
936
|
+
"""skill file matches → not overwritten (unless --force)"""
|
|
937
|
+
from java_codebase_rag.installer import refresh_artifacts, HOSTS
|
|
938
|
+
|
|
939
|
+
# Create skill file with current content
|
|
940
|
+
skills_dir = tmp_path / ".claude" / "skills" / "explore-codebase"
|
|
941
|
+
skills_dir.mkdir(parents=True)
|
|
942
|
+
skill_file = skills_dir / "SKILL.md"
|
|
943
|
+
skill_file.write_text("CURRENT CONTENT")
|
|
944
|
+
|
|
945
|
+
# Mock _read_package_artifact to return same content
|
|
946
|
+
monkeypatch.setattr(
|
|
947
|
+
"java_codebase_rag.installer._read_package_artifact",
|
|
948
|
+
lambda path: "CURRENT CONTENT",
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
host = HOSTS["claude-code"]
|
|
952
|
+
results = refresh_artifacts(host, "project", tmp_path, force=False, dry_run=False)
|
|
953
|
+
|
|
954
|
+
# Should have skipped the skill file (no change needed)
|
|
955
|
+
skill_results = [r for r in results if "SKILL.md" in str(r.path)]
|
|
956
|
+
assert len(skill_results) == 1
|
|
957
|
+
assert skill_results[0].success is True
|
|
958
|
+
# File should remain unchanged
|
|
959
|
+
assert skill_file.read_text() == "CURRENT CONTENT"
|
|
960
|
+
|
|
961
|
+
def test_refresh_mcp_skips_if_correct(self, tmp_path, monkeypatch):
|
|
962
|
+
"""MCP entry matches the current resolved path → not modified"""
|
|
963
|
+
from java_codebase_rag.installer import refresh_artifacts, HOSTS
|
|
964
|
+
import shutil
|
|
965
|
+
|
|
966
|
+
# Create MCP config with correct entry
|
|
967
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
968
|
+
expected_command = "/usr/local/bin/java-codebase-rag-mcp"
|
|
969
|
+
mcp_config.write_text(
|
|
970
|
+
json.dumps(
|
|
971
|
+
{
|
|
972
|
+
"mcpServers": {
|
|
973
|
+
"java-codebase-rag": {
|
|
974
|
+
"command": expected_command,
|
|
975
|
+
"type": "stdio"
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
# Mock shutil.which to return the same path
|
|
983
|
+
monkeypatch.setattr(shutil, "which", lambda x: expected_command)
|
|
984
|
+
|
|
985
|
+
host = HOSTS["claude-code"]
|
|
986
|
+
results = refresh_artifacts(host, "project", tmp_path, force=False, dry_run=False)
|
|
987
|
+
|
|
988
|
+
# MCP config should be skipped (no change needed)
|
|
989
|
+
mcp_results = [r for r in results if ".mcp.json" in str(r.path)]
|
|
990
|
+
assert len(mcp_results) == 1
|
|
991
|
+
assert mcp_results[0].success is True
|
|
992
|
+
# Config should remain unchanged
|
|
993
|
+
config_data = json.loads(mcp_config.read_text())
|
|
994
|
+
assert config_data["mcpServers"]["java-codebase-rag"]["command"] == expected_command
|
|
995
|
+
|
|
996
|
+
def test_refresh_dry_run_prints_no_write(self, tmp_path, monkeypatch, capsys):
|
|
997
|
+
"""--dry-run → prints changes, no files written"""
|
|
998
|
+
from java_codebase_rag.installer import refresh_artifacts, HOSTS
|
|
999
|
+
|
|
1000
|
+
# Create skill file with stale content
|
|
1001
|
+
skills_dir = tmp_path / ".claude" / "skills" / "explore-codebase"
|
|
1002
|
+
skills_dir.mkdir(parents=True)
|
|
1003
|
+
skill_file = skills_dir / "SKILL.md"
|
|
1004
|
+
skill_file.write_text("STALE CONTENT")
|
|
1005
|
+
|
|
1006
|
+
# Mock _read_package_artifact to return new content
|
|
1007
|
+
monkeypatch.setattr(
|
|
1008
|
+
"java_codebase_rag.installer._read_package_artifact",
|
|
1009
|
+
lambda path: "NEW CONTENT",
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
host = HOSTS["claude-code"]
|
|
1013
|
+
refresh_artifacts(host, "project", tmp_path, force=False, dry_run=True)
|
|
1014
|
+
|
|
1015
|
+
# In dry-run mode, files should not be written
|
|
1016
|
+
captured = capsys.readouterr()
|
|
1017
|
+
assert "dry-run" in captured.out.lower() or "would" in captured.out.lower()
|
|
1018
|
+
# File should remain unchanged
|
|
1019
|
+
assert skill_file.read_text() == "STALE CONTENT"
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class TestRunUpdate:
|
|
1023
|
+
"""Test run_update orchestrator for PR-I2."""
|
|
1024
|
+
|
|
1025
|
+
def test_update_no_hosts_exit_2(self, tmp_path, monkeypatch):
|
|
1026
|
+
"""no configured hosts → exit 2"""
|
|
1027
|
+
from java_codebase_rag.installer import run_update
|
|
1028
|
+
|
|
1029
|
+
# No MCP configs exist
|
|
1030
|
+
result = run_update(force=False, dry_run=False, cwd=tmp_path)
|
|
1031
|
+
assert result == 2
|
|
1032
|
+
|
|
1033
|
+
def test_update_no_index_skips_increment(self, tmp_path, monkeypatch):
|
|
1034
|
+
"""hosts configured but no index directory → increment skipped, warning printed"""
|
|
1035
|
+
from java_codebase_rag.installer import run_update
|
|
1036
|
+
import shutil
|
|
1037
|
+
import io
|
|
1038
|
+
|
|
1039
|
+
# Create MCP config to have a configured host
|
|
1040
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
1041
|
+
mcp_config.write_text(
|
|
1042
|
+
json.dumps(
|
|
1043
|
+
{
|
|
1044
|
+
"mcpServers": {
|
|
1045
|
+
"java-codebase-rag": {
|
|
1046
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
1047
|
+
"type": "stdio"
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
)
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
# Create .java-codebase-rag.yml (config exists)
|
|
1055
|
+
config_file = tmp_path / ".java-codebase-rag.yml"
|
|
1056
|
+
config_file.write_text("source_root: .")
|
|
1057
|
+
|
|
1058
|
+
# Mock shutil.which
|
|
1059
|
+
monkeypatch.setattr(shutil, "which", lambda x: "/usr/local/bin/java-codebase-rag-mcp")
|
|
1060
|
+
|
|
1061
|
+
# Mock index_dir_has_existing_artifacts to return False (no index)
|
|
1062
|
+
monkeypatch.setattr(
|
|
1063
|
+
"java_codebase_rag.config.index_dir_has_existing_artifacts",
|
|
1064
|
+
lambda path: (False, []),
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# Mock _read_package_artifact
|
|
1068
|
+
monkeypatch.setattr(
|
|
1069
|
+
"java_codebase_rag.installer._read_package_artifact",
|
|
1070
|
+
lambda path: "PACKAGE CONTENT",
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# Capture stdout
|
|
1074
|
+
fake_stdout = io.StringIO()
|
|
1075
|
+
monkeypatch.setattr("sys.stdout", fake_stdout)
|
|
1076
|
+
|
|
1077
|
+
result = run_update(force=False, dry_run=False, cwd=tmp_path)
|
|
1078
|
+
# Should succeed (no hosts is fatal, but no index is just a warning)
|
|
1079
|
+
assert result == 0
|
|
1080
|
+
|
|
1081
|
+
def test_install_then_update_cycle(self, tmp_path, monkeypatch):
|
|
1082
|
+
"""install then update: artifacts refreshed, no errors"""
|
|
1083
|
+
from java_codebase_rag.installer import run_install, run_update
|
|
1084
|
+
import shutil
|
|
1085
|
+
|
|
1086
|
+
# Copy bank-chat fixture
|
|
1087
|
+
bank_chat = Path("tests/bank-chat-system")
|
|
1088
|
+
if not bank_chat.is_dir():
|
|
1089
|
+
pytest.skip("bank-chat-system fixture not found")
|
|
1090
|
+
shutil.copytree(bank_chat, tmp_path / "bank-chat")
|
|
1091
|
+
|
|
1092
|
+
cwd = tmp_path / "bank-chat"
|
|
1093
|
+
|
|
1094
|
+
# Create .git so update_gitignore works
|
|
1095
|
+
(cwd / ".git").mkdir()
|
|
1096
|
+
|
|
1097
|
+
# Mock shutil.which
|
|
1098
|
+
monkeypatch.setattr(shutil, "which", lambda x: "/usr/local/bin/java-codebase-rag-mcp")
|
|
1099
|
+
|
|
1100
|
+
# Mock pipeline functions
|
|
1101
|
+
def mock_run_cocoindex_update(*args, **kwargs):
|
|
1102
|
+
from subprocess import CompletedProcess
|
|
1103
|
+
return CompletedProcess(["cocoindex"], 0)
|
|
1104
|
+
|
|
1105
|
+
def mock_run_build_ast_graph(*args, **kwargs):
|
|
1106
|
+
from subprocess import CompletedProcess
|
|
1107
|
+
return CompletedProcess(["build_ast_graph"], 0)
|
|
1108
|
+
|
|
1109
|
+
monkeypatch.setattr(
|
|
1110
|
+
"java_codebase_rag.pipeline.run_cocoindex_update",
|
|
1111
|
+
mock_run_cocoindex_update,
|
|
1112
|
+
)
|
|
1113
|
+
monkeypatch.setattr(
|
|
1114
|
+
"java_codebase_rag.pipeline.run_build_ast_graph",
|
|
1115
|
+
mock_run_build_ast_graph,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
# Change to fixture directory
|
|
1119
|
+
monkeypatch.setattr(Path, "cwd", lambda: cwd)
|
|
1120
|
+
|
|
1121
|
+
# Run install
|
|
1122
|
+
install_result = run_install(
|
|
1123
|
+
non_interactive=True,
|
|
1124
|
+
agents=["claude-code"],
|
|
1125
|
+
scope="project",
|
|
1126
|
+
model="auto",
|
|
1127
|
+
source_root=cwd,
|
|
1128
|
+
quiet=True,
|
|
1129
|
+
)
|
|
1130
|
+
assert install_result == 0
|
|
1131
|
+
|
|
1132
|
+
# Verify artifacts were created
|
|
1133
|
+
skill_file = cwd / ".claude" / "skills" / "explore-codebase" / "SKILL.md"
|
|
1134
|
+
assert skill_file.is_file()
|
|
1135
|
+
|
|
1136
|
+
# Modify skill file to make it "stale"
|
|
1137
|
+
skill_file.write_text("MODIFIED CONTENT")
|
|
1138
|
+
|
|
1139
|
+
# Run update
|
|
1140
|
+
update_result = run_update(force=False, dry_run=False, cwd=cwd)
|
|
1141
|
+
assert update_result == 0
|
|
1142
|
+
|
|
1143
|
+
# Skill file should have been refreshed back to package content
|
|
1144
|
+
# (In real scenario, this would be the actual package content)
|
|
1145
|
+
|
|
1146
|
+
def test_update_missing_mcp_binary_returns_partial_failure(self, tmp_path, monkeypatch):
|
|
1147
|
+
"""java-codebase-rag-mcp not found on PATH → returns partial failure (1)"""
|
|
1148
|
+
from java_codebase_rag.installer import run_update
|
|
1149
|
+
import shutil
|
|
1150
|
+
|
|
1151
|
+
# Create MCP config to have a configured host
|
|
1152
|
+
mcp_config = tmp_path / ".mcp.json"
|
|
1153
|
+
mcp_config.write_text(
|
|
1154
|
+
json.dumps(
|
|
1155
|
+
{
|
|
1156
|
+
"mcpServers": {
|
|
1157
|
+
"java-codebase-rag": {
|
|
1158
|
+
"command": "/usr/local/bin/java-codebase-rag-mcp",
|
|
1159
|
+
"type": "stdio"
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
)
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# Mock shutil.which to return None (MCP binary not found)
|
|
1167
|
+
monkeypatch.setattr(shutil, "which", lambda x: None)
|
|
1168
|
+
|
|
1169
|
+
# Mock _read_package_artifact
|
|
1170
|
+
monkeypatch.setattr(
|
|
1171
|
+
"java_codebase_rag.installer._read_package_artifact",
|
|
1172
|
+
lambda path: "PACKAGE CONTENT",
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
result = run_update(force=False, dry_run=False, cwd=tmp_path)
|
|
1176
|
+
# Should return partial failure (1) because artifact refresh failed
|
|
1177
|
+
assert result == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/java_codebase_rag.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_assign_endpoint_client_extraction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_bank_chat_brownfield_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_graph_receiver_resolution.py
RENAMED
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_call_graph_smoke_roundtrip.py
RENAMED
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_cli_progress_stdout_invariant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_cross_service_resolution_flag.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_codebase_rag-0.5.0 → java_codebase_rag-0.5.1}/tests/test_resolve_routes_messaging_layer_c.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|