codeboarding 0.10.1__tar.gz → 0.10.2__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.
Files changed (159) hide show
  1. {codeboarding-0.10.1/codeboarding.egg-info → codeboarding-0.10.2}/PKG-INFO +1 -1
  2. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/cluster_methods_mixin.py +26 -6
  3. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/meta_agent.py +0 -1
  4. {codeboarding-0.10.1 → codeboarding-0.10.2/codeboarding.egg-info}/PKG-INFO +1 -1
  5. {codeboarding-0.10.1 → codeboarding-0.10.2}/install.py +64 -9
  6. {codeboarding-0.10.1 → codeboarding-0.10.2}/main.py +2 -2
  7. {codeboarding-0.10.1 → codeboarding-0.10.2}/pyproject.toml +1 -1
  8. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/cluster_helpers.py +46 -23
  9. {codeboarding-0.10.1 → codeboarding-0.10.2}/tool_registry.py +83 -25
  10. {codeboarding-0.10.1 → codeboarding-0.10.2}/LICENSE +0 -0
  11. {codeboarding-0.10.1 → codeboarding-0.10.2}/PYPI.md +0 -0
  12. {codeboarding-0.10.1 → codeboarding-0.10.2}/README.md +0 -0
  13. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/__init__.py +0 -0
  14. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/abstraction_agent.py +0 -0
  15. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/agent.py +0 -0
  16. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/agent_responses.py +0 -0
  17. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/change_status.py +0 -0
  18. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/constants.py +0 -0
  19. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/dependency_discovery.py +0 -0
  20. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/details_agent.py +0 -0
  21. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/llm_config.py +0 -0
  22. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/planner_agent.py +0 -0
  23. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/__init__.py +0 -0
  24. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/abstract_prompt_factory.py +0 -0
  25. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/claude_prompts.py +0 -0
  26. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/deepseek_prompts.py +0 -0
  27. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/gemini_flash_prompts.py +0 -0
  28. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/glm_prompts.py +0 -0
  29. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/gpt_prompts.py +0 -0
  30. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/kimi_prompts.py +0 -0
  31. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/prompts/prompt_factory.py +0 -0
  32. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/__init__.py +0 -0
  33. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/base.py +0 -0
  34. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/get_external_deps.py +0 -0
  35. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/get_method_invocations.py +0 -0
  36. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_cfg.py +0 -0
  37. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_docs.py +0 -0
  38. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_file.py +0 -0
  39. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_file_structure.py +0 -0
  40. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_git_diff.py +0 -0
  41. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_packages.py +0 -0
  42. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_source.py +0 -0
  43. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/read_structure.py +0 -0
  44. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/tools/toolkit.py +0 -0
  45. {codeboarding-0.10.1 → codeboarding-0.10.2}/agents/validation.py +0 -0
  46. {codeboarding-0.10.1 → codeboarding-0.10.2}/caching/__init__.py +0 -0
  47. {codeboarding-0.10.1 → codeboarding-0.10.2}/caching/cache.py +0 -0
  48. {codeboarding-0.10.1 → codeboarding-0.10.2}/caching/details_cache.py +0 -0
  49. {codeboarding-0.10.1 → codeboarding-0.10.2}/caching/meta_cache.py +0 -0
  50. {codeboarding-0.10.1 → codeboarding-0.10.2}/codeboarding.egg-info/SOURCES.txt +0 -0
  51. {codeboarding-0.10.1 → codeboarding-0.10.2}/codeboarding.egg-info/dependency_links.txt +0 -0
  52. {codeboarding-0.10.1 → codeboarding-0.10.2}/codeboarding.egg-info/entry_points.txt +0 -0
  53. {codeboarding-0.10.1 → codeboarding-0.10.2}/codeboarding.egg-info/requires.txt +0 -0
  54. {codeboarding-0.10.1 → codeboarding-0.10.2}/codeboarding.egg-info/top_level.txt +0 -0
  55. {codeboarding-0.10.1 → codeboarding-0.10.2}/constants.py +0 -0
  56. {codeboarding-0.10.1 → codeboarding-0.10.2}/core/__init__.py +0 -0
  57. {codeboarding-0.10.1 → codeboarding-0.10.2}/core/plugin_loader.py +0 -0
  58. {codeboarding-0.10.1 → codeboarding-0.10.2}/core/protocols.py +0 -0
  59. {codeboarding-0.10.1 → codeboarding-0.10.2}/core/registry.py +0 -0
  60. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/__init__.py +0 -0
  61. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/analysis_json.py +0 -0
  62. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/diagram_generator.py +0 -0
  63. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/file_coverage.py +0 -0
  64. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/incremental_types.py +0 -0
  65. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/incremental_updater.py +0 -0
  66. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/io_utils.py +0 -0
  67. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/manifest.py +0 -0
  68. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/run_context.py +0 -0
  69. {codeboarding-0.10.1 → codeboarding-0.10.2}/diagram_analysis/version.py +0 -0
  70. {codeboarding-0.10.1 → codeboarding-0.10.2}/duckdb_crud.py +0 -0
  71. {codeboarding-0.10.1 → codeboarding-0.10.2}/github_action.py +0 -0
  72. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/__init__.py +0 -0
  73. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/__init__.py +0 -0
  74. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/circular_deps.py +0 -0
  75. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/cohesion.py +0 -0
  76. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/coupling.py +0 -0
  77. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/function_size.py +0 -0
  78. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/god_class.py +0 -0
  79. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/inheritance.py +0 -0
  80. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/instability.py +0 -0
  81. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/checks/unused_code_diagnostics.py +0 -0
  82. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/config.py +0 -0
  83. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/constants.py +0 -0
  84. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/models.py +0 -0
  85. {codeboarding-0.10.1 → codeboarding-0.10.2}/health/runner.py +0 -0
  86. {codeboarding-0.10.1 → codeboarding-0.10.2}/health_main.py +0 -0
  87. {codeboarding-0.10.1 → codeboarding-0.10.2}/logging_config.py +0 -0
  88. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/__init__.py +0 -0
  89. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/callbacks.py +0 -0
  90. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/context.py +0 -0
  91. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/mixin.py +0 -0
  92. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/paths.py +0 -0
  93. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/stats.py +0 -0
  94. {codeboarding-0.10.1 → codeboarding-0.10.2}/monitoring/writers.py +0 -0
  95. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/__init__.py +0 -0
  96. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/html.py +0 -0
  97. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/html_template.py +0 -0
  98. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/markdown.py +0 -0
  99. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/mdx.py +0 -0
  100. {codeboarding-0.10.1 → codeboarding-0.10.2}/output_generators/sphinx.py +0 -0
  101. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/__init__.py +0 -0
  102. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/change_detector.py +0 -0
  103. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/errors.py +0 -0
  104. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/git_diff.py +0 -0
  105. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/ignore.py +0 -0
  106. {codeboarding-0.10.1 → codeboarding-0.10.2}/repo_utils/method_diff.py +0 -0
  107. {codeboarding-0.10.1 → codeboarding-0.10.2}/setup.cfg +0 -0
  108. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/__init__.py +0 -0
  109. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/analysis_cache.py +0 -0
  110. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/analysis_result.py +0 -0
  111. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/cluster_change_analyzer.py +0 -0
  112. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/cluster_relations.py +0 -0
  113. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/constants.py +0 -0
  114. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/__init__.py +0 -0
  115. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/__init__.py +0 -0
  116. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/go_adapter.py +0 -0
  117. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/java_adapter.py +0 -0
  118. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/php_adapter.py +0 -0
  119. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/python_adapter.py +0 -0
  120. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
  121. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/call_graph_builder.py +0 -0
  122. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/edge_build_context.py +0 -0
  123. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/edge_builder.py +0 -0
  124. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/hierarchy_builder.py +0 -0
  125. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/language_adapter.py +0 -0
  126. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/lsp_client.py +0 -0
  127. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/lsp_constants.py +0 -0
  128. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/models.py +0 -0
  129. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/progress.py +0 -0
  130. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/protocols.py +0 -0
  131. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/result_converter.py +0 -0
  132. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/source_inspector.py +0 -0
  133. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/symbol_table.py +0 -0
  134. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/engine/utils.py +0 -0
  135. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/git_diff_analyzer.py +0 -0
  136. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/graph.py +0 -0
  137. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/incremental_orchestrator.py +0 -0
  138. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/java_config_scanner.py +0 -0
  139. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/java_utils.py +0 -0
  140. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/lsp_client/__init__.py +0 -0
  141. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/lsp_client/diagnostics.py +0 -0
  142. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/node.py +0 -0
  143. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/programming_language.py +0 -0
  144. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/reference_resolve_mixin.py +0 -0
  145. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/scanner.py +0 -0
  146. {codeboarding-0.10.1 → codeboarding-0.10.2}/static_analyzer/typescript_config_scanner.py +0 -0
  147. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_github_action.py +0 -0
  148. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_install.py +0 -0
  149. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_logging_config.py +0 -0
  150. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_main.py +0 -0
  151. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_pyproject_packages.py +0 -0
  152. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_tool_registry.py +0 -0
  153. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_user_config.py +0 -0
  154. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_vscode_constants.py +0 -0
  155. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_windows_compatibility.py +0 -0
  156. {codeboarding-0.10.1 → codeboarding-0.10.2}/tests/test_windows_encoding.py +0 -0
  157. {codeboarding-0.10.1 → codeboarding-0.10.2}/user_config.py +0 -0
  158. {codeboarding-0.10.1 → codeboarding-0.10.2}/utils.py +0 -0
  159. {codeboarding-0.10.1 → codeboarding-0.10.2}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.10.1
