codeboarding 0.9.4__tar.gz → 0.9.5__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 (145) hide show
  1. {codeboarding-0.9.4/codeboarding.egg-info → codeboarding-0.9.5}/PKG-INFO +1 -1
  2. codeboarding-0.9.5/README.md +149 -0
  3. {codeboarding-0.9.4 → codeboarding-0.9.5/codeboarding.egg-info}/PKG-INFO +1 -1
  4. {codeboarding-0.9.4 → codeboarding-0.9.5}/codeboarding.egg-info/SOURCES.txt +2 -0
  5. {codeboarding-0.9.4 → codeboarding-0.9.5}/install.py +86 -45
  6. {codeboarding-0.9.4 → codeboarding-0.9.5}/pyproject.toml +1 -1
  7. codeboarding-0.9.5/tests/test_install.py +120 -0
  8. codeboarding-0.9.5/tests/test_tool_registry.py +94 -0
  9. codeboarding-0.9.5/tests/test_user_config.py +65 -0
  10. {codeboarding-0.9.4 → codeboarding-0.9.5}/tool_registry.py +97 -14
  11. codeboarding-0.9.4/README.md +0 -281
  12. codeboarding-0.9.4/tests/test_install.py +0 -51
  13. {codeboarding-0.9.4 → codeboarding-0.9.5}/LICENSE +0 -0
  14. {codeboarding-0.9.4 → codeboarding-0.9.5}/PYPI.md +0 -0
  15. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/__init__.py +0 -0
  16. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/abstraction_agent.py +0 -0
  17. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/agent.py +0 -0
  18. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/agent_responses.py +0 -0
  19. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/cluster_methods_mixin.py +0 -0
  20. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/constants.py +0 -0
  21. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/dependency_discovery.py +0 -0
  22. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/details_agent.py +0 -0
  23. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/llm_config.py +0 -0
  24. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/meta_agent.py +0 -0
  25. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/planner_agent.py +0 -0
  26. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/__init__.py +0 -0
  27. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/abstract_prompt_factory.py +0 -0
  28. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/claude_prompts.py +0 -0
  29. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/deepseek_prompts.py +0 -0
  30. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/gemini_flash_prompts.py +0 -0
  31. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/glm_prompts.py +0 -0
  32. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/gpt_prompts.py +0 -0
  33. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/kimi_prompts.py +0 -0
  34. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/prompts/prompt_factory.py +0 -0
  35. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/__init__.py +0 -0
  36. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/base.py +0 -0
  37. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/get_external_deps.py +0 -0
  38. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/get_method_invocations.py +0 -0
  39. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_cfg.py +0 -0
  40. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_docs.py +0 -0
  41. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_file.py +0 -0
  42. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_file_structure.py +0 -0
  43. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_git_diff.py +0 -0
  44. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_packages.py +0 -0
  45. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_source.py +0 -0
  46. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/read_structure.py +0 -0
  47. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/tools/toolkit.py +0 -0
  48. {codeboarding-0.9.4 → codeboarding-0.9.5}/agents/validation.py +0 -0
  49. {codeboarding-0.9.4 → codeboarding-0.9.5}/caching/__init__.py +0 -0
  50. {codeboarding-0.9.4 → codeboarding-0.9.5}/caching/cache.py +0 -0
  51. {codeboarding-0.9.4 → codeboarding-0.9.5}/caching/meta_cache.py +0 -0
  52. {codeboarding-0.9.4 → codeboarding-0.9.5}/codeboarding.egg-info/dependency_links.txt +0 -0
  53. {codeboarding-0.9.4 → codeboarding-0.9.5}/codeboarding.egg-info/entry_points.txt +0 -0
  54. {codeboarding-0.9.4 → codeboarding-0.9.5}/codeboarding.egg-info/requires.txt +0 -0
  55. {codeboarding-0.9.4 → codeboarding-0.9.5}/codeboarding.egg-info/top_level.txt +0 -0
  56. {codeboarding-0.9.4 → codeboarding-0.9.5}/core/__init__.py +0 -0
  57. {codeboarding-0.9.4 → codeboarding-0.9.5}/core/plugin_loader.py +0 -0
  58. {codeboarding-0.9.4 → codeboarding-0.9.5}/core/protocols.py +0 -0
  59. {codeboarding-0.9.4 → codeboarding-0.9.5}/core/registry.py +0 -0
  60. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/__init__.py +0 -0
  61. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/analysis_json.py +0 -0
  62. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/diagram_generator.py +0 -0
  63. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/file_coverage.py +0 -0
  64. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/__init__.py +0 -0
  65. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/component_checker.py +0 -0
  66. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/file_manager.py +0 -0
  67. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/impact_analyzer.py +0 -0
  68. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/io_utils.py +0 -0
  69. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/models.py +0 -0
  70. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/path_patching.py +0 -0
  71. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/reexpansion.py +0 -0
  72. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/scoped_analysis.py +0 -0
  73. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/updater.py +0 -0
  74. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/incremental/validation.py +0 -0
  75. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/manifest.py +0 -0
  76. {codeboarding-0.9.4 → codeboarding-0.9.5}/diagram_analysis/version.py +0 -0
  77. {codeboarding-0.9.4 → codeboarding-0.9.5}/duckdb_crud.py +0 -0
  78. {codeboarding-0.9.4 → codeboarding-0.9.5}/github_action.py +0 -0
  79. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/__init__.py +0 -0
  80. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/__init__.py +0 -0
  81. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/circular_deps.py +0 -0
  82. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/cohesion.py +0 -0
  83. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/coupling.py +0 -0
  84. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/function_size.py +0 -0
  85. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/god_class.py +0 -0
  86. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/inheritance.py +0 -0
  87. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/instability.py +0 -0
  88. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/checks/unused_code_diagnostics.py +0 -0
  89. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/config.py +0 -0
  90. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/constants.py +0 -0
  91. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/models.py +0 -0
  92. {codeboarding-0.9.4 → codeboarding-0.9.5}/health/runner.py +0 -0
  93. {codeboarding-0.9.4 → codeboarding-0.9.5}/health_main.py +0 -0
  94. {codeboarding-0.9.4 → codeboarding-0.9.5}/logging_config.py +0 -0
  95. {codeboarding-0.9.4 → codeboarding-0.9.5}/main.py +0 -0
  96. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/__init__.py +0 -0
  97. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/callbacks.py +0 -0
  98. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/context.py +0 -0
  99. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/mixin.py +0 -0
  100. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/paths.py +0 -0
  101. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/stats.py +0 -0
  102. {codeboarding-0.9.4 → codeboarding-0.9.5}/monitoring/writers.py +0 -0
  103. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/__init__.py +0 -0
  104. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/html.py +0 -0
  105. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/html_template.py +0 -0
  106. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/markdown.py +0 -0
  107. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/mdx.py +0 -0
  108. {codeboarding-0.9.4 → codeboarding-0.9.5}/output_generators/sphinx.py +0 -0
  109. {codeboarding-0.9.4 → codeboarding-0.9.5}/repo_utils/__init__.py +0 -0
  110. {codeboarding-0.9.4 → codeboarding-0.9.5}/repo_utils/change_detector.py +0 -0
  111. {codeboarding-0.9.4 → codeboarding-0.9.5}/repo_utils/errors.py +0 -0
  112. {codeboarding-0.9.4 → codeboarding-0.9.5}/repo_utils/git_diff.py +0 -0
  113. {codeboarding-0.9.4 → codeboarding-0.9.5}/repo_utils/ignore.py +0 -0
  114. {codeboarding-0.9.4 → codeboarding-0.9.5}/setup.cfg +0 -0
  115. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/__init__.py +0 -0
  116. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/analysis_cache.py +0 -0
  117. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/analysis_result.py +0 -0
  118. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/cluster_change_analyzer.py +0 -0
  119. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/cluster_helpers.py +0 -0
  120. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/constants.py +0 -0
  121. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/git_diff_analyzer.py +0 -0
  122. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/graph.py +0 -0
  123. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/incremental_orchestrator.py +0 -0
  124. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/java_config_scanner.py +0 -0
  125. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/java_utils.py +0 -0
  126. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/__init__.py +0 -0
  127. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/client.py +0 -0
  128. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/diagnostics.py +0 -0
  129. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/java_client.py +0 -0
  130. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/language_settings.py +0 -0
  131. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/lsp_client/typescript_client.py +0 -0
  132. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/programming_language.py +0 -0
  133. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/reference_resolve_mixin.py +0 -0
  134. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/scanner.py +0 -0
  135. {codeboarding-0.9.4 → codeboarding-0.9.5}/static_analyzer/typescript_config_scanner.py +0 -0
  136. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_github_action.py +0 -0
  137. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_incremental_analyzer.py +0 -0
  138. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_logging_config.py +0 -0
  139. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_main.py +0 -0
  140. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_vscode_constants.py +0 -0
  141. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_windows_compatibility.py +0 -0
  142. {codeboarding-0.9.4 → codeboarding-0.9.5}/tests/test_windows_encoding.py +0 -0
  143. {codeboarding-0.9.4 → codeboarding-0.9.5}/user_config.py +0 -0
  144. {codeboarding-0.9.4 → codeboarding-0.9.5}/utils.py +0 -0
  145. {codeboarding-0.9.4 → codeboarding-0.9.5}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -0,0 +1,149 @@
