codebase-onboarder 0.6.0 → 0.7.0

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 (448) hide show
  1. package/README.md +174 -46
  2. package/bin/build-if-source.js +23 -0
  3. package/cli/commands.d.ts +26 -0
  4. package/cli/commands.js +836 -811
  5. package/cli/commands.js.map +1 -0
  6. package/cli/explorer/advanced.d.ts +14 -0
  7. package/cli/explorer/advanced.js +321 -329
  8. package/cli/explorer/advanced.js.map +1 -0
  9. package/cli/explorer/app.d.ts +1 -0
  10. package/cli/explorer/app.js +384 -313
  11. package/cli/explorer/app.js.map +1 -0
  12. package/cli/explorer/assistant.d.ts +9 -0
  13. package/cli/explorer/assistant.js +58 -0
  14. package/cli/explorer/assistant.js.map +1 -0
  15. package/cli/explorer/commands.d.ts +17 -0
  16. package/cli/explorer/commands.js +368 -302
  17. package/cli/explorer/commands.js.map +1 -0
  18. package/cli/explorer/featureViews.d.ts +124 -0
  19. package/cli/explorer/featureViews.js +179 -0
  20. package/cli/explorer/featureViews.js.map +1 -0
  21. package/cli/explorer/github.d.ts +21 -0
  22. package/cli/explorer/github.js +42 -44
  23. package/cli/explorer/github.js.map +1 -0
  24. package/cli/explorer/graphs.d.ts +7 -0
  25. package/cli/explorer/graphs.js +90 -95
  26. package/cli/explorer/graphs.js.map +1 -0
  27. package/cli/explorer/picker.d.ts +22 -0
  28. package/cli/explorer/picker.js +150 -0
  29. package/cli/explorer/picker.js.map +1 -0
  30. package/cli/explorer/report.d.ts +85 -0
  31. package/cli/explorer/report.js +60 -0
  32. package/cli/explorer/report.js.map +1 -0
  33. package/cli/explorer/session.d.ts +449 -0
  34. package/cli/explorer/session.js +143 -144
  35. package/cli/explorer/session.js.map +1 -0
  36. package/cli/explorer/views.d.ts +19 -0
  37. package/cli/explorer/views.js +465 -482
  38. package/cli/explorer/views.js.map +1 -0
  39. package/cli/explorer/wrap.d.ts +1 -0
  40. package/cli/explorer/wrap.js +28 -44
  41. package/cli/explorer/wrap.js.map +1 -0
  42. package/cli/main.d.ts +1 -0
  43. package/cli/main.js +214 -161
  44. package/cli/main.js.map +1 -0
  45. package/cli/prompt.d.ts +5 -0
  46. package/cli/prompt.js +84 -80
  47. package/cli/prompt.js.map +1 -0
  48. package/cli/scanOptions.d.ts +2 -0
  49. package/cli/scanOptions.js +11 -0
  50. package/cli/scanOptions.js.map +1 -0
  51. package/cli/ui.d.ts +33 -0
  52. package/cli/ui.js +35 -44
  53. package/cli/ui.js.map +1 -0
  54. package/cli/wizard.d.ts +187 -0
  55. package/cli/wizard.js +304 -284
  56. package/cli/wizard.js.map +1 -0
  57. package/package.json +15 -4
  58. package/public/app.d.ts +1 -0
  59. package/public/app.js +1057 -1009
  60. package/public/app.js.map +1 -0
  61. package/public/browser.d.ts +68 -0
  62. package/public/browser.js +2 -0
  63. package/public/browser.js.map +1 -0
  64. package/public/index.html +30 -13
  65. package/public/js/about.d.ts +6 -0
  66. package/public/js/about.js +152 -172
  67. package/public/js/about.js.map +1 -0
  68. package/public/js/aiDraft.d.ts +3 -0
  69. package/public/js/aiDraft.js +116 -124
  70. package/public/js/aiDraft.js.map +1 -0
  71. package/public/js/analysisPanel.d.ts +7 -0
  72. package/public/js/analysisPanel.js +166 -166
  73. package/public/js/analysisPanel.js.map +1 -0
  74. package/public/js/analysisReport.d.ts +38 -0
  75. package/public/js/analysisReport.js +105 -110
  76. package/public/js/analysisReport.js.map +1 -0
  77. package/public/js/api.d.ts +26 -0
  78. package/public/js/api.js +176 -167
  79. package/public/js/api.js.map +1 -0
  80. package/public/js/atlas.d.ts +6 -0
  81. package/public/js/atlas.js +50 -58
  82. package/public/js/atlas.js.map +1 -0
  83. package/public/js/blameView.d.ts +2 -0
  84. package/public/js/blameView.js +36 -37
  85. package/public/js/blameView.js.map +1 -0
  86. package/public/js/codeTab.d.ts +3 -0
  87. package/public/js/codeTab.js +175 -180
  88. package/public/js/codeTab.js.map +1 -0
  89. package/public/js/codeViewer.d.ts +5 -0
  90. package/public/js/codeViewer.js +162 -150
  91. package/public/js/codeViewer.js.map +1 -0
  92. package/public/js/components/FileChip.d.ts +9 -0
  93. package/public/js/components/FileChip.js +45 -49
  94. package/public/js/components/FileChip.js.map +1 -0
  95. package/public/js/components/MetricSparkline.d.ts +7 -0
  96. package/public/js/components/MetricSparkline.js +35 -44
  97. package/public/js/components/MetricSparkline.js.map +1 -0
  98. package/public/js/components/RiskBadge.d.ts +9 -0
  99. package/public/js/components/RiskBadge.js +55 -63
  100. package/public/js/components/RiskBadge.js.map +1 -0
  101. package/public/js/deepAnalysisView.d.ts +99 -0
  102. package/public/js/deepAnalysisView.js +282 -302
  103. package/public/js/deepAnalysisView.js.map +1 -0
  104. package/public/js/diagramPane.d.ts +8 -0
  105. package/public/js/diagramPane.js +133 -135
  106. package/public/js/diagramPane.js.map +1 -0
  107. package/public/js/diffView.d.ts +6 -0
  108. package/public/js/diffView.js +222 -209
  109. package/public/js/diffView.js.map +1 -0
  110. package/public/js/docsView.d.ts +2 -0
  111. package/public/js/docsView.js +441 -442
  112. package/public/js/docsView.js.map +1 -0
  113. package/public/js/dom.d.ts +189 -0
  114. package/public/js/dom.js +5 -0
  115. package/public/js/dom.js.map +1 -0
  116. package/public/js/fileSourceBrowser.d.ts +5 -0
  117. package/public/js/fileSourceBrowser.js +33 -41
  118. package/public/js/fileSourceBrowser.js.map +1 -0
  119. package/public/js/flameGraph.d.ts +5 -0
  120. package/public/js/flameGraph.js +62 -72
  121. package/public/js/flameGraph.js.map +1 -0
  122. package/public/js/forceGraph.d.ts +10 -0
  123. package/public/js/forceGraph.js +581 -556
  124. package/public/js/forceGraph.js.map +1 -0
  125. package/public/js/heatmap.d.ts +4 -0
  126. package/public/js/heatmap.js +142 -164
  127. package/public/js/heatmap.js.map +1 -0
  128. package/public/js/highlight.d.ts +2 -0
  129. package/public/js/highlight.js +116 -107
  130. package/public/js/highlight.js.map +1 -0
  131. package/public/js/html.d.ts +2 -0
  132. package/public/js/html.js +14 -44
  133. package/public/js/html.js.map +1 -0
  134. package/public/js/insightsView.d.ts +5 -0
  135. package/public/js/insightsView.js +93 -111
  136. package/public/js/insightsView.js.map +1 -0
  137. package/public/js/inspector.d.ts +37 -0
  138. package/public/js/inspector.js +416 -445
  139. package/public/js/inspector.js.map +1 -0
  140. package/public/js/llm.d.ts +113 -0
  141. package/public/js/llm.js +233 -272
  142. package/public/js/llm.js.map +1 -0
  143. package/public/js/mapView.d.ts +3 -0
  144. package/public/js/mapView.js +121 -120
  145. package/public/js/mapView.js.map +1 -0
  146. package/public/js/markdown.d.ts +3 -0
  147. package/public/js/markdown.js +84 -91
  148. package/public/js/markdown.js.map +1 -0
  149. package/public/js/mindmap.d.ts +32 -0
  150. package/public/js/mindmap.js +244 -259
  151. package/public/js/mindmap.js.map +1 -0
  152. package/public/js/mobilePanels.d.ts +5 -0
  153. package/public/js/mobilePanels.js +50 -0
  154. package/public/js/mobilePanels.js.map +1 -0
  155. package/public/js/repoFiles.d.ts +12 -0
  156. package/public/js/repoFiles.js +28 -27
  157. package/public/js/repoFiles.js.map +1 -0
  158. package/public/js/reviewView.d.ts +28 -0
  159. package/public/js/reviewView.js +256 -0
  160. package/public/js/reviewView.js.map +1 -0
  161. package/public/js/sbomView.d.ts +3 -0
  162. package/public/js/sbomView.js +59 -67
  163. package/public/js/sbomView.js.map +1 -0
  164. package/public/js/scanCache.d.ts +16 -0
  165. package/public/js/scanCache.js +67 -65
  166. package/public/js/scanCache.js.map +1 -0
  167. package/public/js/search.d.ts +7 -0
  168. package/public/js/search.js +868 -800
  169. package/public/js/search.js.map +1 -0
  170. package/public/js/searchLocal.d.ts +42 -0
  171. package/public/js/searchLocal.js +87 -0
  172. package/public/js/searchLocal.js.map +1 -0
  173. package/public/js/serverSettings.d.ts +5 -0
  174. package/public/js/serverSettings.js +153 -164
  175. package/public/js/serverSettings.js.map +1 -0
  176. package/public/js/state.d.ts +798 -0
  177. package/public/js/state.js +114 -122
  178. package/public/js/state.js.map +1 -0
  179. package/public/js/tour.d.ts +6 -0
  180. package/public/js/tour.js +5 -7
  181. package/public/js/tour.js.map +1 -0
  182. package/public/js/transitions.d.ts +2 -0
  183. package/public/js/transitions.js +9 -10
  184. package/public/js/transitions.js.map +1 -0
  185. package/public/js/tree.d.ts +12 -0
  186. package/public/js/tree.js +157 -154
  187. package/public/js/tree.js.map +1 -0
  188. package/public/js/workflowsView.d.ts +3 -0
  189. package/public/js/workflowsView.js +15 -22
  190. package/public/js/workflowsView.js.map +1 -0
  191. package/public/search.css +495 -0
  192. package/public/styles.css +163 -346
  193. package/public/workbench.css +166 -0
  194. package/server/apiAuth.d.ts +3 -0
  195. package/server/apiAuth.js +18 -20
  196. package/server/apiAuth.js.map +1 -0
  197. package/server/apiDiff.d.ts +2 -0
  198. package/server/apiDiff.js +20 -18
  199. package/server/apiDiff.js.map +1 -0
  200. package/server/apiDocs.d.ts +2 -0
  201. package/server/apiDocs.js +38 -40
  202. package/server/apiDocs.js.map +1 -0
  203. package/server/apiFile.d.ts +1 -0
  204. package/server/apiFile.js +14 -17
  205. package/server/apiFile.js.map +1 -0
  206. package/server/apiGitBlame.d.ts +2 -0
  207. package/server/apiGitBlame.js +50 -51
  208. package/server/apiGitBlame.js.map +1 -0
  209. package/server/apiMcp.d.ts +4 -0
  210. package/server/apiMcp.js +64 -66
  211. package/server/apiMcp.js.map +1 -0
  212. package/server/apiReview.d.ts +2 -0
  213. package/server/apiReview.js +45 -0
  214. package/server/apiReview.js.map +1 -0
  215. package/server/apiScan.d.ts +5 -0
  216. package/server/apiScan.js +90 -71
  217. package/server/apiScan.js.map +1 -0
  218. package/server/apiSearch.d.ts +33 -0
  219. package/server/apiSearch.js +184 -200
  220. package/server/apiSearch.js.map +1 -0
  221. package/server/apiSettings.d.ts +4 -0
  222. package/server/apiSettings.js +84 -85
  223. package/server/apiSettings.js.map +1 -0
  224. package/server/apiTools.d.ts +3 -0
  225. package/server/apiTools.js +55 -56
  226. package/server/apiTools.js.map +1 -0
  227. package/server/auth.d.ts +5 -0
  228. package/server/auth.js +54 -51
  229. package/server/auth.js.map +1 -0
  230. package/server/config.d.ts +42 -0
  231. package/server/config.js +189 -194
  232. package/server/config.js.map +1 -0
  233. package/server/daemon.d.ts +12 -0
  234. package/server/daemon.js +124 -114
  235. package/server/daemon.js.map +1 -0
  236. package/server/fileSourceNode.d.ts +2 -0
  237. package/server/fileSourceNode.js +34 -36
  238. package/server/fileSourceNode.js.map +1 -0
  239. package/server/gitClone.d.ts +9 -0
  240. package/server/gitClone.js +59 -61
  241. package/server/gitClone.js.map +1 -0
  242. package/server/gitDiff.d.ts +36 -0
  243. package/server/gitDiff.js +271 -169
  244. package/server/gitDiff.js.map +1 -0
  245. package/server/gitHistory.d.ts +12 -0
  246. package/server/gitHistory.js +80 -80
  247. package/server/gitHistory.js.map +1 -0
  248. package/server/gitRemote.d.ts +12 -0
  249. package/server/gitRemote.js +37 -38
  250. package/server/gitRemote.js.map +1 -0
  251. package/server/htmlText.d.ts +4 -0
  252. package/server/htmlText.js +16 -23
  253. package/server/htmlText.js.map +1 -0
  254. package/server/http.d.ts +5 -0
  255. package/server/http.js +27 -30
  256. package/server/http.js.map +1 -0
  257. package/server/httpGuards.d.ts +2 -0
  258. package/server/httpGuards.js +47 -43
  259. package/server/httpGuards.js.map +1 -0
  260. package/server/https.d.ts +34 -0
  261. package/server/https.js +121 -111
  262. package/server/https.js.map +1 -0
  263. package/server/index.d.ts +40 -0
  264. package/server/index.js +139 -147
  265. package/server/index.js.map +1 -0
  266. package/server/layout.d.ts +26 -0
  267. package/server/layout.js +59 -64
  268. package/server/layout.js.map +1 -0
  269. package/server/llmProxy.d.ts +3 -0
  270. package/server/llmProxy.js +137 -135
  271. package/server/llmProxy.js.map +1 -0
  272. package/server/logger.d.ts +38 -0
  273. package/server/logger.js +198 -211
  274. package/server/logger.js.map +1 -0
  275. package/server/mcp/analysis.d.ts +29 -0
  276. package/server/mcp/analysis.js +99 -120
  277. package/server/mcp/analysis.js.map +1 -0
  278. package/server/mcp/http.d.ts +9 -0
  279. package/server/mcp/http.js +128 -137
  280. package/server/mcp/http.js.map +1 -0
  281. package/server/mcp/runner.d.ts +68 -0
  282. package/server/mcp/runner.js +208 -218
  283. package/server/mcp/runner.js.map +1 -0
  284. package/server/mcp/server.d.ts +29 -0
  285. package/server/mcp/server.js +166 -179
  286. package/server/mcp/server.js.map +1 -0
  287. package/server/mcp/standalone.d.ts +1 -0
  288. package/server/mcp/standalone.js +8 -10
  289. package/server/mcp/standalone.js.map +1 -0
  290. package/server/mcp/tools.d.ts +2020 -0
  291. package/server/mcp/tools.js +523 -574
  292. package/server/mcp/tools.js.map +1 -0
  293. package/server/paths.d.ts +3 -0
  294. package/server/paths.js +17 -15
  295. package/server/paths.js.map +1 -0
  296. package/server/pidfile.d.ts +9 -0
  297. package/server/pidfile.js +43 -45
  298. package/server/pidfile.js.map +1 -0
  299. package/server/review.d.ts +5 -0
  300. package/server/review.js +35 -0
  301. package/server/review.js.map +1 -0
  302. package/server/router.d.ts +33 -0
  303. package/server/router.js +191 -197
  304. package/server/router.js.map +1 -0
  305. package/server/scanRequest.d.ts +3 -0
  306. package/server/scanRequest.js +36 -0
  307. package/server/scanRequest.js.map +1 -0
  308. package/server/searchIndex.d.ts +17 -0
  309. package/server/searchIndex.js +83 -80
  310. package/server/searchIndex.js.map +1 -0
  311. package/server/sessions.d.ts +14 -0
  312. package/server/sessions.js +62 -70
  313. package/server/sessions.js.map +1 -0
  314. package/server/startup.d.ts +118 -0
  315. package/server/startup.js +143 -148
  316. package/server/startup.js.map +1 -0
  317. package/server/static.d.ts +6 -0
  318. package/server/static.js +28 -31
  319. package/server/static.js.map +1 -0
  320. package/server/tools/install.d.ts +4 -0
  321. package/server/tools/install.js +176 -185
  322. package/server/tools/install.js.map +1 -0
  323. package/server/tools/parse.d.ts +8 -0
  324. package/server/tools/parse.js +112 -114
  325. package/server/tools/parse.js.map +1 -0
  326. package/server/tools/platform.d.ts +9 -0
  327. package/server/tools/platform.js +49 -47
  328. package/server/tools/platform.js.map +1 -0
  329. package/server/tools/registry.d.ts +137 -0
  330. package/server/tools/registry.js +173 -172
  331. package/server/tools/registry.js.map +1 -0
  332. package/server/tools/scan.d.ts +34 -0
  333. package/server/tools/scan.js +89 -98
  334. package/server/tools/scan.js.map +1 -0
  335. package/server/tools.d.ts +34 -0
  336. package/server/tools.js +94 -107
  337. package/server/tools.js.map +1 -0
  338. package/server/tunnel.d.ts +29 -0
  339. package/server/tunnel.js +61 -66
  340. package/server/tunnel.js.map +1 -0
  341. package/shared/analyzer/docs.d.ts +16 -0
  342. package/shared/analyzer/docs.js +99 -93
  343. package/shared/analyzer/docs.js.map +1 -0
  344. package/shared/analyzer/explainLocal.d.ts +4 -0
  345. package/shared/analyzer/explainLocal.js +122 -142
  346. package/shared/analyzer/explainLocal.js.map +1 -0
  347. package/shared/analyzer/github.d.ts +37 -0
  348. package/shared/analyzer/github.js +41 -43
  349. package/shared/analyzer/github.js.map +1 -0
  350. package/shared/analyzer/graph.d.ts +41 -0
  351. package/shared/analyzer/graph.js +393 -400
  352. package/shared/analyzer/graph.js.map +1 -0
  353. package/shared/analyzer/health.d.ts +33 -0
  354. package/shared/analyzer/health.js +197 -193
  355. package/shared/analyzer/health.js.map +1 -0
  356. package/shared/analyzer/history.d.ts +29 -0
  357. package/shared/analyzer/history.js +110 -111
  358. package/shared/analyzer/history.js.map +1 -0
  359. package/shared/analyzer/jsonConfig.d.ts +2 -0
  360. package/shared/analyzer/jsonConfig.js +61 -0
  361. package/shared/analyzer/jsonConfig.js.map +1 -0
  362. package/shared/analyzer/languages/csharp.d.ts +26 -0
  363. package/shared/analyzer/languages/csharp.js +83 -88
  364. package/shared/analyzer/languages/csharp.js.map +1 -0
  365. package/shared/analyzer/languages/generic.d.ts +23 -0
  366. package/shared/analyzer/languages/generic.js +94 -105
  367. package/shared/analyzer/languages/generic.js.map +1 -0
  368. package/shared/analyzer/languages/go.d.ts +24 -0
  369. package/shared/analyzer/languages/go.js +45 -54
  370. package/shared/analyzer/languages/go.js.map +1 -0
  371. package/shared/analyzer/languages/index.d.ts +6 -0
  372. package/shared/analyzer/languages/index.js +19 -20
  373. package/shared/analyzer/languages/index.js.map +1 -0
  374. package/shared/analyzer/languages/java.d.ts +20 -0
  375. package/shared/analyzer/languages/java.js +73 -78
  376. package/shared/analyzer/languages/java.js.map +1 -0
  377. package/shared/analyzer/languages/javascript.d.ts +12 -0
  378. package/shared/analyzer/languages/javascript.js +258 -209
  379. package/shared/analyzer/languages/javascript.js.map +1 -0
  380. package/shared/analyzer/languages/python.d.ts +23 -0
  381. package/shared/analyzer/languages/python.js +92 -100
  382. package/shared/analyzer/languages/python.js.map +1 -0
  383. package/shared/analyzer/languages/rust.d.ts +17 -0
  384. package/shared/analyzer/languages/rust.js +96 -98
  385. package/shared/analyzer/languages/rust.js.map +1 -0
  386. package/shared/analyzer/languages/typescript.d.ts +11 -0
  387. package/shared/analyzer/languages/typescript.js +117 -116
  388. package/shared/analyzer/languages/typescript.js.map +1 -0
  389. package/shared/analyzer/licenses.d.ts +18 -0
  390. package/shared/analyzer/licenses.js +130 -133
  391. package/shared/analyzer/licenses.js.map +1 -0
  392. package/shared/analyzer/metrics.d.ts +20 -0
  393. package/shared/analyzer/metrics.js +112 -104
  394. package/shared/analyzer/metrics.js.map +1 -0
  395. package/shared/analyzer/pathUtil.d.ts +10 -0
  396. package/shared/analyzer/pathUtil.js +29 -35
  397. package/shared/analyzer/pathUtil.js.map +1 -0
  398. package/shared/analyzer/patterns.d.ts +11 -0
  399. package/shared/analyzer/patterns.js +231 -253
  400. package/shared/analyzer/patterns.js.map +1 -0
  401. package/shared/analyzer/scan.d.ts +62 -0
  402. package/shared/analyzer/scan.js +451 -442
  403. package/shared/analyzer/scan.js.map +1 -0
  404. package/shared/analyzer/scanControl.d.ts +11 -0
  405. package/shared/analyzer/scanControl.js +62 -0
  406. package/shared/analyzer/scanControl.js.map +1 -0
  407. package/shared/analyzer/security.d.ts +18 -0
  408. package/shared/analyzer/security.js +159 -159
  409. package/shared/analyzer/security.js.map +1 -0
  410. package/shared/analyzer/services.d.ts +5 -0
  411. package/shared/analyzer/services.js +141 -138
  412. package/shared/analyzer/services.js.map +1 -0
  413. package/shared/analyzer/stack.d.ts +6 -0
  414. package/shared/analyzer/stack.js +134 -141
  415. package/shared/analyzer/stack.js.map +1 -0
  416. package/shared/analyzer/tour.d.ts +2 -0
  417. package/shared/analyzer/tour.js +45 -53
  418. package/shared/analyzer/tour.js.map +1 -0
  419. package/shared/analyzer/util.d.ts +8 -0
  420. package/shared/analyzer/util.js +130 -76
  421. package/shared/analyzer/util.js.map +1 -0
  422. package/shared/analyzer/workflows.d.ts +8 -0
  423. package/shared/analyzer/workflows.js +139 -134
  424. package/shared/analyzer/workflows.js.map +1 -0
  425. package/shared/contracts.d.ts +250 -0
  426. package/shared/contracts.js +2 -0
  427. package/shared/contracts.js.map +1 -0
  428. package/shared/diagram/aiFacts.d.ts +35 -0
  429. package/shared/diagram/aiFacts.js +102 -105
  430. package/shared/diagram/aiFacts.js.map +1 -0
  431. package/shared/diagram/aiMermaid.d.ts +5 -0
  432. package/shared/diagram/aiMermaid.js +46 -45
  433. package/shared/diagram/aiMermaid.js.map +1 -0
  434. package/shared/diagram/atlas.d.ts +16 -0
  435. package/shared/diagram/atlas.js +51 -60
  436. package/shared/diagram/atlas.js.map +1 -0
  437. package/shared/diagram/mermaid.d.ts +43 -0
  438. package/shared/diagram/mermaid.js +348 -370
  439. package/shared/diagram/mermaid.js.map +1 -0
  440. package/shared/review/contracts.d.ts +95 -0
  441. package/shared/review/contracts.js +2 -0
  442. package/shared/review/contracts.js.map +1 -0
  443. package/shared/review/review.d.ts +10 -0
  444. package/shared/review/review.js +184 -0
  445. package/shared/review/review.js.map +1 -0
  446. package/shared/search/query.d.ts +32 -0
  447. package/shared/search/query.js +395 -388
  448. package/shared/search/query.js.map +1 -0
