raise-cli 2.2.1__py3-none-any.whl

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 (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,374 @@
1
+ """Project type detection for RaiSE initialization.
2
+
3
+ Detects whether a directory is greenfield (no code) or brownfield (existing code)
4
+ by counting source code files while excluding common non-project directories.
5
+ Also detects dominant language and suggests toolchain commands.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import Counter
11
+ from dataclasses import dataclass
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+
15
+ from raise_cli.core.files import EXCLUDED_DIRS, should_exclude_dir
16
+
17
+ # Re-export for backward compatibility
18
+ __all__ = [
19
+ "CODE_EXTENSIONS",
20
+ "EXCLUDED_DIRS",
21
+ "LANGUAGE_TOOLCHAIN",
22
+ "ProjectType",
23
+ "DetectionResult",
24
+ "ToolchainInfo",
25
+ "detect_language",
26
+ "detect_project_type",
27
+ ]
28
+
29
+ # Common code file extensions to detect
30
+ CODE_EXTENSIONS: frozenset[str] = frozenset(
31
+ {
32
+ # Python
33
+ ".py",
34
+ # JavaScript/TypeScript
35
+ ".js",
36
+ ".jsx",
37
+ ".ts",
38
+ ".tsx",
39
+ ".mjs",
40
+ ".cjs",
41
+ # JVM
42
+ ".java",
43
+ ".kt",
44
+ ".scala",
45
+ # Systems
46
+ ".c",
47
+ ".cpp",
48
+ ".cc",
49
+ ".cxx",
50
+ ".h",
51
+ ".hpp",
52
+ ".rs",
53
+ ".go",
54
+ # Scripting
55
+ ".rb",
56
+ ".php",
57
+ ".pl",
58
+ ".pm",
59
+ # .NET
60
+ ".cs",
61
+ ".fs",
62
+ ".vb",
63
+ # Other
64
+ ".swift",
65
+ ".m",
66
+ ".mm",
67
+ ".lua",
68
+ ".r",
69
+ ".R",
70
+ ".jl",
71
+ ".dart",
72
+ ".ex",
73
+ ".exs",
74
+ ".erl",
75
+ ".hrl",
76
+ ".clj",
77
+ ".cljs",
78
+ ".elm",
79
+ ".hs",
80
+ }
81
+ )
82
+
83
+
84
+ # Map file extensions to language names
85
+ EXTENSION_TO_LANGUAGE: dict[str, str] = {
86
+ ".py": "python",
87
+ ".pyi": "python",
88
+ ".ts": "typescript",
89
+ ".tsx": "typescript",
90
+ ".js": "javascript",
91
+ ".jsx": "javascript",
92
+ ".mjs": "javascript",
93
+ ".cjs": "javascript",
94
+ ".cs": "csharp",
95
+ ".fs": "fsharp",
96
+ ".vb": "vb",
97
+ ".java": "java",
98
+ ".kt": "kotlin",
99
+ ".scala": "scala",
100
+ ".go": "go",
101
+ ".rs": "rust",
102
+ ".rb": "ruby",
103
+ ".php": "php",
104
+ ".dart": "dart",
105
+ ".swift": "swift",
106
+ ".c": "c",
107
+ ".cpp": "cpp",
108
+ ".cc": "cpp",
109
+ ".cxx": "cpp",
110
+ ".h": "c",
111
+ ".hpp": "cpp",
112
+ ".ex": "elixir",
113
+ ".exs": "elixir",
114
+ ".erl": "erlang",
115
+ ".hrl": "erlang",
116
+ ".hs": "haskell",
117
+ ".elm": "elm",
118
+ ".clj": "clojure",
119
+ ".cljs": "clojure",
120
+ ".jl": "julia",
121
+ ".lua": "lua",
122
+ ".r": "r",
123
+ ".R": "r",
124
+ ".pl": "perl",
125
+ ".pm": "perl",
126
+ ".m": "objective-c",
127
+ ".mm": "objective-c",
128
+ }
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class ToolchainInfo:
133
+ """Suggested toolchain commands for a language.
134
+
135
+ Attributes:
136
+ language: Detected language name.
137
+ test_command: Suggested test runner command.
138
+ lint_command: Suggested linter command, if known.
139
+ type_check_command: Suggested type checker command, if known.
140
+ """
141
+
142
+ language: str
143
+ test_command: str | None = None
144
+ lint_command: str | None = None
145
+ type_check_command: str | None = None
146
+
147
+
148
+ # Default toolchain commands per language
149
+ LANGUAGE_TOOLCHAIN: dict[str, ToolchainInfo] = {
150
+ "python": ToolchainInfo(
151
+ language="python",
152
+ test_command="uv run pytest --tb=short",
153
+ lint_command="uv run ruff check",
154
+ type_check_command="uv run pyright",
155
+ ),
156
+ "typescript": ToolchainInfo(
157
+ language="typescript",
158
+ test_command="npx vitest run",
159
+ lint_command="npx eslint .",
160
+ type_check_command="npx tsc --noEmit",
161
+ ),
162
+ "javascript": ToolchainInfo(
163
+ language="javascript",
164
+ test_command="npx vitest run",
165
+ lint_command="npx eslint .",
166
+ ),
167
+ "csharp": ToolchainInfo(
168
+ language="csharp",
169
+ test_command="dotnet test --verbosity quiet",
170
+ lint_command="dotnet format --verify-no-changes",
171
+ type_check_command="dotnet build --no-restore",
172
+ ),
173
+ "java": ToolchainInfo(
174
+ language="java",
175
+ test_command="mvn test",
176
+ lint_command="mvn checkstyle:check",
177
+ ),
178
+ "go": ToolchainInfo(
179
+ language="go",
180
+ test_command="go test ./...",
181
+ lint_command="golangci-lint run",
182
+ type_check_command="go vet ./...",
183
+ ),
184
+ "rust": ToolchainInfo(
185
+ language="rust",
186
+ test_command="cargo test",
187
+ lint_command="cargo clippy",
188
+ type_check_command="cargo check",
189
+ ),
190
+ "php": ToolchainInfo(
191
+ language="php",
192
+ test_command="vendor/bin/phpunit",
193
+ lint_command="vendor/bin/php-cs-fixer fix --dry-run",
194
+ type_check_command="vendor/bin/phpstan analyse",
195
+ ),
196
+ "dart": ToolchainInfo(
197
+ language="dart",
198
+ test_command="flutter test",
199
+ lint_command="dart fix --dry-run",
200
+ type_check_command="dart analyze",
201
+ ),
202
+ "ruby": ToolchainInfo(
203
+ language="ruby",
204
+ test_command="bundle exec rspec",
205
+ lint_command="bundle exec rubocop",
206
+ ),
207
+ "kotlin": ToolchainInfo(
208
+ language="kotlin",
209
+ test_command="./gradlew test",
210
+ lint_command="./gradlew ktlintCheck",
211
+ ),
212
+ "swift": ToolchainInfo(
213
+ language="swift",
214
+ test_command="swift test",
215
+ lint_command="swiftlint",
216
+ ),
217
+ "elixir": ToolchainInfo(
218
+ language="elixir",
219
+ test_command="mix test",
220
+ lint_command="mix credo",
221
+ type_check_command="mix dialyzer",
222
+ ),
223
+ }
224
+
225
+
226
+ class ProjectType(StrEnum):
227
+ """Type of project based on existing code.
228
+
229
+ Values:
230
+ GREENFIELD: No existing code files (new project)
231
+ BROWNFIELD: Has existing code files
232
+ """
233
+
234
+ GREENFIELD = "greenfield"
235
+ BROWNFIELD = "brownfield"
236
+
237
+
238
+ @dataclass(frozen=True)
239
+ class DetectionResult:
240
+ """Result of project type detection.
241
+
242
+ Attributes:
243
+ project_type: Whether the project is greenfield or brownfield.
244
+ code_file_count: Number of code files detected.
245
+ language: Dominant language detected, if any.
246
+ toolchain: Suggested toolchain commands for the detected language.
247
+ """
248
+
249
+ project_type: ProjectType
250
+ code_file_count: int
251
+ language: str | None = None
252
+ toolchain: ToolchainInfo | None = None
253
+
254
+
255
+ def _count_extensions(directory: Path) -> Counter[str]:
256
+ """Count code file extensions in a directory recursively.
257
+
258
+ Excludes hidden directories, node_modules, __pycache__, etc.
259
+
260
+ Args:
261
+ directory: Root directory to scan.
262
+
263
+ Returns:
264
+ Counter mapping file extensions to their counts.
265
+ """
266
+ counts: Counter[str] = Counter()
267
+ if not directory.is_dir():
268
+ return counts
269
+
270
+ try:
271
+ for item in directory.iterdir():
272
+ if item.is_dir():
273
+ if not should_exclude_dir(item):
274
+ counts += _count_extensions(item)
275
+ elif item.is_file() and item.suffix in CODE_EXTENSIONS:
276
+ counts[item.suffix] += 1
277
+ except PermissionError:
278
+ pass
279
+
280
+ return counts
281
+
282
+
283
+ def count_code_files(directory: Path) -> int:
284
+ """Count code files in a directory recursively.
285
+
286
+ Excludes hidden directories, node_modules, __pycache__, etc.
287
+
288
+ Args:
289
+ directory: Root directory to scan.
290
+
291
+ Returns:
292
+ Number of code files found.
293
+ """
294
+ return sum(_count_extensions(directory).values())
295
+
296
+
297
+ def detect_language(directory: Path) -> ToolchainInfo | None:
298
+ """Detect the dominant language in a directory.
299
+
300
+ Counts file extensions, maps them to languages, and returns the
301
+ toolchain info for the most common language.
302
+
303
+ Args:
304
+ directory: Root directory to scan.
305
+
306
+ Returns:
307
+ ToolchainInfo for the dominant language, or None if no code files found.
308
+ """
309
+ ext_counts = _count_extensions(directory)
310
+ if not ext_counts:
311
+ return None
312
+
313
+ # Map extensions to language counts
314
+ lang_counts: Counter[str] = Counter()
315
+ for ext, count in ext_counts.items():
316
+ lang = EXTENSION_TO_LANGUAGE.get(ext)
317
+ if lang:
318
+ lang_counts[lang] += count
319
+
320
+ if not lang_counts:
321
+ return None
322
+
323
+ dominant_lang = lang_counts.most_common(1)[0][0]
324
+ return LANGUAGE_TOOLCHAIN.get(
325
+ dominant_lang,
326
+ ToolchainInfo(language=dominant_lang),
327
+ )
328
+
329
+
330
+ def detect_project_type(directory: Path) -> DetectionResult:
331
+ """Detect whether a directory is greenfield or brownfield.
332
+
333
+ A greenfield project has no code files.
334
+ A brownfield project has at least one code file.
335
+ For brownfield projects, also detects the dominant language
336
+ and suggests toolchain commands.
337
+
338
+ Args:
339
+ directory: Directory to analyze.
340
+
341
+ Returns:
342
+ DetectionResult with project type, file count, and language info.
343
+ """
344
+ ext_counts = _count_extensions(directory)
345
+ code_file_count = sum(ext_counts.values())
346
+
347
+ if code_file_count == 0:
348
+ return DetectionResult(
349
+ project_type=ProjectType.GREENFIELD,
350
+ code_file_count=0,
351
+ )
352
+
353
+ # Detect dominant language from extension counts
354
+ lang_counts: Counter[str] = Counter()
355
+ for ext, count in ext_counts.items():
356
+ lang = EXTENSION_TO_LANGUAGE.get(ext)
357
+ if lang:
358
+ lang_counts[lang] += count
359
+
360
+ language: str | None = None
361
+ toolchain: ToolchainInfo | None = None
362
+ if lang_counts:
363
+ language = lang_counts.most_common(1)[0][0]
364
+ toolchain = LANGUAGE_TOOLCHAIN.get(
365
+ language,
366
+ ToolchainInfo(language=language),
367
+ )
368
+
369
+ return DetectionResult(
370
+ project_type=ProjectType.BROWNFIELD,
371
+ code_file_count=code_file_count,
372
+ language=language,
373
+ toolchain=toolchain,
374
+ )