1
+ # CodeBoarding
2
+
3
+ See what your AI is building before it breaks.
4
+
5
+ CodeBoarding gives developers and coding agents a visual map of a codebase. It combines static analysis with LLM reasoning to generate architecture diagrams, component-level documentation, and navigable outputs you can use in your IDE, CI, and docs.
6
+
7
+ [Website](https://codeboarding.org) · [Open VSX extension](https://open-vsx.org/extension/CodeBoarding/codeboarding) · [Explore examples](https://codeboarding.org/diagrams) · [VS Code extension](https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding) · [GitHub Action](https://github.com/marketplace/actions/codeboarding-diagram-first-documentation) ·[Discord](https://discord.gg/T5zHTJYFuy)
8
+
9
+ [![CodeBoarding demo](docs/assets/codeboarding-demo.gif)](https://open-vsx.org/extension/CodeBoarding/codeboarding)
10
+
11
+ Install the extension from Open VSX.
12
+
13
+ [![JavaScript](https://img.shields.io/badge/JavaScript-222222?style=flat-square&logo=javascript&logoColor=F7DF1E)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
15
+ [![Java](https://img.shields.io/badge/Java-E76F00?style=flat-square&logo=openjdk&logoColor=white)](https://www.java.com/)
16
+ [![Python](https://img.shields.io/badge/Python-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
17
+ [![Go](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev/)
18
+ [![PHP](https://img.shields.io/badge/PHP-777BB4?style=flat-square&logo=php&logoColor=white)](https://www.php.net/)
19
+
20
+ ## Few use cases:
21
+
22
+ - Keep architecture visible while agents code.
23
+ - Review AI-generated changes with system context before they turn into hidden debt.
24
+ - Understand large repositories faster with layered diagrams and component breakdowns.
25
+ - Share the same visual model across local workflows, IDEs, pull requests, and docs.
26
+
27
+ ## What CodeBoarding generates
28
+
29
+ - High-level system architecture diagrams.
30
+ - Deeper component diagrams for important subsystems.
31
+ - Markdown documentation in `.codeboarding/`.
32
+ - Mermaid output that is easy to embed in docs and PRs.
33
+ - Incremental updates when only part of the codebase changes.
34
+
35
+ ## How it works
36
+
37
+ ```mermaid
38
+ graph LR
39
+ Application_Orchestrator_Repository_Manager["Application Orchestrator & Repository Manager"]
40
+ LLM_Agent_Core["LLM Agent Core"]
41
+ Static_Code_Analyzer["Static Code Analyzer"]
42
+ Agent_Tooling_Interface["Agent Tooling Interface"]
43
+ Incremental_Analysis_Engine["Incremental Analysis Engine"]
44
+ Documentation_Diagram_Generator["Documentation & Diagram Generator"]
45
+ Application_Orchestrator_Repository_Manager -- "Orchestrator initiates analysis workflow, leveraging incremental updates based on detected code changes." --> Incremental_Analysis_Engine
46
+ Application_Orchestrator_Repository_Manager -- "Orchestrator passes project context and triggers the main analysis workflow for the LLM Agent." --> LLM_Agent_Core
47
+ Incremental_Analysis_Engine -- "Incremental engine requests static analysis for specific code segments (new or changed)." --> Static_Code_Analyzer
48
+ Static_Code_Analyzer -- "Static analyzer provides analysis results to the incremental engine for caching." --> Incremental_Analysis_Engine
49
+ LLM_Agent_Core -- "LLM Agent invokes specialized tools to interact with the codebase and analysis data." --> Agent_Tooling_Interface
50
+ Agent_Tooling_Interface -- "Agent tools query the static analysis engine for detailed code insights." --> Static_Code_Analyzer
51
+ Static_Code_Analyzer -- "Static analysis engine provides requested data to the agent tools." --> Agent_Tooling_Interface
52
+ LLM_Agent_Core -- "LLM Agent delivers structured analysis insights for documentation and diagram generation." --> Documentation_Diagram_Generator
53
+ click Application_Orchestrator_Repository_Manager href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/Application_Orchestrator_Repository_Manager.md" "Details"
54
+ click LLM_Agent_Core href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/LLM_Agent_Core.md" "Details"
55
+ click Static_Code_Analyzer href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/Static_Code_Analyzer.md" "Details"
56
+ click Agent_Tooling_Interface href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/Agent_Tooling_Interface.md" "Details"
57
+ click Incremental_Analysis_Engine href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/Incremental_Analysis_Engine.md" "Details"
58
+ click Documentation_Diagram_Generator href "https://github.com/CodeBoarding/CodeBoarding/blob/main/.codeboarding/Documentation_Diagram_Generator.md" "Details"
59
+ ```
60
+
61
+ For a deeper architecture walkthrough, see [`.codeboarding/overview.md`](.codeboarding/overview.md).
62
+
63
+ ## Quick start
64
+
65
+ ### Run from source
66
+
67
+ ```bash
68
+ uv sync --frozen
69
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
70
+ python install.py
71
+ python main.py --local /path/to/repo
72
+ ```
73
+
74
+ ### Use the packaged CLI
75
+
76
+ ```bash
77
+ pip install codeboarding
78
+ codeboarding-setup
79
+ codeboarding --local /path/to/repo
80
+ ```
81
+
82
+ Output is written to `/path/to/repo/.codeboarding/`.
83
+
84
+ `python install.py` and `codeboarding-setup` download language server binaries to `~/.codeboarding/servers/`, shared across projects. `npm` is required for Python, TypeScript, JavaScript, and PHP language servers; if it is missing, setup can install it via `nodeenv`.
85
+
86
+ ## Configuration
87
+
88
+ On first run, CodeBoarding creates `~/.codeboarding/config.toml`. Set one provider there or use environment variables.
89
+
90
+ ```toml
91
+ [provider]
92
+ # openai_api_key = "sk-..."
93
+ # anthropic_api_key = "sk-ant-..."
94
+ # google_api_key = "AIza..."
95
+ # vercel_api_key = "vck_..."
96
+ # aws_bearer_token_bedrock = "..."
97
+ # ollama_base_url = "http://localhost:11434"
98
+ # openrouter_api_key = "sk-..."
99
+
100
+ [llm]
101
+ # agent_model = "gemini-3-flash"
102
+ # parsing_model = "gemini-3-flash"
103
+ ```
104
+
105
+ Shell environment variables such as `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, and `OLLAMA_BASE_URL` take precedence over the config file. For private repositories, set `GITHUB_TOKEN` in your environment.
106
+
107
+ ## Common commands
108
+
109
+ ```bash
110
+ # Analyze a local repository
111
+ python main.py --local ./my-project
112
+
113
+ # Increase diagram depth
114
+ python main.py --local ./my-project --depth-level 2
115
+
116
+ # Re-analyze only changed parts when possible
117
+ python main.py --local ./my-project --incremental
118
+
119
+ # Update a single component by ID
120
+ python main.py --local ./my-project --partial-component-id "a3f2b1c4d5e6f789"
121
+
122
+ # Analyze a remote GitHub repository
123
+ python main.py https://github.com/pytorch/pytorch
124
+ ```
125
+
126
+ ## Where to use it
127
+
128
+ - [CLI](https://github.com/CodeBoarding/CodeBoarding) for local analysis, automation, and CI workflows.
129
+ - [VS Code extension](https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding) for in-editor visual architecture.
130
+ - [GitHub Action](https://github.com/marketplace/actions/codeboarding-diagram-first-documentation) to keep diagrams updated in CI.
131
+
132
+ ## Supported stack
133
+
134
+ - Languages: Python, TypeScript, JavaScript, Java, Go, PHP.
135
+ - LLM providers: OpenAI, Anthropic, Google, Vercel AI Gateway, AWS Bedrock, Ollama, OpenRouter, and more.
136
+
137
+ ## Examples
138
+
139
+ - Visualized 800+ open-source repositories.
140
+ - Browse generated examples in [GeneratedOnBoardings](https://github.com/CodeBoarding/GeneratedOnBoardings).
141
+ - Try the hosted explorer at [codeboarding.org/diagrams](https://codeboarding.org/diagrams).
142
+
143
+ ## Contributing
144
+
145
+ If you want to improve CodeBoarding, open an [issue](https://github.com/CodeBoarding/CodeBoarding/issues) or send a pull request. We welcome improvements to analysis quality, output generators, integrations, and developer experience.
146
+
147
+ ## Vision
148
+
149
+ CodeBoarding is building an open standard for code understanding: a visual, accurate, high-level representation of a codebase that both humans and agents can use.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -134,6 +134,8 @@ tests/test_incremental_analyzer.py
134
134
  tests/test_install.py
135
135
  tests/test_logging_config.py
136
136
  tests/test_main.py
137
+ tests/test_tool_registry.py
138
+ tests/test_user_config.py
137
139
  tests/test_vscode_constants.py
138
140
  tests/test_windows_compatibility.py
139
141
  tests/test_windows_encoding.py
@@ -5,6 +5,7 @@ import platform
5
5
  import shutil
6
6
  import subprocess
7
7
  import sys
8
+ import tarfile
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
 
@@ -18,7 +19,10 @@ from tool_registry import (
18
19
  install_archive_tool,
19
20
  install_native_tools,
20
21
  install_node_tools,
22
+ npm_subprocess_env,
21
23
  platform_bin_dir,
24
+ preferred_node_path,
25
+ preferred_npm_command,
22
26
  )
23
27
  from user_config import ensure_config_template
24
28
 
@@ -43,15 +47,17 @@ class LanguageSupportCheck:
43
47
  return False, reason
44
48
 
45
49
 
46
- def check_npm():
47
- """Check if npm is installed on the system."""
50
+ def check_npm(target_dir: Path | None = None) -> bool:
51
+ """Check if npm is available via the configured Node.js runtime or PATH."""
48
52
  print("Step: npm check started")
49
53
 
50
- npm_path = shutil.which("npm")
54
+ target = (target_dir or get_servers_dir()).resolve()
55
+ npm_command = preferred_npm_command(target)
51
56
 
52
- if npm_path:
57
+ if npm_command:
53
58
  try:
54
- result = subprocess.run([npm_path, "--version"], capture_output=True, text=True, check=True)
59
+ env = npm_subprocess_env(target)
60
+ result = subprocess.run([*npm_command, "--version"], capture_output=True, text=True, check=True, env=env)
55
61
  print(f"Step: npm check finished: success (version {result.stdout.strip()})")
56
62
  return True
57
63
  except Exception as e:
@@ -59,31 +65,71 @@ def check_npm():
59
65
  f"Step: npm check finished: failure - npm command failed ({e}). Skipping Language Servers installation."
60
66
  )
61
67
  return False
62
- else:
63
- print("Step: npm check finished: failure - npm not found")
64
- return False
68
+
69
+ print("Step: npm check finished: failure - npm not found")
70
+ return False
71
+
72
+
73
+ def bootstrapped_npm_cli_path(target_dir: Path) -> Path:
74
+ """Return the npm CLI entrypoint inside the target directory."""
75
+ return target_dir / "npm" / "package" / "bin" / "npm-cli.js"
65
76
 
66
77
 
67
- def install_npm_with_nodeenv() -> bool:
68
- """Install npm locally in the active Python virtual environment using nodeenv."""
69
- command = [sys.executable, "-m", "nodeenv", "--python-virtualenv"]
78
+ def extract_tarball_safely(fileobj: io.BytesIO, destination: Path) -> None:
79
+ """Extract a tarball while rejecting path traversal entries."""
80
+ destination = destination.resolve()
81
+ with tarfile.open(fileobj=fileobj, mode="r:gz") as tar:
82
+ for member in tar.getmembers():
83
+ member_path = (destination / member.name).resolve()
84
+ if not member_path.is_relative_to(destination):
85
+ raise ValueError(f"Unsafe tar entry: {member.name}")
86
+ tar.extractall(destination)
87
+
88
+
89
+ def bootstrap_npm(target_dir: Path | None = None) -> bool:
90
+ """Download npm from the registry and invoke it through the configured Node.js runtime."""
91
+ target = (target_dir or get_servers_dir()).resolve()
92
+ target.mkdir(parents=True, exist_ok=True)
93
+ node_path = preferred_node_path(target)
94
+
70
95
  print("Step: npm remediation started")
71
- print(f" command: {' '.join(command)}")
72
- print(" impact: installs Node.js + npm into the active Python virtual environment")
96
+ print(f" target: {target}")
97
+ print(" impact: installs npm into the CodeBoarding tools directory and invokes it via Node.js")
73
98
 
74
- try:
75
- result = subprocess.run(command, capture_output=True, text=True, check=True)
76
- except subprocess.CalledProcessError as e:
77
- print(f"Step: npm remediation finished: failure - {e}")
78
- if e.stderr:
79
- print(f" {e.stderr.strip()}")
80
- print(" You can install Node.js manually from: https://nodejs.org/en/download")
81
- print(" Then verify with: npm --version")
99
+ if not node_path:
100
+ print("Step: npm remediation finished: failure - no Node.js runtime available")
101
+ print(" Set CODEBOARDING_NODE_PATH or install Node.js from: https://nodejs.org/en/download")
82
102
  return False
83
103
 
84
- npm_available = check_npm()
104
+ os.environ.setdefault("CODEBOARDING_NODE_PATH", node_path)
105
+ print(f" node: {os.environ['CODEBOARDING_NODE_PATH']}")
106
+
107
+ npm_cli = bootstrapped_npm_cli_path(target)
108
+ if not npm_cli.exists():
109
+ npm_root = npm_cli.parent.parent.parent
110
+ metadata_url = "https://registry.npmjs.org/npm/latest"
111
+ try:
112
+ metadata_response = requests.get(metadata_url, timeout=30)
113
+ metadata_response.raise_for_status()
114
+ metadata = metadata_response.json()
115
+ tarball_url = metadata["dist"]["tarball"]
116
+ print(f" source: {tarball_url}")
117
+
118
+ tarball_response = requests.get(tarball_url, timeout=60)
119
+ tarball_response.raise_for_status()
120
+
121
+ shutil.rmtree(npm_root, ignore_errors=True)
122
+ npm_root.mkdir(parents=True, exist_ok=True)
123
+ extract_tarball_safely(io.BytesIO(tarball_response.content), npm_root)
124
+ except (requests.RequestException, KeyError, OSError, ValueError, tarfile.TarError) as e:
125
+ print(f"Step: npm remediation finished: failure - {e}")
126
+ print(" Install Node.js manually from: https://nodejs.org/en/download")
127
+ print(" Then verify with: npm --version")
128
+ return False
129
+
130
+ npm_available = check_npm(target)
85
131
  if not npm_available:
86
- print("Step: npm remediation finished: failure - npm still not found after nodeenv install")
132
+ print("Step: npm remediation finished: failure - npm still not found after bootstrap")
87
133
  print(" You can install Node.js manually from: https://nodejs.org/en/download")
88
134
  print(" Then verify with: npm --version")
89
135
  return False
@@ -97,30 +143,25 @@ def is_non_interactive_mode() -> bool:
97
143
  return bool(os.getenv("CI")) or not sys.stdin.isatty()
98
144
 
99
145
 
100
- def resolve_missing_npm(auto_install_npm: bool = False) -> bool:
101
- """Prompt the user to install npm; abort if they decline or if non-interactive.
146
+ def resolve_missing_npm(auto_install_npm: bool = False, target_dir: Path | None = None) -> bool:
147
+ """Try to bootstrap npm; fall back gracefully if it cannot be obtained.
102
148
 
103
- Returns True only when npm becomes available. Raises SystemExit if the user
104
- declines or if running non-interactively, because npm is required.
149
+ Returns True when npm becomes available, False otherwise.
150
+ In non-interactive mode (VS Code extension / CI) the function never raises —
151
+ a missing npm just means Node-based language servers will be unavailable.
105
152
  """
106
153
  print("Step: npm required for TypeScript/JavaScript/PHP/Python language servers")
107
154
 
108
- if auto_install_npm:
109
- installed = install_npm_with_nodeenv()
155
+ if auto_install_npm or is_non_interactive_mode():
156
+ installed = bootstrap_npm(target_dir=target_dir)
110
157
  if not installed:
111
- print("Error: npm installation failed. Install Node.js from https://nodejs.org/en/download and retry.")
112
- raise SystemExit(1)
113
- return True
114
-
115
- if is_non_interactive_mode():
116
- print("Error: npm is required but not found and cannot be installed non-interactively.")
117
- print(" Re-run with --auto-install-npm to install npm in this virtual environment,")
118
- print(" or install Node.js manually from: https://nodejs.org/en/download")
119
- raise SystemExit(1)
158
+ print("Warning: npm bootstrap failed. Node-based language servers will be unavailable.")
159
+ print(" Install Node.js from https://nodejs.org/en/download to enable them.")
160
+ return installed
120
161
 
121
- choice = input("npm is missing. Install it now using nodeenv in this virtual environment? [y/N]: ").strip().lower()
162
+ choice = input("npm is missing. Install it now using the configured Node.js runtime? [y/N]: ").strip().lower()
122
163
  if choice in {"y", "yes"}:
123
- installed = install_npm_with_nodeenv()
164
+ installed = bootstrap_npm(target_dir=target_dir)
124
165
  if not installed:
125
166
  print("Error: npm installation failed. Install Node.js from https://nodejs.org/en/download and retry.")
126
167
  raise SystemExit(1)
@@ -130,12 +171,12 @@ def resolve_missing_npm(auto_install_npm: bool = False) -> bool:
130
171
  raise SystemExit(1)
131
172
 
132
173
 
133
- def resolve_npm_availability(auto_install_npm: bool = False) -> bool:
174
+ def resolve_npm_availability(auto_install_npm: bool = False, target_dir: Path | None = None) -> bool:
134
175
  """Determine npm availability and run remediation when needed."""
135
- npm_available = check_npm()
176
+ npm_available = check_npm(target_dir)
136
177
 
137
178
  if not npm_available:
138
- npm_available = resolve_missing_npm(auto_install_npm=auto_install_npm)
179
+ npm_available = resolve_missing_npm(auto_install_npm=auto_install_npm, target_dir=target_dir)
139
180
 
140
181
  return npm_available
141
182
 
@@ -146,7 +187,7 @@ def parse_args() -> argparse.Namespace:
146
187
  parser.add_argument(
147
188
  "--auto-install-npm",
148
189
  action="store_true",
149
- help="Automatically install npm via nodeenv when npm is missing",
190
+ help="Automatically bootstrap npm when missing (downloads from registry, runs via Node.js)",
150
191
  )
151
192
  parser.add_argument(
152
193
  "--auto-install-vcpp",
@@ -483,7 +524,7 @@ def run_install(
483
524
 
484
525
  ensure_config_template()
485
526
 
486
- npm_available = resolve_npm_availability(auto_install_npm=auto_install_npm)
527
+ npm_available = resolve_npm_availability(auto_install_npm=auto_install_npm, target_dir=target)
487
528
  if npm_available:
488
529
  install_node_servers(target)
489
530
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.9.4"
7
+ version = "0.9.5"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,120 @@
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+ from unittest.mock import Mock, patch
7
+
8
+ import install
9
+
10
+
11
+ class TestParseArgs(unittest.TestCase):
12
+ def test_parse_args_defaults(self):
13
+ with patch.object(sys, "argv", ["install.py"]):
14
+ args = install.parse_args()
15
+ self.assertFalse(args.auto_install_npm)
16
+
17
+ def test_parse_args_auto_install_npm(self):
18
+ with patch.object(sys, "argv", ["install.py", "--auto-install-npm"]):
19
+ args = install.parse_args()
20
+ self.assertTrue(args.auto_install_npm)
21
+
22
+
23
+ class TestResolveMissingNpm(unittest.TestCase):
24
+ @patch("install.bootstrap_npm", return_value=True)
25
+ def test_auto_install_mode_bootstraps_npm(self, mock_bootstrap):
26
+ target_dir = Path("/tmp/codeboarding-servers")
27
+ result = install.resolve_missing_npm(auto_install_npm=True, target_dir=target_dir)
28
+ self.assertTrue(result)
29
+ mock_bootstrap.assert_called_once_with(target_dir=target_dir)
30
+
31
+ @patch("install.bootstrap_npm", return_value=False)
32
+ @patch("install.is_non_interactive_mode", return_value=True)
33
+ def test_non_interactive_falls_back_gracefully(self, mock_non_interactive, mock_bootstrap):
34
+ """Non-interactive mode should not raise SystemExit — just return False."""
35
+ result = install.resolve_missing_npm(auto_install_npm=False)
36
+ self.assertFalse(result)
37
+ mock_bootstrap.assert_called_once()
38
+
39
+ @patch("install.bootstrap_npm", return_value=False)
40
+ def test_auto_install_failure_degrades_gracefully(self, mock_bootstrap):
41
+ """When auto_install is True but bootstrap fails, return False (don't crash server)."""
42
+ result = install.resolve_missing_npm(auto_install_npm=True, target_dir=Path("/tmp/test"))
43
+ self.assertFalse(result)
44
+
45
+
46
+ class TestResolveNpmAvailability(unittest.TestCase):
47
+ @patch("install.resolve_missing_npm")
48
+ @patch("install.check_npm", return_value=True)
49
+ def test_npm_present_does_not_attempt_remediation(self, mock_check_npm, mock_resolve_missing_npm):
50
+ target_dir = Path("/tmp/codeboarding-servers")
51
+ result = install.resolve_npm_availability(auto_install_npm=False, target_dir=target_dir)
52
+ self.assertTrue(result)
53
+ mock_check_npm.assert_called_once_with(target_dir)
54
+ mock_resolve_missing_npm.assert_not_called()
55
+
56
+ @patch("install.resolve_missing_npm", return_value=True)
57
+ @patch("install.check_npm", return_value=False)
58
+ def test_missing_npm_attempts_remediation(self, mock_check_npm, mock_resolve_missing_npm):
59
+ target_dir = Path("/tmp/codeboarding-servers")
60
+ result = install.resolve_npm_availability(auto_install_npm=True, target_dir=target_dir)
61
+ self.assertTrue(result)
62
+ mock_check_npm.assert_called_once_with(target_dir)
63
+ mock_resolve_missing_npm.assert_called_once_with(auto_install_npm=True, target_dir=target_dir)
64
+
65
+
66
+ class TestBootstrapNpm(unittest.TestCase):
67
+ @patch("install.check_npm", return_value=True)
68
+ @patch("install.requests.get")
69
+ @patch("install.preferred_node_path", return_value="/custom/node")
70
+ def test_bootstraps_npm_cli_when_missing(self, mock_preferred_node_path, mock_requests_get, mock_check_npm):
71
+ with tempfile.TemporaryDirectory() as temp_dir, patch.dict(os.environ, {}, clear=True):
72
+ target_dir = Path(temp_dir)
73
+
74
+ metadata_response = Mock()
75
+ metadata_response.raise_for_status.return_value = None
76
+ metadata_response.json.return_value = {
77
+ "dist": {"tarball": "https://registry.npmjs.org/npm/-/npm-1.0.0.tgz"}
78
+ }
79
+ tarball_response = Mock()
80
+ tarball_response.raise_for_status.return_value = None
81
+ tarball_response.content = b"fake-tarball"
82
+ mock_requests_get.side_effect = [metadata_response, tarball_response]
83
+
84
+ with patch("install.extract_tarball_safely") as mock_extract, patch("install.shutil.rmtree") as mock_rmtree:
85
+ result = install.bootstrap_npm(target_dir=target_dir)
86
+ self.assertEqual(os.environ["CODEBOARDING_NODE_PATH"], "/custom/node")
87
+
88
+ self.assertTrue(result)
89
+ mock_preferred_node_path.assert_called_once_with(target_dir.resolve())
90
+ mock_requests_get.assert_any_call("https://registry.npmjs.org/npm/latest", timeout=30)
91
+ mock_requests_get.assert_any_call("https://registry.npmjs.org/npm/-/npm-1.0.0.tgz", timeout=60)
92
+ mock_rmtree.assert_called_once_with(target_dir.resolve() / "npm", ignore_errors=True)
93
+ mock_extract.assert_called_once()
94
+ mock_check_npm.assert_called_once_with(target_dir.resolve())
95
+
96
+ @patch("install.check_npm", return_value=True)
97
+ @patch("install.requests.get")
98
+ @patch("install.preferred_node_path", return_value="/custom/node")
99
+ def test_uses_existing_bootstrapped_npm_cli(self, mock_preferred_node_path, mock_requests_get, mock_check_npm):
100
+ with tempfile.TemporaryDirectory() as temp_dir, patch.dict(os.environ, {}, clear=True):
101
+ target_dir = Path(temp_dir)
102
+ npm_cli = install.bootstrapped_npm_cli_path(target_dir)
103
+ npm_cli.parent.mkdir(parents=True, exist_ok=True)
104
+ npm_cli.write_text("", encoding="utf-8")
105
+
106
+ result = install.bootstrap_npm(target_dir=target_dir)
107
+
108
+ self.assertTrue(result)
109
+ mock_preferred_node_path.assert_called_once_with(target_dir.resolve())
110
+ mock_requests_get.assert_not_called()
111
+ mock_check_npm.assert_called_once_with(target_dir.resolve())
112
+
113
+ @patch("install.preferred_node_path", return_value=None)
114
+ def test_returns_false_when_no_node_runtime_is_available(self, mock_preferred_node_path):
115
+ with tempfile.TemporaryDirectory() as temp_dir, patch.dict(os.environ, {}, clear=True):
116
+ target_dir = Path(temp_dir)
117
+ result = install.bootstrap_npm(target_dir=target_dir)
118
+
119
+ self.assertFalse(result)
120
+ mock_preferred_node_path.assert_called_once_with(target_dir.resolve())
@@ -0,0 +1,94 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from tool_registry import TOOL_REGISTRY, ToolKind, install_node_tools, resolve_config
8
+
9
+
10
+ class TestToolRegistry(unittest.TestCase):
11
+ @patch("platform.system", return_value="Linux")
12
+ @patch.dict(os.environ, {"CODEBOARDING_NODE_PATH": "/vscode/node"}, clear=False)
13
+ def test_resolve_config_uses_explicit_node_path_for_node_servers(self, mock_system):
14
+ with tempfile.TemporaryDirectory() as temp_dir:
15
+ base_dir = Path(temp_dir)
16
+ (base_dir / "node_modules" / ".bin").mkdir(parents=True)
17
+ (base_dir / "node_modules" / ".bin" / "typescript-language-server").write_text("")
18
+ ts_dir = base_dir / "node_modules" / "typescript-language-server"
19
+ ts_dir.mkdir(parents=True)
20
+ (ts_dir / "cli.mjs").write_text("")
21
+
22
+ config = resolve_config(base_dir)
23
+ command = config["lsp_servers"]["typescript"]["command"]
24
+
25
+ self.assertEqual(command[0], "/vscode/node")
26
+ self.assertTrue(command[1].endswith("cli.mjs"))
27
+ self.assertEqual(command[2:], ["--stdio", "--log-level=2"])
28
+
29
+ @patch("platform.system", return_value="Linux")
30
+ def test_resolve_config_falls_back_to_embedded_nodeenv_node(self, mock_system):
31
+ with tempfile.TemporaryDirectory() as temp_dir:
32
+ base_dir = Path(temp_dir)
33
+ nodeenv_bin = base_dir / "nodeenv" / "bin"
34
+ nodeenv_bin.mkdir(parents=True)
35
+ (nodeenv_bin / "node").write_text("")
36
+ (nodeenv_bin / "npm").write_text("")
37
+
38
+ (base_dir / "node_modules" / ".bin").mkdir(parents=True)
39
+ (base_dir / "node_modules" / ".bin" / "pyright-langserver").write_text("")
40
+ pyright_dir = base_dir / "node_modules" / "pyright" / "dist"
41
+ pyright_dir.mkdir(parents=True)
42
+ (pyright_dir / "langserver.index.js").write_text("")
43
+
44
+ config = resolve_config(base_dir)
45
+ command = config["lsp_servers"]["python"]["command"]
46
+
47
+ self.assertEqual(command[0], str(nodeenv_bin / "node"))
48
+ self.assertTrue(command[1].endswith("langserver.index.js"))
49
+ self.assertEqual(command[2:], ["--stdio"])
50
+
51
+ @patch("tool_registry.subprocess.run")
52
+ @patch("platform.system", return_value="Linux")
53
+ def test_install_node_tools_prefers_embedded_npm(self, mock_system, mock_run):
54
+ with tempfile.TemporaryDirectory() as temp_dir:
55
+ base_dir = Path(temp_dir)
56
+ nodeenv_bin = base_dir / "nodeenv" / "bin"
57
+ nodeenv_bin.mkdir(parents=True)
58
+ (nodeenv_bin / "npm").write_text("")
59
+
60
+ node_deps = [dep for dep in TOOL_REGISTRY if dep.kind is ToolKind.NODE]
61
+ install_node_tools(base_dir, node_deps)
62
+
63
+ self.assertGreaterEqual(mock_run.call_count, 2)
64
+ first_command = mock_run.call_args_list[0].args[0]
65
+ second_command = mock_run.call_args_list[1].args[0]
66
+ self.assertEqual(first_command[0], str(nodeenv_bin / "npm"))
67
+ self.assertEqual(second_command[0], str(nodeenv_bin / "npm"))
68
+ # Verify env is passed with ELECTRON_RUN_AS_NODE
69
+ for call in mock_run.call_args_list:
70
+ env = call.kwargs.get("env", {})
71
+ self.assertEqual(env.get("ELECTRON_RUN_AS_NODE"), "1")
72
+
73
+ @patch("tool_registry.subprocess.run")
74
+ @patch("platform.system", return_value="Linux")
75
+ @patch.dict(os.environ, {"CODEBOARDING_NODE_PATH": "/vscode/node"}, clear=False)
76
+ def test_install_node_tools_uses_bootstrapped_npm_cli(self, mock_system, mock_run):
77
+ """When only a bootstrapped npm-cli.js exists, use [node, npm-cli.js, ...]."""
78
+ with tempfile.TemporaryDirectory() as temp_dir:
79
+ base_dir = Path(temp_dir)
80
+ npm_cli = base_dir / "npm" / "package" / "bin" / "npm-cli.js"
81
+ npm_cli.parent.mkdir(parents=True)
82
+ npm_cli.write_text("")
83
+
84
+ node_deps = [dep for dep in TOOL_REGISTRY if dep.kind is ToolKind.NODE]
85
+ install_node_tools(base_dir, node_deps)
86
+
87
+ self.assertGreaterEqual(mock_run.call_count, 2)
88
+ first_command = mock_run.call_args_list[0].args[0]
89
+ self.assertEqual(first_command[0], "/vscode/node")
90
+ self.assertEqual(first_command[1], str(npm_cli))
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main()