@@ -5,12 +5,10 @@
5
5
  // draws a canvas (a heat grid, a force graph) the terminal draws block
6
6
  // characters and an arrow tree from the same numbers, because those are the
7
7
  // terminal's own idioms — not degraded versions of the web views.
8
-
9
8
  import fs from 'node:fs/promises';
10
9
  import path from 'node:path';
11
10
  import { execFile } from 'node:child_process';
12
11
  import { promisify } from 'node:util';
13
-
14
12
  import { bold, cyan, dim, ok, warn, bad, panel, row, fit, termWidth } from '../ui.js';
15
13
  import { bar, columns, couplingGrid, depTree, labelled, step } from './graphs.js';
16
14
  import { fileFacts, resolveTarget } from './session.js';
@@ -22,436 +20,430 @@ import { parsePorcelainBlame } from '../../server/apiGitBlame.js';
22
20
  import { couplingMatrix } from '../../shared/analyzer/patterns.js';
23
21
  import { overviewDiagram, layersDiagram, fileDetailDiagram } from '../../shared/diagram/mermaid.js';
24
22
  import { fileStaticDoc, folderStaticDoc } from '../../shared/analyzer/docs.js';
25
-
26
23
  const run = promisify(execFile);
27
24
  const MAX = 200;
28
-
29
25
  // Git history is loaded lazily and cached on the repo object. It shells out, and