3
+ Version: 0.10.2
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -16,8 +16,11 @@ from agents.agent_responses import (
16
16
  from constants import MIN_CLUSTERS_THRESHOLD
17
17
  from static_analyzer.analysis_result import StaticAnalysisResults
18
18
  from static_analyzer.cluster_helpers import (
19
+ MAX_LLM_CLUSTERS,
20
+ enforce_cross_language_budget,
19
21
  get_all_cluster_ids,
20
22
  get_files_for_cluster_ids,
23
+ merge_clusters,
21
24
  )
22
25
  from static_analyzer.cluster_relations import (
23
26
  build_component_relations,
@@ -250,7 +253,6 @@ class ClusterMethodsMixin:
250
253
  abs_path = os.path.join(self.repo_dir, f) if not os.path.isabs(f) else f
251
254
  assigned_file_set.add(abs_path)
252
255
 
253
- result_parts = []
254
256
  cluster_results: dict[str, ClusterResult] = {}
255
257
  subgraph_cfgs: dict[str, CallGraph] = {}
256
258
 
@@ -266,15 +268,33 @@ class ClusterMethodsMixin:
266
268
  # Calculate clusters for the subgraph
267
269
  sub_cluster_result = sub_cfg.cluster()
268
270
 
271
+ # Merge into super-clusters if too many (same limit as AbstractionAgent)
272
+ if len(sub_cluster_result.clusters) > MAX_LLM_CLUSTERS:
273
+ n_before = len(sub_cluster_result.clusters)
274
+ sub_cluster_result = merge_clusters(sub_cluster_result, sub_cfg.to_networkx(), MAX_LLM_CLUSTERS)
275
+ logger.info(
276
+ f"[DetailsAgent] Subgraph for '{component.name}': "
277
+ f"merged {n_before} -> {len(sub_cluster_result.clusters)} super-clusters"
278
+ )
279
+
269
280
  # Expand to method-level if insufficient clusters
270
281
  sub_cluster_result = self._expand_to_method_level_clusters(sub_cfg, sub_cluster_result)
271
282
  cluster_results[lang] = sub_cluster_result
272
283
 
273
- cluster_str = sub_cfg.to_cluster_string(cluster_result=sub_cluster_result)
274
- if cluster_str.strip() and cluster_str not in ("empty", "none", "No clusters found."):
275
- result_parts.append(f"\n## {lang.capitalize()} - Component CFG\n")
276
- result_parts.append(cluster_str)
277
- result_parts.append("\n")
284
+ # Cross-language: enforce combined budget and unique IDs
285
+ if len(cluster_results) > 1:
286
+ cfg_nx = {lang: subgraph_cfgs[lang].to_networkx() for lang in cluster_results}
287
+ enforce_cross_language_budget(cluster_results, cfg_nx)
288
+
289
+ result_parts = []
290
+ for lang in self.static_analysis.get_languages():
291
+ if lang not in cluster_results:
292
+ continue
293
+ cluster_str = subgraph_cfgs[lang].to_cluster_string(cluster_result=cluster_results[lang])
294
+ if cluster_str.strip() and cluster_str not in ("empty", "none", "No clusters found."):
295
+ result_parts.append(f"\n## {lang.capitalize()} - Component CFG\n")
296
+ result_parts.append(cluster_str)
297
+ result_parts.append("\n")
278
298
 
279
299
  result = "".join(result_parts)
280
300
 
@@ -46,7 +46,6 @@ class MetaAgent(CodeBoardingAgent):
46
46
  )
47
47
 
48
48
  self._meta_cache = MetaCache(repo_dir=repo_dir, ignore_manager=self.ignore_manager)
49
- self._cache = self._meta_cache # Backward-compatible alias for tests/callers.
50
49
 
51
50
  @trace
52
51
  def analyze_project_metadata(self, skip_cache: bool = False) -> MetaAnalysisInsights:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.10.1
3
+ Version: 0.10.2
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -14,12 +14,15 @@ import requests
14
14
 
15
15
  from tool_registry import (
16
16
  TOOL_REGISTRY,
17
+ ProgressCallback,
17
18
  ToolKind,
19
+ _acquire_lock,
18
20
  _write_manifest,
19
21
  get_servers_dir,
20
22
  install_archive_tool,
21
23
  install_native_tools,
22
24
  install_node_tools,
25
+ needs_install,
23
26
  npm_subprocess_env,
24
27
  platform_bin_dir,
25
28
  preferred_node_path,
@@ -207,13 +210,13 @@ def get_platform_bin_dir(servers_dir: Path) -> Path:
207
210
  return platform_bin_dir(servers_dir)
208
211
 
209
212
 
210
- def install_node_servers(target_dir: Path):
213
+ def install_node_servers(target_dir: Path, on_progress: ProgressCallback | None = None):
211
214
  """Install Node.js based servers (TypeScript, Pyright) using npm in target_dir."""
212
215
  print("Step: Node.js servers installation started")
213
216
  target_dir.mkdir(parents=True, exist_ok=True)
214
217
 
215
218
  node_deps = [d for d in TOOL_REGISTRY if d.kind is ToolKind.NODE]
216
- install_node_tools(target_dir, node_deps)
219
+ install_node_tools(target_dir, node_deps, on_progress=on_progress)
217
220
 
218
221
  # Verify the installation
219
222
  ts_lsp_path = target_dir / "node_modules" / ".bin" / "typescript-language-server"
@@ -369,11 +372,11 @@ def resolve_missing_vcpp(auto_install_vcpp: bool = False) -> bool:
369
372
  return False
370
373
 
371
374
 
372
- def download_binaries(target_dir: Path, auto_install_vcpp: bool = False):
375
+ def download_binaries(target_dir: Path, auto_install_vcpp: bool = False, on_progress: ProgressCallback | None = None):
373
376
  """Download tokei and gopls binaries from the latest GitHub release."""
374
377
  print("Step: Binary download started")
375
378
  native_deps = [d for d in TOOL_REGISTRY if d.kind is ToolKind.NATIVE]
376
- install_native_tools(target_dir, native_deps)
379
+ install_native_tools(target_dir, native_deps, on_progress=on_progress)
377
380
 
378
381
  # Verify downloaded binaries actually work (catch missing DLL issues on Windows)
379
382
  if platform.system() == "Windows":
@@ -411,12 +414,12 @@ def download_binaries(target_dir: Path, auto_install_vcpp: bool = False):
411
414
  print("Step: Binary download finished")
412
415
 
413
416
 
414
- def download_jdtls(target_dir: Path):
417
+ def download_jdtls(target_dir: Path, on_progress: ProgressCallback | None = None):
415
418
  """Download and extract JDTLS from the latest GitHub release."""
416
419
  print("Step: JDTLS download started")
417
420
  archive_deps = [d for d in TOOL_REGISTRY if d.kind is ToolKind.ARCHIVE]
418
421
  for dep in archive_deps:
419
- install_archive_tool(target_dir, dep)
422
+ install_archive_tool(target_dir, dep, on_progress=on_progress)
420
423
 
421
424
  print("Step: JDTLS download finished")
422
425
  return True
@@ -530,27 +533,79 @@ def print_language_support_summary(npm_available: bool, target_dir: Path):
530
533
  print(f" reason: {reason}")
531
534
 
532
535
 
536
+ def ensure_tools(
537
+ auto_install_npm: bool = False,
538
+ auto_install_vcpp: bool = False,
539
+ on_progress: ProgressCallback | None = None,
540
+ ) -> None:
541
+ """Install tools to ~/.codeboarding/servers/ if needed. No-op if already current.
542
+
543
+ Uses a file lock so that concurrent instances (multiple VSCode windows)
544
+ don't corrupt binaries by downloading simultaneously.
545
+
546
+ Args:
547
+ on_progress: Optional callback invoked as (tool_name, step, total) during downloads.
548
+ """
549
+ servers_dir = get_servers_dir()
550
+ servers_dir.mkdir(parents=True, exist_ok=True)
551
+ lock_path = servers_dir / ".download.lock"
552
+
553
+ with open(lock_path, "w") as lock_fd:
554
+ _acquire_lock(lock_fd)
555
+
556
+ if not needs_install():
557
+ return
558
+
559
+ run_install(
560
+ target_dir=servers_dir,
561
+ auto_install_npm=auto_install_npm,
562
+ auto_install_vcpp=auto_install_vcpp,
563
+ on_progress=on_progress,
564
+ )
565
+ _write_manifest()
566
+
567
+
533
568
  def run_install(
534
569
  target_dir: Path | None = None,
535
570
  auto_install_npm: bool = False,
536
571
  auto_install_vcpp: bool = False,
572
+ on_progress: ProgressCallback | None = None,
537
573
  ) -> None:
538
574
  """Core installation logic — callable programmatically or via CLI.
539
575
 
540
576
  Downloads language server binaries to target_dir (defaults to ~/.codeboarding/servers/).
541
577
  Safe to call multiple times; already-installed tools are skipped.
578
+
579
+ The ``on_progress`` callback receives ``(tool_name, step, total)`` where
580
+ step/total count across *all* tool categories (native, node, archive).
542
581
  """
543
582
  target = (target_dir or get_servers_dir()).resolve()
544
583
  target.mkdir(parents=True, exist_ok=True)
545
584
 
546
585
  ensure_config_template()
547
586
 
587
+ # Compute a unified total so the caller sees a single progress stream.
588
+ native_count = sum(1 for d in TOOL_REGISTRY if d.kind is ToolKind.NATIVE and d.github_asset_template)
589
+ node_deps = [d for d in TOOL_REGISTRY if d.kind is ToolKind.NODE]
590
+ archive_count = sum(1 for d in TOOL_REGISTRY if d.kind is ToolKind.ARCHIVE)
548
591
  npm_available = resolve_npm_availability(auto_install_npm=auto_install_npm, target_dir=target)
592
+ total_steps = native_count + (1 if npm_available and node_deps else 0) + archive_count
593
+
594
+ step = 0
595
+
596
+ def unified_progress(name: str, _i: int, _t: int) -> None:
597
+ nonlocal step
598
+ step += 1
599
+ if on_progress:
600
+ on_progress(name, step, total_steps)
601
+
602
+ tracker = unified_progress if on_progress else None
603
+
549
604
  if npm_available:
550
- install_node_servers(target)
605
+ install_node_servers(target, on_progress=tracker)
551
606
 
552
- download_binaries(target, auto_install_vcpp=auto_install_vcpp)
553
- download_jdtls(target)
607
+ download_binaries(target, auto_install_vcpp=auto_install_vcpp, on_progress=tracker)
608
+ download_jdtls(target, on_progress=tracker)
554
609
  install_pre_commit_hooks()
555
610
  print_language_support_summary(npm_available, target)
556
611
 
@@ -17,6 +17,8 @@ from diagram_analysis.io_utils import load_full_analysis, save_sub_analysis
17
17
  from logging_config import setup_logging
18
18
  from monitoring import monitor_execution
19
19
  from monitoring.paths import get_monitoring_run_dir
20
+ from install import ensure_tools
21
+ from tool_registry import needs_install
20
22
  from output_generators.markdown import generate_markdown_file
21
23
  from repo_utils import (
22
24
  clone_repository,
@@ -473,8 +475,6 @@ Examples:
473
475
  if args.binary_location:
474
476
  update_config(args.binary_location)
475
477
  else:
476
- from tool_registry import ensure_tools, needs_install
477
-
478
478
  if needs_install():
479
479
  logger.info("First run: downloading language server binaries to ~/.codeboarding/servers/ ...")
480
480
  ensure_tools(auto_install_npm=True, auto_install_vcpp=True)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.10.1"
7
+ version = "0.10.2"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = "MIT"
@@ -88,32 +88,55 @@ def build_all_cluster_results(static_analysis: StaticAnalysisResults) -> dict[st
88
88
  # within MAX_LLM_CLUSTERS by proportionally reducing per-language counts,
89
89
  # then re-index IDs so they don't overlap across languages.
90
90
  if len(cluster_results) > 1:
91
- total_clusters = sum(len(cr.clusters) for cr in cluster_results.values())
92
- if total_clusters > MAX_LLM_CLUSTERS:
93
- # Proportionally allocate budget, minimum 2 per language
94
- for lang in list(cluster_results.keys()):
95
- cr = cluster_results[lang]
96
- lang_count = len(cr.clusters)
97
- lang_target = max(2, round(MAX_LLM_CLUSTERS * lang_count / total_clusters))
98
- if lang_count > lang_target:
99
- cfg = static_analysis.get_cfg(lang)
100
- logger.info(
101
- f"[SuperCluster] {lang}: reducing {lang_count} -> {lang_target} (cross-language budget)"
102
- )
103
- cluster_results[lang] = merge_clusters(cr, cfg.to_networkx(), lang_target)
104
-
105
- # Re-index so IDs don't overlap across languages
106
- offset = 0
107
- for lang in sorted(cluster_results.keys()):
108
- cr = cluster_results[lang]
109
- if offset > 0:
110
- cluster_results[lang] = _reindex_cluster_result(cr, offset)
111
- logger.info(f"[ReIndex] {lang}: offset IDs by +{offset} (now {offset + 1}-{offset + len(cr.clusters)})")
112
- offset += len(cr.clusters)
91
+ cfg_graphs = {lang: static_analysis.get_cfg(lang).to_networkx() for lang in cluster_results}
92
+ enforce_cross_language_budget(cluster_results, cfg_graphs)
113
93
 
114
94
  return cluster_results
115
95
 
116
96
 
97
+ def enforce_cross_language_budget(
98
+ cluster_results: dict[str, ClusterResult],
99
+ cfg_graphs: dict[str, nx.DiGraph],
100
+ target: int = MAX_LLM_CLUSTERS,
101
+ ) -> None:
102
+ """Enforce a combined cluster budget across languages and re-index IDs.
103
+
104
+ Mutates *cluster_results* in place:
105
+ 1. If the combined cluster count exceeds *target*, proportionally reduce
106
+ each language's count (minimum 2 per language) via ``merge_clusters``.
107
+ 2. Re-index cluster IDs with per-language offsets so they form a unique,
108
+ non-overlapping namespace (required by downstream code that maps
109
+ cluster_id -> component in a single dict).
110
+
111
+ Args:
112
+ cluster_results: Language -> ClusterResult mapping (mutated in place).
113
+ cfg_graphs: Language -> networkx DiGraph for each language (needed by
114
+ ``merge_clusters`` when reducing).
115
+ target: Maximum total clusters across all languages.
116
+ """
117
+ if len(cluster_results) <= 1:
118
+ return
119
+
120
+ total_clusters = sum(len(cr.clusters) for cr in cluster_results.values())
121
+ if total_clusters > target:
122
+ for lang in list(cluster_results.keys()):
123
+ cr = cluster_results[lang]
124
+ lang_count = len(cr.clusters)
125
+ lang_target = max(2, round(target * lang_count / total_clusters))
126
+ if lang_count > lang_target:
127
+ logger.info(f"[SuperCluster] {lang}: reducing {lang_count} -> {lang_target} (cross-language budget)")
128
+ cluster_results[lang] = merge_clusters(cr, cfg_graphs[lang], lang_target)
129
+
130
+ # Re-index so IDs don't overlap across languages
131
+ offset = 0
132
+ for lang in sorted(cluster_results.keys()):
133
+ cr = cluster_results[lang]
134
+ if offset > 0:
135
+ cluster_results[lang] = reindex_cluster_result(cr, offset)
136
+ logger.info(f"[ReIndex] {lang}: offset IDs by +{offset} (now {offset + 1}-{offset + len(cr.clusters)})")
137
+ offset += len(cr.clusters)
138
+
139
+
117
140
  # ---------------------------------------------------------------------------
118
141
  # Meta-graph construction
119
142
  # ---------------------------------------------------------------------------
@@ -267,7 +290,7 @@ def _find_nearest_by_file_overlap(
267
290
  return best_idx
268
291
 
269
292
 
270
- def _reindex_cluster_result(cluster_result: ClusterResult, offset: int) -> ClusterResult:
293
+ def reindex_cluster_result(cluster_result: ClusterResult, offset: int) -> ClusterResult:
271
294
  """Re-index all cluster IDs in a ClusterResult by adding an offset.
272
295
 
273
296
  Args:
@@ -18,19 +18,30 @@ import os
18
18
  import platform
19
19
  import shutil
20
20
  import subprocess
21
+ import sys
21
22
  import tarfile
23
+ import time
24
+ from collections.abc import Callable
22
25
  from copy import deepcopy
23
26
  from dataclasses import dataclass, field
24
27
  from enum import StrEnum
25
28
  from pathlib import Path
26
29
  from typing import Any, cast
27
30
 
31
+ if sys.platform == "win32":
32
+ import msvcrt
33
+ else:
34
+ import fcntl
35
+
28
36
  import requests
29
37
 
30
38
  from vscode_constants import VSCODE_CONFIG, find_runnable
31
39
 
32
40
  logger = logging.getLogger(__name__)
33
41
 
42
+ # Callback type for reporting download progress: (tool_name, current_step, total_steps)
43
+ ProgressCallback = Callable[[str, int, int], None]
44
+
34
45
  GITHUB_REPO = "CodeBoarding/CodeBoarding"
35
46
 
36
47
  _PLATFORM_SUFFIX = {
@@ -292,18 +303,31 @@ def needs_install() -> bool:
292
303
  return not has_required_tools(get_servers_dir())
293
304
 
294
305
 
295
- def ensure_tools(auto_install_npm: bool = False, auto_install_vcpp: bool = False) -> None:
296
- """Install tools to ~/.codeboarding/servers/ if needed. No-op if already current."""
297
- if not needs_install():
298
- return
299
- from install import run_install # deferred to avoid circular import at module level
300
-
301
- run_install(
302
- target_dir=get_servers_dir(),
303
- auto_install_npm=auto_install_npm,
304
- auto_install_vcpp=auto_install_vcpp,
305
- )
306
- _write_manifest()
306
+ def _acquire_lock(lock_fd: Any) -> None:
307
+ """Acquire an exclusive file lock, logging if we have to wait."""
308
+ if sys.platform == "win32":
309
+ # msvcrt.LK_LOCK only retries for ~10 s which is too short for tool
310
+ # downloads. Poll with LK_NBLCK every 2 s instead no hard timeout.
311
+ try:
312
+ msvcrt.locking(lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
313
+ except OSError:
314
+ logger.info("Another instance is downloading tools, waiting...")
315
+ print("Waiting for another instance to finish downloading tools...", flush=True, file=sys.stderr)
316
+ while True:
317
+ time.sleep(2)
318
+ try:
319
+ msvcrt.locking(lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
320
+ break
321
+ except OSError:
322
+ continue
323
+ else:
324
+ # fcntl.LOCK_EX blocks indefinitely — exactly what we want.
325
+ try:
326
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
327
+ except BlockingIOError:
328
+ logger.info("Another instance is downloading tools, waiting...")
329
+ print("Waiting for another instance to finish downloading tools...", flush=True, file=sys.stderr)
330
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
307
331
 
308
332
 
309
333
  def build_config() -> dict[str, Any]:
@@ -461,19 +485,36 @@ def get_latest_release_tag() -> str:
461
485
 
462
486
 
463
487
  def download_asset(tag: str, asset_name: str, destination: Path) -> bool:
464
- """Download a GitHub release asset to destination. Returns True on success."""
488
+ """Download a GitHub release asset to destination. Returns True on success.
489
+
490
+ Writes to a temp file first, then atomically renames to prevent
491
+ corrupt binaries if the download is interrupted.
492
+ """
465
493
  destination.parent.mkdir(parents=True, exist_ok=True)
494
+ temp_dest = destination.with_suffix(destination.suffix + ".download")
466
495
  url = f"https://github.com/{GITHUB_REPO}/releases/download/{tag}/{asset_name}"
467
- response = requests.get(url, stream=True, timeout=300, allow_redirects=True)
468
- response.raise_for_status()
469
- with open(destination, "wb") as f:
470
- for chunk in response.iter_content(chunk_size=32768):
471
- if chunk:
472
- f.write(chunk)
473
- return destination.exists() and destination.stat().st_size > 0
496
+ try:
497
+ response = requests.get(url, stream=True, timeout=300, allow_redirects=True)
498
+ response.raise_for_status()
499
+ with open(temp_dest, "wb") as f:
500
+ for chunk in response.iter_content(chunk_size=32768):
501
+ if chunk:
502
+ f.write(chunk)
503
+ if temp_dest.stat().st_size > 0:
504
+ os.replace(temp_dest, destination)
505
+ return True
506
+ temp_dest.unlink(missing_ok=True)
507
+ return False
508
+ except Exception:
509
+ temp_dest.unlink(missing_ok=True)
510
+ raise
474
511
 
475
512
 
476
- def install_native_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
513
+ def install_native_tools(
514
+ target_dir: Path,
515
+ deps: list[ToolDependency],
516
+ on_progress: ProgressCallback | None = None,
517
+ ) -> None:
477
518
  """Download native binaries from GitHub releases."""
478
519
  system = platform.system()
479
520
  suffix = _PLATFORM_SUFFIX.get(system)
@@ -491,8 +532,10 @@ def install_native_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
491
532
  logger.exception("Could not determine latest release")
492
533
  return
493
534
 
494
- for dep in deps:
495
- assert dep.github_asset_template, f"{dep.key}: github_asset_template required for native tools"
535
+ downloadable = [d for d in deps if d.github_asset_template]
536
+ for i, dep in enumerate(downloadable, 1):
537
+ if on_progress:
538
+ on_progress(dep.binary_name, i, len(downloadable))
496
539
  binary_path = bin_dir / f"{dep.binary_name}{exe_suffix()}"
497
540
  if binary_path.exists():
498
541
  logger.info(" %s: already installed, skipping", dep.binary_name)
@@ -511,7 +554,11 @@ def install_native_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
511
554
  binary_path.unlink(missing_ok=True)
512
555
 
513
556
 
514
- def install_node_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
557
+ def install_node_tools(
558
+ target_dir: Path,
559
+ deps: list[ToolDependency],
560
+ on_progress: ProgressCallback | None = None,
561
+ ) -> None:
515
562
  """Install Node.js tools via npm."""
516
563
  npm_command = preferred_npm_command(target_dir)
517
564
  if not npm_command:
@@ -527,6 +574,8 @@ def install_node_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
527
574
  return
528
575
 
529
576
  env = npm_subprocess_env(target_dir)
577
+ if on_progress:
578
+ on_progress("npm packages", 1, 1)
530
579
  logger.info("Installing Node.js packages: %s", all_packages)
531
580
  try:
532
581
  if not (target_dir / "package.json").exists():
@@ -548,11 +597,18 @@ def install_node_tools(target_dir: Path, deps: list[ToolDependency]) -> None:
548
597
  logger.exception("Node.js package installation failed")
549
598
 
550
599
 
551
- def install_archive_tool(target_dir: Path, dep: ToolDependency) -> None:
600
+ def install_archive_tool(
601
+ target_dir: Path,
602
+ dep: ToolDependency,
603
+ on_progress: ProgressCallback | None = None,
604
+ ) -> None:
552
605
  """Download and extract an archive tool."""
553
606
  assert dep.archive_asset, f"{dep.key}: archive_asset required for archive tools"
554
607
  assert dep.archive_subdir, f"{dep.key}: archive_subdir required for archive tools"
555
608
 
609
+ if on_progress:
610
+ on_progress(dep.key, 1, 1)
611
+
556
612
  extract_dir = target_dir / "bin" / dep.archive_subdir
557
613
  if extract_dir.exists() and (extract_dir / "plugins").is_dir():
558
614
  logger.info("%s already installed", dep.key)
@@ -575,3 +631,5 @@ def install_archive_tool(target_dir: Path, dep: ToolDependency) -> None:
575
631
  except Exception:
576
632
  logger.exception("%s installation failed", dep.key)
577
633
  archive_path.unlink(missing_ok=True)
634
+ logger.exception("%s installation failed", dep.key)
635
+ archive_path.unlink(missing_ok=True)
File without changes
File without changes
File without changes
File without changes
File without changes