30
26
  // a person who never types `log` or `blame` should not pay for it — and on a
31
27
  // large repo that is the difference between an instant prompt and a visible
32
28
  // pause. A failure is cached too, so a missing `git` binary costs one failed
33
29
  // call rather than one per keystroke.
34
30
  export async function loadHistory(repo, { force = false } = {}) {
35
- if (!force && repo._history !== undefined) return repo._history;
36
- try {
37
- const log = await gitLog(repo.root);
38
- if (!log.ok) {
39
- repo._history = unavailableHistory(log.reason);
40
- return repo._history;
31
+ if (!force && repo._history !== undefined)
32
+ return repo._history;
33
+ try {
34
+ const log = await gitLog(repo.root);
35
+ if (!log.ok) {
36
+ repo._history = unavailableHistory(log.reason);
37
+ return repo._history;
38
+ }
39
+ repo._history = analyzeHistory(repo.scan, parseGitLog(log.text), { totalCommits: log.totalCommits });
41
40
  }
42
- repo._history = analyzeHistory(repo.scan, parseGitLog(log.text), { totalCommits: log.totalCommits });
43
- } catch (e) {
44
- repo._history = unavailableHistory('The git history could not be read — the scan itself is unaffected.');
45
- }
46
- return repo._history;
41
+ catch (e) {
42
+ repo._history = unavailableHistory('The git history could not be read — the scan itself is unaffected.');
43
+ }
44
+ return repo._history;
47
45
  }
48
-
49
46
  // ------------------------------------------------------------------- log ---
50
-
51
47
  // Recent commits, and the people who wrote them. The site shows this in a
52
48
  // history tab; the terminal answer is a compact list, because the question this
53
49
  // answers — "who is working on this, and what did they touch lately" — is one
54
50
  // glance, not a browsing session.
55
51
  export async function log(repo, { limit = 15 } = {}) {
56
- const h = await loadHistory(repo);
57
- if (!h.available) return dim(wrapText('No git history: ' + h.reason));
58
- const w = termWidth();
59
- const out = [panel('git history', [
60
- { label: 'commits', value: `${h.commitCount} analyzed` + (h.truncated ? dim(` (of ${h.totalCommits} — log capped)`) : '') },
61
- { label: 'authors', value: h.authors.slice(0, 4).map((a) => `${a.name} (${a.commits})`).join(', ') || '—' },
62
- { label: 'window', value: `${String(h.firstCommitAt || '').slice(0, 10)} → ${String(h.lastCommitAt || '').slice(0, 10)}` },
63
- ])];
64
-
65
- out.push(dim(fit(' recent commits', Math.max(4, termWidth() - 2))));
66
- for (const c of h.commits.slice(0, Math.min(Number(limit) || 15, MAX))) {
67
- // sha / author / date as marker, value, tail, so a narrow terminal drops the
68
- // date rather than running the row off the edge.
69
- out.push(labelled(dim(c.hash.slice(0, 8) + ' '), c.author?.name || '?',
70
- dim(String(c.date || '').slice(0, 10) + ' ' + (c.files?.length || 0) + 'f'), { pad: 4 }));
71
- }
72
- if (h.authors.length) {
73
- out.push(dim(fit(' commits per author', Math.max(4, termWidth() - 2))));
74
- const max = Math.max(...h.authors.map((a) => a.commits));
75
- for (const a of h.authors.slice(0, 8)) {
76
- const tail = dim(String(a.commits).padStart(4)) + (w > 30 ? ' ' + dim(bar(a.commits, max, Math.max(0, Math.min(18, w - 28)))) : '');
77
- out.push(labelled('', a.name, tail, { pad: 4 }));
52
+ const h = await loadHistory(repo);
53
+ if (!h.available)
54
+ return dim(wrapText('No git history: ' + h.reason));
55
+ const w = termWidth();
56
+ const out = [panel('git history', [
57
+ { label: 'commits', value: `${h.commitCount} analyzed` + (h.truncated ? dim(` (of ${h.totalCommits} — log capped)`) : '') },
58
+ { label: 'authors', value: h.authors.slice(0, 4).map((a) => `${a.name} (${a.commits})`).join(', ') || '—' },
59
+ { label: 'window', value: `${String(h.firstCommitAt || '').slice(0, 10)} → ${String(h.lastCommitAt || '').slice(0, 10)}` },
60
+ ])];
61
+ out.push(dim(fit(' recent commits', Math.max(4, termWidth() - 2))));
62
+ for (const c of h.commits.slice(0, Math.min(Number(limit) || 15, MAX))) {
63
+ // sha / author / date as marker, value, tail, so a narrow terminal drops the
64
+ // date rather than running the row off the edge.
65
+ out.push(labelled(dim(c.hash.slice(0, 8) + ' '), c.author?.name || '?', dim(String(c.date || '').slice(0, 10) + ' ' + (c.files?.length || 0) + 'f'), { pad: 4 }));
66
+ }
67
+ if (h.authors.length) {
68
+ out.push(dim(fit(' commits per author', Math.max(4, termWidth() - 2))));
69
+ const max = Math.max(...h.authors.map((a) => a.commits));
70
+ for (const a of h.authors.slice(0, 8)) {
71
+ const tail = dim(String(a.commits).padStart(4)) + (w > 30 ? ' ' + dim(bar(a.commits, max, Math.max(0, Math.min(18, w - 28)))) : '');
72
+ out.push(labelled('', a.name, tail, { pad: 4 }));
73
+ }
78
74
  }
79
- }
80
- return out.join('\n');
75
+ return out.join('\n');
81
76
  }
82
-
83
77
  const GLYPH_MARK = '●';
84
-
85
78
  // ---------------------------------------------------------------- blame ---
86
-
87
79
  // Who wrote each line, and when. `git blame --porcelain` through the same parser
88
80
  // the site's blame view uses, so a line's author and date are the same answer in
89
81
  // both places. Shown as a histogram of authors per line plus the recent
90
82
  // commits, because a per-line listing of a 400-line file is a page of output
91
83
  // nobody reads.
92
84
  export async function blame(repo, { target = '', limit = 20 } = {}) {
93
- const found = resolveTarget(repo, target);
94
- if (found.error) return targetError(found);
95
- if (found.folder) return dim(wrapText(`${found.folder.path || '.'} is a folder — blame a file inside it.`, ' '));
96
-
97
- const p = found.file.path;
98
- let raw;
99
- try {
100
- const { stdout } = await run('git', ['-C', repo.root, 'blame', '--line-porcelain', '--', p], { maxBuffer: 32 * 1024 * 1024 });
101
- raw = stdout;
102
- } catch (e) {
103
- const why = /not a git repository|Unable to read/.test(String(e.stderr || e.message))
104
- ? 'This folder is not in a git repository.'
105
- : 'git blame could not read this file.';
106
- return dim(wrapText(why));
107
- }
108
-
109
- const lines = parsePorcelainBlame(raw);
110
- if (!lines.length) return dim(wrapText('No blame information for this file.'));
111
-
112
- const w = termWidth();
113
- const byAuthor = new Map();
114
- for (const l of lines) byAuthor.set(l.author, (byAuthor.get(l.author) || 0) + 1);
115
- const authors = [...byAuthor.entries()].sort((a, b) => b[1] - a[1]);
116
- const recent = [...new Map(lines.map((l) => [l.sha, l])).values()]
117
- .sort((a, b) => String(b.date).localeCompare(String(a.date)))
118
- .slice(0, Math.min(Number(limit) || 20, 10));
119
-
120
- const out = [panel(p, [
121
- { label: 'lines', value: String(lines.length) },
122
- { label: 'authors', value: String(authors.length) },
123
- { label: 'oldest', value: lines.map((l) => l.date).filter(Boolean).sort()[0]?.slice(0, 10) || '—' },
124
- { label: 'newest', value: lines.map((l) => l.date).filter(Boolean).sort().pop()?.slice(0, 10) || '—' },
125
- ])];
126
-
127
- out.push(dim(fit(' lines per author', Math.max(4, termWidth() - 2))));
128
- const max = Math.max(...authors.map(([, n]) => n));
129
- for (const [name, n] of authors) {
130
- const tail = dim(String(n).padStart(5)) + (w > 34 ? ' ' + dim(bar(n, max, Math.max(0, Math.min(20, w - 30)))) : '');
131
- out.push(labelled('', name, tail, { pad: 4 }));
132
- }
133
- if (recent.length) {
134
- out.push(dim(fit(' most recent commits touching this file', Math.max(4, termWidth() - 2))));
135
- for (const c of recent) {
136
- // Without a separator the author and date ran together into
137
- // "Amitpandey882026-09-25" — the sha-width fit left no gap.
138
- out.push(labelled(dim(c.sha.slice(0, 8) + ' '), c.author, dim(String(c.date || '').slice(0, 10)), { pad: 4 }));
85
+ const found = resolveTarget(repo, target);
86
+ if (found.error)
87
+ return targetError(found);
88
+ if (found.folder)
89
+ return dim(wrapText(`${found.folder.path || '.'} is a folder — blame a file inside it.`, ' '));
90
+ const p = found.file.path;
91
+ let raw;
92
+ try {
93
+ const { stdout } = await run('git', ['-C', repo.root, 'blame', '--line-porcelain', '--', p], { maxBuffer: 32 * 1024 * 1024 });
94
+ raw = stdout;
95
+ }
96
+ catch (e) {
97
+ const why = /not a git repository|Unable to read/.test(String(e.stderr || e.message))
98
+ ? 'This folder is not in a git repository.'
99
+ : 'git blame could not read this file.';
100
+ return dim(wrapText(why));
101
+ }
102
+ const lines = parsePorcelainBlame(raw);
103
+ if (!lines.length)
104
+ return dim(wrapText('No blame information for this file.'));
105
+ const w = termWidth();
106
+ const byAuthor = new Map();
107
+ for (const l of lines)
108
+ byAuthor.set(l.author, (byAuthor.get(l.author) || 0) + 1);
109
+ const authors = [...byAuthor.entries()].sort((a, b) => b[1] - a[1]);
110
+ const recent = [...new Map(lines.map((l) => [l.sha, l])).values()]
111
+ .sort((a, b) => String(b.date).localeCompare(String(a.date)))
112
+ .slice(0, Math.min(Number(limit) || 20, 10));
113
+ const out = [panel(p, [
114
+ { label: 'lines', value: String(lines.length) },
115
+ { label: 'authors', value: String(authors.length) },
116
+ { label: 'oldest', value: lines.map((l) => l.date).filter(Boolean).sort()[0]?.slice(0, 10) || '—' },
117
+ { label: 'newest', value: lines.map((l) => l.date).filter(Boolean).sort().pop()?.slice(0, 10) || '—' },
118
+ ])];
119
+ out.push(dim(fit(' lines per author', Math.max(4, termWidth() - 2))));
120
+ const max = Math.max(...authors.map(([, n]) => n));
121
+ for (const [name, n] of authors) {
122
+ const tail = dim(String(n).padStart(5)) + (w > 34 ? ' ' + dim(bar(n, max, Math.max(0, Math.min(20, w - 30)))) : '');
123
+ out.push(labelled('', name, tail, { pad: 4 }));
139
124
  }
140
- }
141
- return out.join('\n');
125
+ if (recent.length) {
126
+ out.push(dim(fit(' most recent commits touching this file', Math.max(4, termWidth() - 2))));
127
+ for (const c of recent) {
128
+ // Without a separator the author and date ran together into
129
+ // "Amitpandey882026-09-25" — the sha-width fit left no gap.
130
+ out.push(labelled(dim(c.sha.slice(0, 8) + ' '), c.author, dim(String(c.date || '').slice(0, 10)), { pad: 4 }));
131
+ }
132
+ }
133
+ return out.join('\n');
142
134
  }
143
-
144
135
  // ------------------------------------------------------------- coupling ---
145
-
146
136
  // Folder-to-folder traffic as a heat grid. The site draws this on a canvas; a
147
137
  // terminal draws density with block characters, which survives being piped to a
148
138
  // file, printed, or read by someone who cannot distinguish the colors the web
149
139
  // version relies on.
150
140
  export function coupling(repo) {
151
- if (!repo.coupling || !repo.coupling.folders.length) {
152
- return dim(wrapText('No cross-folder imports — this repo is a single package, or nothing imports across folders.'));
153
- }
154
- const grid = couplingGrid(repo, { maxFolders: 10 });
155
- const head = dim(wrapText('folder-to-folder imports (darker = more edges)', ' '));
156
- const legend = dim(fit(' ' + [1, 2, 3, 4].map((n) => step(n, 4)).join('') + ' low → high', Math.max(1, termWidth() - 2)));
157
- return [head, ...grid, legend].join('\n');
141
+ if (!repo.coupling || !repo.coupling.folders.length) {
142
+ return dim(wrapText('No cross-folder imports — this repo is a single package, or nothing imports across folders.'));
143
+ }
144
+ const grid = couplingGrid(repo, { maxFolders: 10 });
145
+ const head = dim(wrapText('folder-to-folder imports (darker = more edges)', ' '));
146
+ const legend = dim(fit(' ' + [1, 2, 3, 4].map((n) => step(n, 4)).join('') + ' low → high', Math.max(1, termWidth() - 2)));
147
+ return [head, ...grid, legend].join('\n');
158
148
  }
159
-
160
149
  // ------------------------------------------------------------- clusters ---
161
-
162
150
  // Louvain communities, described. The site draws these as colored blobs in a
163
151
  // force graph; the terminal names them by the folder they mostly live in, which
164
152
  // is the fact a person actually wants ("this repo is really four things") and is
165
153
  // the same number, not a picture of it.
166
154
  export function clusters(repo, { limit = 8 } = {}) {
167
- const groups = (repo.facts.communities || []).filter((c) => c.size > 1);
168
- if (!groups.length) return dim(wrapText('No distinct module clusters — the graph is too small or too interconnected.'));
169
- const w = termWidth();
170
- const out = [
171
- dim(fit(' module clusters', Math.max(1, termWidth() - 2))),
172
- dim(wrapText('groups of files that import each other more than the rest', ' ')),
173
- ];
174
- for (const c of groups.slice(0, Math.min(Number(limit) || 8, MAX))) {
175
- const folders = tally(c.members);
176
- const where = Object.entries(folders).sort((a, b) => b[1] - a[1]).slice(0, 3)
177
- .map(([f, n]) => `${f}/${n}`).join(' ');
178
- out.push(labelled(ok(GLYPH_MARK + ' '), cyan(where || '(mixed)'), dim(` ${c.size} files`), { pad: 4 }));
179
- }
180
- return out.join('\n');
155
+ const groups = (repo.facts.communities || []).filter((c) => c.size > 1);
156
+ if (!groups.length)
157
+ return dim(wrapText('No distinct module clusters — the graph is too small or too interconnected.'));
158
+ const w = termWidth();
159
+ const out = [
160
+ dim(fit(' module clusters', Math.max(1, termWidth() - 2))),
161
+ dim(wrapText('groups of files that import each other more than the rest', ' ')),
162
+ ];
163
+ for (const c of groups.slice(0, Math.min(Number(limit) || 8, MAX))) {
164
+ const folders = tally(c.members);
165
+ const where = Object.entries(folders).sort((a, b) => b[1] - a[1]).slice(0, 3)
166
+ .map(([f, n]) => `${f}/${n}`).join(' ');
167
+ out.push(labelled(ok(GLYPH_MARK + ' '), cyan(where || '(mixed)'), dim(` ${c.size} files`), { pad: 4 }));
168
+ }
169
+ return out.join('\n');
181
170
  }
182
-
183
171
  function tally(paths) {
184
- const out = {};
185
- for (const p of paths) {
186
- const top = p.includes('/') ? p.slice(0, p.indexOf('/')) : '.';
187
- out[top] = (out[top] || 0) + 1;
188
- }
189
- return out;
172
+ const out = {};
173
+ for (const p of paths) {
174
+ const top = p.includes('/') ? p.slice(0, p.indexOf('/')) : '.';
175
+ out[top] = (out[top] || 0) + 1;
176
+ }
177
+ return out;
190
178
  }
191
-
192
179
  // ---------------------------------------------------------------- blast ---
193
-
194
180
  // What breaks if this file breaks: the transitive set of files that import it,
195
181
  // directly or through anything in between. Direct fan-in is on `deps`; this is
196
182
  // the number that decides whether a change is a one-file edit or a quarter of
197
183
  // the repo, and it is the reason the site has a blast-radius view.
198
184
  export function blast(repo, { target = '', limit = 15 } = {}) {
199
- const found = resolveTarget(repo, target);
200
- if (found.error) return targetError(found);
201
- if (found.folder) return dim(wrapText('Name a file, not a folder.', ' '));
202
-
203
- const p = found.file.path;
204
- // Walk importers transitively. The reverse graph is built once; the site's
205
- // health engine does the same condensation with Tarjan, and for a single file
206
- // this breadth-first walk is the cheap, obvious version of the same answer.
207
- const reverse = new Map();
208
- for (const e of repo.scan.edges) {
209
- if (!reverse.has(e.to)) reverse.set(e.to, []);
210
- reverse.get(e.to).push(e.from);
211
- }
212
- const seen = new Set([p]);
213
- let frontier = [p];
214
- const direct = (repo.facts.importers[p] || []).length;
215
- while (frontier.length) {
216
- const next = [];
217
- for (const cur of frontier) {
218
- for (const parent of reverse.get(cur) || []) {
219
- if (seen.has(parent)) continue;
220
- seen.add(parent);
221
- next.push(parent);
222
- }
185
+ const found = resolveTarget(repo, target);
186
+ if (found.error)
187
+ return targetError(found);
188
+ if (found.folder)
189
+ return dim(wrapText('Name a file, not a folder.', ' '));
190
+ const p = found.file.path;
191
+ // Walk importers transitively. The reverse graph is built once; the site's
192
+ // health engine does the same condensation with Tarjan, and for a single file
193
+ // this breadth-first walk is the cheap, obvious version of the same answer.
194
+ const reverse = new Map();
195
+ for (const e of repo.scan.edges) {
196
+ if (!reverse.has(e.to))
197
+ reverse.set(e.to, []);
198
+ reverse.get(e.to).push(e.from);
223
199
  }
224
- frontier = next;
225
- }
226
- seen.delete(p);
227
-
228
- const affected = [...seen].sort();
229
- const w = termWidth();
230
- const health = repo.health.perFile?.find((f) => f.path === p);
231
- const out = [panel(p, [
232
- { label: 'direct', value: `${direct} file${direct === 1 ? '' : 's'} import this` },
233
- { label: 'transitive', value: `${affected.length} file${affected.length === 1 ? '' : 's'} affected if it breaks` },
234
- { label: 'blast radius', value: repo.scan.files.length ? Math.round((affected.length / repo.scan.files.length) * 100) + '% of the repo' : '—' },
235
- ...(health?.blast !== undefined ? [{ label: 'measured', value: health.blastExact ? String(health.blast) : dim('not computed at this size') }] : []),
236
- ])];
237
-
238
- if (!affected.length) {
239
- out.push(ok(wrapText('Nothing imports this, directly or transitively. It is a leaf — safe to change.', ' ')));
240
- } else {
241
- out.push(dim(fit(` affected files (${affected.length})`, Math.max(4, w - 2))));
242
- for (const a of affected.slice(0, Math.min(Number(limit) || 15, MAX))) {
243
- out.push(labelled(warn(GLYPH_MARK + ' '), a, '', { pad: 4 }));
200
+ const seen = new Set([p]);
201
+ let frontier = [p];
202
+ const direct = (repo.facts.importers[p] || []).length;
203
+ while (frontier.length) {
204
+ const next = [];
205
+ for (const cur of frontier) {
206
+ for (const parent of reverse.get(cur) || []) {
207
+ if (seen.has(parent))
208
+ continue;
209
+ seen.add(parent);
210
+ next.push(parent);
211
+ }
212
+ }
213
+ frontier = next;
244
214
  }
245
- const shown = Math.min(Number(limit) || 15, MAX);
246
- if (affected.length > shown) out.push(dim(fit(` … and ${affected.length - shown} more`, Math.max(4, w - 2))));
247
- }
248
- return out.join('\n');
215
+ seen.delete(p);
216
+ const affected = [...seen].sort();
217
+ const w = termWidth();
218
+ const health = repo.health.perFile?.find((f) => f.path === p);
219
+ const out = [panel(p, [
220
+ { label: 'direct', value: `${direct} file${direct === 1 ? '' : 's'} import this` },
221
+ { label: 'transitive', value: `${affected.length} file${affected.length === 1 ? '' : 's'} affected if it breaks` },
222
+ { label: 'blast radius', value: repo.scan.files.length ? Math.round((affected.length / repo.scan.files.length) * 100) + '% of the repo' : '—' },
223
+ ...(health?.blast !== undefined ? [{ label: 'measured', value: health.blastExact ? String(health.blast) : dim('not computed at this size') }] : []),
224
+ ])];
225
+ if (!affected.length) {
226
+ out.push(ok(wrapText('Nothing imports this, directly or transitively. It is a leaf — safe to change.', ' ')));
227
+ }
228
+ else {
229
+ out.push(dim(fit(` affected files (${affected.length})`, Math.max(4, w - 2))));
230
+ for (const a of affected.slice(0, Math.min(Number(limit) || 15, MAX))) {
231
+ out.push(labelled(warn(GLYPH_MARK + ' '), a, '', { pad: 4 }));
232
+ }
233
+ const shown = Math.min(Number(limit) || 15, MAX);
234
+ if (affected.length > shown)
235
+ out.push(dim(fit(` … and ${affected.length - shown} more`, Math.max(4, w - 2))));
236
+ }
237
+ return out.join('\n');
249
238
  }
250
-
251
-
252
239
  // Files that are both complex and frequently changed. That combination is the
253
240
  // thing worth a second look — complexity alone is a style opinion, churn alone
254
241
  // is just activity, and the intersection is where bugs live. The same number the
255
242
  // site's hotspot view ranks by.
256
243
  export async function hotspots(repo, { limit = 12 } = {}) {
257
- const h = await loadHistory(repo);
258
- if (!h.available) return dim(wrapText('No git history: ' + h.reason));
259
- if (!h.perFile.length) return dim(wrapText('No file has been committed in this window.'));
260
- const w = termWidth();
261
- const top = h.perFile.slice().sort((a, b) => b.hotspot - a.hotspot).slice(0, Math.min(Number(limit) || 12, MAX));
262
- const max = Math.max(...top.map((f) => f.hotspot));
263
- const nameRoom = Math.max(12, w - 34);
264
- const out = [
265
- dim(fit(' hotspots', Math.max(1, termWidth() - 2))),
266
- dim(wrapText('complexity × churn — the files most worth a second look', ' ')),
267
- ];
268
- for (const f of top) {
269
- // The hotspot score is drawn as a bar in the tail, and the bar is the first
270
- // thing `labelled` drops on a narrow terminal — which is the right order:
271
- // the path identifies the file, the churn/complexity numbers explain why it
272
- // is on the list, and the bar is decoration.
273
- const barRoom = Math.max(0, Math.min(16, w - 40));
274
- const tail = dim(` ${f.churn}c ${f.complexity}x `) + (barRoom > 4 ? dim(bar(f.hotspot, max, barRoom)) : '');
275
- out.push(labelled(warn(GLYPH_MARK + ' '), cyan(f.path), tail, { pad: 4 }));
276
- }
277
- const solo = h.perFile.filter((f) => f.solo).length;
278
- if (solo) out.push(dim(wrapText(`${solo} file${solo === 1 ? '' : 's'} changed by exactly one person — a bus factor of 1`, ' ')));
279
- return out.join('\n');
244
+ const h = await loadHistory(repo);
245
+ if (!h.available)
246
+ return dim(wrapText('No git history: ' + h.reason));
247
+ if (!h.perFile.length)
248
+ return dim(wrapText('No file has been committed in this window.'));
249
+ const w = termWidth();
250
+ const top = h.perFile.slice().sort((a, b) => b.hotspot - a.hotspot).slice(0, Math.min(Number(limit) || 12, MAX));
251
+ const max = Math.max(...top.map((f) => f.hotspot));
252
+ const nameRoom = Math.max(12, w - 34);
253
+ const out = [
254
+ dim(fit(' hotspots', Math.max(1, termWidth() - 2))),
255
+ dim(wrapText('complexity × churn — the files most worth a second look', ' ')),
256
+ ];
257
+ for (const f of top) {
258
+ // The hotspot score is drawn as a bar in the tail, and the bar is the first
259
+ // thing `labelled` drops on a narrow terminal — which is the right order:
260
+ // the path identifies the file, the churn/complexity numbers explain why it
261
+ // is on the list, and the bar is decoration.
262
+ const barRoom = Math.max(0, Math.min(16, w - 40));
263
+ const tail = dim(` ${f.churn}c ${f.complexity}x `) + (barRoom > 4 ? dim(bar(f.hotspot, max, barRoom)) : '');
264
+ out.push(labelled(warn(GLYPH_MARK + ' '), cyan(f.path), tail, { pad: 4 }));
265
+ }
266
+ const solo = h.perFile.filter((f) => f.solo).length;
267
+ if (solo)
268
+ out.push(dim(wrapText(`${solo} file${solo === 1 ? '' : 's'} changed by exactly one person — a bus factor of 1`, ' ')));
269
+ return out.join('\n');
280
270
  }
281
-
282
271
  // -------------------------------------------------------------- diagram ---
283
-
284
272
  // Real Mermaid source, from the same generator the site's diagram pane uses. A
285
273
  // terminal cannot render Mermaid, so the honest thing is to emit it and say
286
274
  // where to paste it. The thing a terminal *can* draw is `graph` below, which is
287
275
  // the same information in the terminal's own idiom.
288
276
  export function diagram(repo, { target = '' } = {}) {
289
- const w = termWidth();
290
- const dump = (title, source) => [
291
- bold(' mermaid — ' + title),
292
- dim(' paste into any Mermaid renderer, or open it in the web UI'),
293
- '',
294
- ...source.split('\n').map((l) => ' ' + fit(l, w - 2)),
295
- ].join('\n');
296
-
297
- if (!String(target).trim()) return dump('whole repo', overviewDiagram(repo.scan, repo.facts).source);
298
-
299
- const found = resolveTarget(repo, target);
300
- if (found.error) return targetError(found);
301
- if (found.folder) return dim(wrapText('Name a file, not a folder.', ' '));
302
- return dump(found.file.path, fileDetailDiagram(repo.scan, repo.facts, found.file.path).source);
277
+ const w = termWidth();
278
+ const dump = (title, source) => [
279
+ bold(' mermaid — ' + title),
280
+ dim(' paste into any Mermaid renderer, or open it in the web UI'),
281
+ '',
282
+ ...source.split('\n').map((l) => ' ' + fit(l, w - 2)),
283
+ ].join('\n');
284
+ if (!String(target).trim())
285
+ return dump('whole repo', overviewDiagram(repo.scan, repo.facts).source);
286
+ const found = resolveTarget(repo, target);
287
+ if (found.error)
288
+ return targetError(found);
289
+ if (found.folder)
290
+ return dim(wrapText('Name a file, not a folder.', ' '));
291
+ return dump(found.file.path, fileDetailDiagram(repo.scan, repo.facts, found.file.path).source);
303
292
  }
304
-
305
293
  // The layer stack as Mermaid — the site's "layers" diagram, verbatim.
306
294
  export function layerDiagram(repo) {
307
- const w = termWidth();
308
- return [
309
- bold(' mermaid — layers'),
310
- ...layersDiagram(repo.scan, repo.facts, repo.layers).source.split('\n').map((l) => ' ' + fit(l, w - 2)),
311
- ].join('\n');
295
+ const w = termWidth();
296
+ return [
297
+ bold(' mermaid — layers'),
298
+ ...layersDiagram(repo.scan, repo.facts, repo.layers).source.split('\n').map((l) => ' ' + fit(l, w - 2)),
299
+ ].join('\n');
312
300
  }
313
-
314
301
  // ----------------------------------------------------------------- graph ---
315
-
316
302
  // The dependency tree around a file, in arrows. This is the terminal's force
317
303
  // graph: `→` is "imports", `←` is "imported by", and a cycle shows up as an
318
304
  // arrow pointing back the way it came. Depth-limited because a terminal is not a
319
305
  // canvas — an unbounded graph is a screen of noise in both.
320
306
  export function graph(repo, { target = '', depth = 2, direction = 'both' } = {}) {
321
- const found = resolveTarget(repo, target);
322
- if (found.error) return targetError(found);
323
- if (found.folder) return dim(wrapText('Name a file, not a folder.', ' '));
324
-
325
- if (!repo.byPath) repo.byPath = new Map(repo.scan.files.map((f) => [f.path, f]));
326
- const d = Math.max(1, Math.min(Number(depth) || 2, 6));
327
- return [
328
- dim(fit(' graph — ' + found.file.path, Math.max(4, termWidth() - 2))),
329
- ...depTree(repo, found.file.path, { depth: d, direction }),
330
- dim(fit(' → imports ← imported by', Math.max(4, termWidth() - 2))),
331
- ].join('\n');
307
+ const found = resolveTarget(repo, target);
308
+ if (found.error)
309
+ return targetError(found);
310
+ if (found.folder)
311
+ return dim(wrapText('Name a file, not a folder.', ' '));
312
+ if (!repo.byPath)
313
+ repo.byPath = new Map(repo.scan.files.map((f) => [f.path, f]));
314
+ const d = Math.max(1, Math.min(Number(depth) || 2, 6));
315
+ return [
316
+ dim(fit(' graph — ' + found.file.path, Math.max(4, termWidth() - 2))),
317
+ ...depTree(repo, found.file.path, { depth: d, direction }),
318
+ dim(fit(' → imports ← imported by', Math.max(4, termWidth() - 2))),
319
+ ].join('\n');
332
320
  }
333
-
334
321
  // ------------------------------------------------------------------ docs ---
335
-
336
322
  // Prose documentation for a file or a folder, from the same `docs.js` the site
337
323
  // generates its reference pages from. With no argument it writes a whole
338
324
  // ONBOARDER.md into the repo root — the terminal equivalent of the site's docs
339
325
  // generator, and the thing you want when handing a codebase to someone.
340
326
  export function docs(repo, { target = '' } = {}) {
341
- if (String(target).trim()) {
342
- const found = resolveTarget(repo, target);
343
- if (found.error) return targetError(found);
344
- if (found.folder) {
345
- return [bold(' ' + (found.folder.path || '.')), dim(wrapText(folderStaticDoc(found.folder.path, repo.scan, repo.facts, 0)))].join('\n');
327
+ if (String(target).trim()) {
328
+ const found = resolveTarget(repo, target);
329
+ if (found.error)
330
+ return targetError(found);
331
+ if (found.folder) {
332
+ return [bold(' ' + (found.folder.path || '.')), dim(wrapText(folderStaticDoc(found.folder.path, repo.scan, repo.facts, 0)))].join('\n');
333
+ }
334
+ return [bold(' ' + found.file.path), dim(wrapText(fileStaticDoc(found.file.path, repo.scan, repo.facts)))].join('\n');
335
+ }
336
+ const lines = [
337
+ `# ${repo.name}`, '',
338
+ `> Generated by \`onboarder docs\` — ${new Date().toISOString().slice(0, 10)}`, '',
339
+ 'A map of this codebase: what it is made of, where to start reading, and which files everything leans on.', '',
340
+ '## Start here', '',
341
+ ];
342
+ for (const stop of repo.tour)
343
+ lines.push(`- \`${stop.path}\` — ${stop.why}`);
344
+ lines.push('', '## Folders', '');
345
+ for (const folder of repo.scan.folders) {
346
+ lines.push(`- \`${folder.path}\` — ${folderStaticDoc(folder.path, repo.scan, repo.facts, 0)}`);
346
347
  }
347
- return [bold(' ' + found.file.path), dim(wrapText(fileStaticDoc(found.file.path, repo.scan, repo.facts)))].join('\n');
348
- }
349
-
350
- const lines = [
351
- `# ${repo.name}`, '',
352
- `> Generated by \`onboarder docs\` — ${new Date().toISOString().slice(0, 10)}`, '',
353
- 'A map of this codebase: what it is made of, where to start reading, and which files everything leans on.', '',
354
- '## Start here', '',
355
- ];
356
- for (const stop of repo.tour) lines.push(`- \`${stop.path}\` — ${stop.why}`);
357
- lines.push('', '## Folders', '');
358
- for (const folder of repo.scan.folders) {
359
- lines.push(`- \`${folder.path}\` — ${folderStaticDoc(folder.path, repo.scan, repo.facts, 0)}`);
360
- }
361
- return lines.join('\n');
348
+ return lines.join('\n');
362
349
  }
363
-
364
350
  // Write the whole-repo doc to disk. Separated from `docs` because this one does
365
351
  // I/O and needs to say where it wrote, and because the print form must stay pure
366
352
  // for the tests.
367
353
  export async function writeDocs(repo, target = 'ONBOARDER.md') {
368
- const body = docs(repo, {});
369
- const abs = path.join(repo.root, target);
370
- await fs.writeFile(abs, body + '\n', 'utf8');
371
- return abs;
354
+ const body = docs(repo, {});
355
+ const abs = path.join(repo.root, target);
356
+ await fs.writeFile(abs, body + '\n', 'utf8');
357
+ return abs;
372
358
  }
373
-
374
359
  // --------------------------------------------------------------- symbols ---
375
-
376
360
  // The functions, classes and exports in one file, with line numbers. The site
377
361
  // gets this from a Monaco outline panel; the terminal gets a list you can pipe
378
362
  // into `grep` or read in one screen.
379
363
  export function symbols(repo, { target = '' } = {}) {
380
- const found = resolveTarget(repo, target);
381
- if (found.error) return targetError(found);
382
- if (found.folder) return dim(wrapText('Name a file, not a folder.', ' '));
383
- const f = found.file;
384
- const w = termWidth();
385
- const fns = f.functions || [];
386
- const classes = f.classes || [];
387
- const exports = f.exports || [];
388
- if (!fns.length && !classes.length && !exports.length) {
389
- return dim(wrapText(`${f.name} has no functions, classes, or exports we can recognize.`));
390
- }
391
- const out = [panel(f.path, [
392
- { label: 'functions', value: String(fns.length) },
393
- { label: 'classes', value: String(classes.length) },
394
- { label: 'exports', value: String(exports.length) },
395
- ])];
396
- if (classes.length) {
397
- out.push(dim(fit(' classes', Math.max(2, w - 2))));
398
- for (const c of classes) out.push(labelled('', cyan(c.name), dim(` line ${c.line ?? '?'}`), { pad: 4 }));
399
- }
400
- if (fns.length) {
401
- out.push(dim(fit(' functions', Math.max(2, w - 2))));
402
- for (const fn of fns.slice(0, 60)) {
403
- out.push(labelled('', cyan(fn.name), dim(` ${fn.kind || 'fn'} line ${fn.line ?? '?'}`), { pad: 4 }));
364
+ const found = resolveTarget(repo, target);
365
+ if (found.error)
366
+ return targetError(found);
367
+ if (found.folder)
368
+ return dim(wrapText('Name a file, not a folder.', ' '));
369
+ const f = found.file;
370
+ const w = termWidth();
371
+ const fns = f.functions || [];
372
+ const classes = f.classes || [];
373
+ const exports = f.exports || [];
374
+ if (!fns.length && !classes.length && !exports.length) {
375
+ return dim(wrapText(`${f.name} has no functions, classes, or exports we can recognize.`));
376
+ }
377
+ const out = [panel(f.path, [
378
+ { label: 'functions', value: String(fns.length) },
379
+ { label: 'classes', value: String(classes.length) },
380
+ { label: 'exports', value: String(exports.length) },
381
+ ])];
382
+ if (classes.length) {
383
+ out.push(dim(fit(' classes', Math.max(2, w - 2))));
384
+ for (const c of classes)
385
+ out.push(labelled('', cyan(c.name), dim(` line ${c.line ?? '?'}`), { pad: 4 }));
386
+ }
387
+ if (fns.length) {
388
+ out.push(dim(fit(' functions', Math.max(2, w - 2))));
389
+ for (const fn of fns.slice(0, 60)) {
390
+ out.push(labelled('', cyan(fn.name), dim(` ${fn.kind || 'fn'} line ${fn.line ?? '?'}`), { pad: 4 }));
391
+ }
392
+ if (fns.length > 60)
393
+ out.push(dim(fit(` … and ${fns.length - 60} more`, Math.max(4, w - 2))));
404
394
  }
405
- if (fns.length > 60) out.push(dim(fit(` … and ${fns.length - 60} more`, Math.max(4, w - 2))));
406
- }
407
- return out.join('\n');
395
+ return out.join('\n');
408
396
  }
409
-
410
397
  // ----------------------------------------------------------------- risks ---
411
-
412
398
  // The problems, gathered in one place: cycles, orphans, dead exports, test
413
399
  // coverage, dependency drift. Each has its own command on the site; in a
414
400
  // terminal, "what is wrong with this repo" is one question and deserves one
415
401
  // answer, so they are collected rather than made you ask five times.
416
402
  export function risks(repo) {
417
- const f = repo.facts;
418
- const w = termWidth();
419
- const out = [dim(fit(' risks', Math.max(4, w - 2)))];
420
-
421
- const findings = [];
422
- if (f.cycles?.length) findings.push([f.cycles.length, `circular import${f.cycles.length === 1 ? '' : 's'} — the largest is ${f.cycles[0].length} files`]);
423
- if (f.orphans?.length) findings.push([f.orphans.length, 'files nothing imports — dead code, or entry points we did not see']);
424
- if (f.deadExports?.length) findings.push([f.deadExports.length, `exports nothing imports (${f.deadExports.slice(0, 2).map((e) => e.name).join(', ')})`]);
425
- if (f.testCoverage && f.testCoverage.ratio < 50) findings.push([f.testCoverage.ratio + '%', 'of non-test files are touched by a test — coverage is low']);
426
- if (f.depsDrift?.undeclaredImported?.length) findings.push([f.depsDrift.undeclaredImported.length, `imported but not declared (${f.depsDrift.undeclaredImported.slice(0, 2).join(', ')})`]);
427
- if (f.depsDrift?.unusedDeclared?.length) findings.push([f.depsDrift.unusedDeclared.length, `declared but never imported (${f.depsDrift.unusedDeclared.slice(0, 2).join(', ')})`]);
428
-
429
- if (!findings.length) return ok(wrapText('Nothing obvious is wrong with this repo. health and patterns have more.', ' '));
430
-
431
- // Two layouts. Wide enough: marker, count, then the sentence filling the rest.
432
- // Narrower than the marker plus a usable sentence: the count moves onto its
433
- // own line, because a row of "● 11 imported" is worse than useless — the
434
- // words that carry the meaning get one cell each. Everything is measured as
435
- // plain text; styling happens last, so no escape sequence is ever sliced.
436
- const wide = w >= leadWidth() + 14;
437
- for (const [n, text] of findings) {
438
- if (wide) {
439
- const num = String(n).padStart(4);
440
- const gutter = ' '.repeat(leadWidth() + num.length + 2);
441
- const room = Math.max(1, w - gutter.length);
442
- const wrapped = wrapText(text, '', room).split('\n');
443
- out.push(lead() + dim(num) + ' ' + wrapped[0]
444
- + (wrapped.length > 1 ? '\n' + gutter + wrapped.slice(1).join('\n' + gutter) : ''));
445
- } else {
446
- out.push(fit(lead() + n, Math.max(1, w)));
447
- out.push(wrapText(text, ' '));
403
+ const f = repo.facts;
404
+ const w = termWidth();
405
+ const out = [dim(fit(' risks', Math.max(4, w - 2)))];
406
+ const findings = [];
407
+ if (f.cycles?.length)
408
+ findings.push([f.cycles.length, `circular import${f.cycles.length === 1 ? '' : 's'} — the largest is ${f.cycles[0].length} files`]);
409
+ if (f.orphans?.length)
410
+ findings.push([f.orphans.length, 'files nothing imports — dead code, or entry points we did not see']);
411
+ if (f.deadExports?.length)
412
+ findings.push([f.deadExports.length, `exports nothing imports (${f.deadExports.slice(0, 2).map((e) => e.name).join(', ')})`]);
413
+ if (f.testCoverage && f.testCoverage.ratio < 50)
414
+ findings.push([f.testCoverage.ratio + '%', 'of non-test files are reachable from test imports']);
415
+ if (f.depsDrift?.undeclaredImported?.length)
416
+ findings.push([f.depsDrift.undeclaredImported.length, `imported but not declared (${f.depsDrift.undeclaredImported.slice(0, 2).join(', ')})`]);
417
+ if (f.depsDrift?.unusedDeclared?.length)
418
+ findings.push([f.depsDrift.unusedDeclared.length, `declared but never imported (${f.depsDrift.unusedDeclared.slice(0, 2).join(', ')})`]);
419
+ if (!findings.length)
420
+ return ok(wrapText('Nothing obvious is wrong with this repo. health and patterns have more.', ' '));
421
+ // Two layouts. Wide enough: marker, count, then the sentence filling the rest.
422
+ // Narrower than the marker plus a usable sentence: the count moves onto its
423
+ // own line, because a row of "● 11 imported" is worse than useless — the
424
+ // words that carry the meaning get one cell each. Everything is measured as
425
+ // plain text; styling happens last, so no escape sequence is ever sliced.
426
+ const wide = w >= leadWidth() + 14;
427
+ for (const [n, text] of findings) {
428
+ if (wide) {
429
+ const num = String(n).padStart(4);
430
+ const gutter = ' '.repeat(leadWidth() + num.length + 2);
431
+ const room = Math.max(1, w - gutter.length);
432
+ const wrapped = wrapText(text, '', room).split('\n');
433
+ out.push(lead() + dim(num) + ' ' + wrapped[0]
434
+ + (wrapped.length > 1 ? '\n' + gutter + wrapped.slice(1).join('\n' + gutter) : ''));
435
+ }
436
+ else {
437
+ out.push(fit(lead() + n, Math.max(1, w)));
438
+ out.push(wrapText(text, ' '));
439
+ }
448
440
  }
449
- }
450
- if (f.cycles?.length) out.push(' ' + dim(fit(f.cycles[0].slice(0, 3).join(' → ') + ' → …', Math.max(4, w - 6))));
451
- out.push('');
452
- out.push(dim(fit(' details: health · patterns · deps <file> · externals', Math.max(4, w - 2))));
453
- return out.join('\n');
441
+ if (f.cycles?.length)
442
+ out.push(' ' + dim(fit(f.cycles[0].slice(0, 3).join(' → ') + ' → …', Math.max(4, w - 6))));
443
+ out.push('');
444
+ out.push(dim(fit(' details: health · patterns · deps <file> · externals', Math.max(4, w - 2))));
445
+ return out.join('\n');
454
446
  }
455
-
456
447
  const leadWidth = () => (' ' + GLYPH_MARK + ' ').length;
457
448
  const lead = () => ' ' + GLYPH_MARK + ' ';
449
+ //# sourceMappingURL=advanced.js.map