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
@@ -9,224 +9,214 @@
9
9
  // startup line already use (`server/layout.js` for geometry, `cli/ui.js` for
10
10
  // color), so a panel here is fitted and elided to the terminal exactly the way
11
11
  // every other panel in this project is, including after a resize.
12
-
13
12
  import { bold, cyan, dim, ok, warn, bad, panel, row, fit, termWidth } from '../ui.js';
14
13
  import { labelled } from './graphs.js';
15
14
  import { fileFacts, readRepoFile, resolveTarget, searchRepo, explainRepoFile, explainRepoFolder, explainRepoOverview } from './session.js';
16
15
  import { scanCaveats } from '../../shared/analyzer/explainLocal.js';
17
16
  import { wrapText } from './wrap.js';
18
-
19
17
  const MAX = 200; // a ceiling on any list, so one command cannot flood a terminal
20
-
21
18
  // Glyphs degrade for terminals that cannot show them. TUIKit's rule: a missing
22
19
  // glyph is a rendering bug the user should never have to report, so the ASCII
23
20
  // set is a first-class path, not a fallback we hope nobody needs.
24
21
  const GLYPHS = process.platform === 'win32' && !process.env.WT_SESSION
25
- ? { dir: '+', file: '-', entry: '>', hub: '*', arrow: '->', warn: '!', good: '+' }
26
- : { dir: '▸', file: '·', entry: '▶', hub: '◆', arrow: '→', warn: '⚠', good: '✓' };
27
-
22
+ ? { dir: '+', file: '-', entry: '>', hub: '*', arrow: '->', warn: '!', good: '+' }
23
+ : { dir: '▸', file: '·', entry: '▶', hub: '◆', arrow: '→', warn: '⚠', good: '✓' };
28
24
  // The footer is a hint bar, and a hint bar that wraps is worse than no hint bar
29
25
  // — it is the first thing printed, so it sets the impression of everything else.
30
26
  // On a narrow terminal it drops hints rather than wrapping.
31
27
  function hintBar(hints) {
32
- // The room is the terminal minus the 2-space lead. The `Math.max(8, …)` that
33
- // used to be here was a floor *above* the available width on an 8-column
34
- // terminal, which guarantees an overflow — the exact opposite of what a
35
- // hint bar is for. Below the width of one hint, the hint is cut to fit
36
- // rather than allowed to wrap.
37
- const room = Math.max(1, termWidth() - 2);
38
- const parts = [];
39
- for (const h of hints) {
40
- const next = parts.length ? parts.join(' · ') + ' · ' + h : h;
41
- if (next.length > room) break;
42
- parts.push(h);
43
- }
44
- const line = parts.join(' · ') || hints[0].slice(0, room);
45
- return dim(' ' + line);
28
+ // The room is the terminal minus the 2-space lead. The `Math.max(8, …)` that
29
+ // used to be here was a floor *above* the available width on an 8-column
30
+ // terminal, which guarantees an overflow — the exact opposite of what a
31
+ // hint bar is for. Below the width of one hint, the hint is cut to fit
32
+ // rather than allowed to wrap.
33
+ const room = Math.max(1, termWidth() - 2);
34
+ const parts = [];
35
+ for (const h of hints) {
36
+ const next = parts.length ? parts.join(' · ') + ' · ' + h : h;
37
+ if (next.length > room)
38
+ break;
39
+ parts.push(h);
40
+ }
41
+ const line = parts.join(' · ') || hints[0].slice(0, room);
42
+ return dim(' ' + line);
46
43
  }
47
-
48
44
  // `wrapText` now lives in `./wrap.js`, shared with the command table's help screen.
49
-
50
45
  // -------------------------------------------------------------- overview ---
51
-
52
46
  // The landing view. Answers "what am I looking at, and where do I start" in one
53
47
  // screen — the same three questions the site's first paint answers.
54
48
  export function overview(repo) {
55
- const s = repo.scan.stats;
56
- const h = repo.health;
57
- const langs = repo.languages.slice(0, 4).map((l) => `${l.label} ${l.loc}`).join(', ')
58
- + (repo.languages.length > 4 ? `, +${repo.languages.length - 4} more` : '');
59
-
60
- const rows = [
61
- row('folder', repo.root),
62
- row('files', `${s.filesParsed} parsed` + (s.skipped ? dim(` (${s.skipped} skipped)`) : '') + ` · ${s.edgeCount} imports`),
63
- row('languages', langs || '—'),
64
- ];
65
- if (repo.manifest.packageName) rows.push(row('package', repo.manifest.packageName));
66
- rows.push(row('license', repo.scan.license?.name || 'unknown'));
67
- if (repo.facts.entries.length) rows.push(row('start at', repo.facts.entries.slice(0, 3).join(', ')));
68
- if (repo.facts.hubs.length) {
69
- const top = repo.facts.hubs[0];
70
- rows.push(row('top hub', `${top.path} ${dim(`(${top.fanIn} files)`)}`));
71
- }
72
- rows.push(row('health', gradeColor(h.grade) + dim(' score ') + `${h.score}/100`));
73
-
74
- const notes = scanCaveats(repo.scan);
75
- const out = [panel(repo.name, rows)];
76
- if (notes.length) out.push(dim(wrapText(notes.join('\n'))));
77
- out.push(hintBar(['tour explains it', 'tree lists it', 'find searches it', 'help lists everything']));
78
- return out.join('\n');
49
+ const s = repo.scan.stats;
50
+ const h = repo.health;
51
+ const langs = repo.languages.slice(0, 4).map((l) => `${l.label} ${l.loc}`).join(', ')
52
+ + (repo.languages.length > 4 ? `, +${repo.languages.length - 4} more` : '');
53
+ const rows = [
54
+ row('folder', repo.root),
55
+ row('files', `${s.filesParsed} parsed` + (s.skipped ? dim(` (${s.skipped} skipped)`) : '') + ` · ${s.edgeCount} imports`),
56
+ row('languages', langs || '—'),
57
+ ];
58
+ if (repo.manifest.packageName)
59
+ rows.push(row('package', repo.manifest.packageName));
60
+ rows.push(row('license', repo.scan.license?.name || 'unknown'));
61
+ if (repo.facts.entries.length)
62
+ rows.push(row('start at', repo.facts.entries.slice(0, 3).join(', ')));
63
+ if (repo.facts.hubs.length) {
64
+ const top = repo.facts.hubs[0];
65
+ rows.push(row('top hub', `${top.path} ${dim(`(${top.fanIn} files)`)}`));
66
+ }
67
+ rows.push(row('health', gradeColor(h.grade) + dim(' score ') + `${h.score}/100`));
68
+ const notes = scanCaveats(repo.scan);
69
+ const out = [panel(repo.name, rows)];
70
+ if (notes.length)
71
+ out.push(dim(wrapText(notes.join('\n'))));
72
+ out.push(hintBar(['tour explains it', 'tree lists it', 'find searches it', 'help lists everything']));
73
+ return out.join('\n');
79
74
  }
80
-
81
75
  function gradeColor(grade) {
82
- if (grade === 'A' || grade === 'B') return ok(grade);
83
- if (grade === 'C') return warn(grade);
84
- return bad(grade);
76
+ if (grade === 'A' || grade === 'B')
77
+ return ok(grade);
78
+ if (grade === 'C')
79
+ return warn(grade);
80
+ return bad(grade);
85
81
  }
86
-
87
82
  // ------------------------------------------------------------------ tree ---
88
-
89
83
  // The file tree. Directories first, then files, each marked with the role that
90
84
  // makes it worth noticing — an entry point or a hub is the two things a new
91
85
  // reader is looking for, and marking them here saves a separate `hubs` command
92
86
  // on every repo.
93
87
  export function tree(repo, { sub = '', depth = 2, limit = MAX } = {}) {
94
- const files = repo.scan.files;
95
- const prefix = String(sub || '').replace(/^\/+|\/+$/g, '');
96
- const scoped = prefix ? files.filter((f) => f.path.startsWith(prefix + '/')) : files;
97
- if (!scoped.length) return dim(` Nothing under ${prefix || 'the root'}.`);
98
-
99
- const root = { name: prefix || repo.name, path: prefix, dirs: new Map(), files: [] };
100
- for (const f of scoped) {
101
- const rel = prefix ? f.path.slice(prefix.length + 1) : f.path;
102
- const parts = rel.split('/');
103
- let node = root;
104
- for (let i = 0; i < parts.length - 1; i++) {
105
- const name = parts[i];
106
- if (!node.dirs.has(name)) {
107
- node.dirs.set(name, {
108
- name,
109
- path: node.path ? `${node.path}/${name}` : name,
110
- dirs: new Map(),
111
- files: [],
112
- });
113
- }
114
- node = node.dirs.get(name);
115
- }
116
- node.files.push(f);
117
- }
118
-
119
- const lines = [];
120
- let budget = Math.min(Number(limit) || MAX, MAX);
121
- const walk = (node, depthLeft, indent) => {
122
- for (const child of [...node.dirs.values()].sort((a, b) => a.name.localeCompare(b.name))) {
123
- if (budget <= 0) return;
124
- const room = Math.max(4, termWidth() - indent.length - 2 - 4);
125
- lines.push(labelled(dim(GLYPHS.dir + ' '), child.name, dim(` ${countUnder(child)}`), { pad: indent.length }));
126
- budget--;
127
- if (depthLeft > 1) walk(child, depthLeft - 1, indent + ' ');
88
+ const files = repo.scan.files;
89
+ const prefix = String(sub || '').replace(/^\/+|\/+$/g, '');
90
+ const scoped = prefix ? files.filter((f) => f.path.startsWith(prefix + '/')) : files;
91
+ if (!scoped.length)
92
+ return dim(` Nothing under ${prefix || 'the root'}.`);
93
+ const root = { name: prefix || repo.name, path: prefix, dirs: new Map(), files: [] };
94
+ for (const f of scoped) {
95
+ const rel = prefix ? f.path.slice(prefix.length + 1) : f.path;
96
+ const parts = rel.split('/');
97
+ let node = root;
98
+ for (let i = 0; i < parts.length - 1; i++) {
99
+ const name = parts[i];
100
+ if (!node.dirs.has(name)) {
101
+ node.dirs.set(name, {
102
+ name,
103
+ path: node.path ? `${node.path}/${name}` : name,
104
+ dirs: new Map(),
105
+ files: [],
106
+ });
107
+ }
108
+ node = node.dirs.get(name);
109
+ }
110
+ node.files.push(f);
128
111
  }
129
- for (const f of node.files) {
130
- if (budget <= 0) return;
131
- lines.push(fileLine(repo, f, indent.length));
132
- budget--;
133
- }
134
- };
135
- walk(root, Math.max(1, Number(depth) || 2), ' ');
136
-
137
- const out = [dim(fit(` tree · ${prefix || '.'} ${scoped.length} files, depth ${Math.max(1, Number(depth) || 2)}`, Math.max(4, termWidth() - 2))), lines.join('\n')];
138
- if (budget <= 0) out.push(dim(wrapText('… list stopped early — tree <folder> <depth> to go deeper', ' ')));
139
- return out.join('\n');
112
+ const lines = [];
113
+ let budget = Math.min(Number(limit) || MAX, MAX);
114
+ const walk = (node, depthLeft, indent) => {
115
+ for (const child of [...node.dirs.values()].sort((a, b) => a.name.localeCompare(b.name))) {
116
+ if (budget <= 0)
117
+ return;
118
+ const room = Math.max(4, termWidth() - indent.length - 2 - 4);
119
+ lines.push(labelled(dim(GLYPHS.dir + ' '), child.name, dim(` ${countUnder(child)}`), { pad: indent.length }));
120
+ budget--;
121
+ if (depthLeft > 1)
122
+ walk(child, depthLeft - 1, indent + ' ');
123
+ }
124
+ for (const f of node.files) {
125
+ if (budget <= 0)
126
+ return;
127
+ lines.push(fileLine(repo, f, indent.length));
128
+ budget--;
129
+ }
130
+ };
131
+ walk(root, Math.max(1, Number(depth) || 2), ' ');
132
+ const out = [dim(fit(` tree · ${prefix || '.'} ${scoped.length} files, depth ${Math.max(1, Number(depth) || 2)}`, Math.max(4, termWidth() - 2))), lines.join('\n')];
133
+ if (budget <= 0)
134
+ out.push(dim(wrapText('… list stopped early — tree <folder> <depth> to go deeper', ' ')));
135
+ return out.join('\n');
140
136
  }
141
-
142
137
  function fileLine(repo, f, indent = 2) {
143
- const role = fileFacts(repo, f).role;
144
- const badge = role === 'entry' ? ok(GLYPHS.entry) : role === 'hub' ? warn(GLYPHS.hub) : dim(GLYPHS.file);
145
- // The name is the identifying part; the extension and line count are the first
146
- // things to go when there is no room. `tree` on a phone-width terminal should
147
- // still list the files, just less verbosely.
148
- const tail = ' ' + dim(String(f.ext || '').padEnd(6)) + dim(String(f.loc || 0).padStart(5));
149
- // The depth indent is part of the budget — it used to be prepended by the
150
- // caller, which made every nested row as many cells too wide as its depth.
151
- return labelled(badge + ' ', f.name, tail, { pad: indent });
138
+ const role = fileFacts(repo, f).role;
139
+ const badge = role === 'entry' ? ok(GLYPHS.entry) : role === 'hub' ? warn(GLYPHS.hub) : dim(GLYPHS.file);
140
+ // The name is the identifying part; the extension and line count are the first
141
+ // things to go when there is no room. `tree` on a phone-width terminal should
142
+ // still list the files, just less verbosely.
143
+ const tail = ' ' + dim(String(f.ext || '').padEnd(6)) + dim(String(f.loc || 0).padStart(5));
144
+ // The depth indent is part of the budget — it used to be prepended by the
145
+ // caller, which made every nested row as many cells too wide as its depth.
146
+ return labelled(badge + ' ', f.name, tail, { pad: indent });
152
147
  }
153
-
154
148
  function countUnder(node) {
155
- let n = node.files.length;
156
- for (const d of node.dirs.values()) n += countUnder(d);
157
- return n;
149
+ let n = node.files.length;
150
+ for (const d of node.dirs.values())
151
+ n += countUnder(d);
152
+ return n;
158
153
  }
159
-
160
154
  // ------------------------------------------------------------------ find ---
161
-
162
155
  // Search, using the site's query language. The result rows carry the same three
163
156
  // things the site's palette shows — path, line, snippet — fitted to the
164
157
  // terminal, with the path keeping the room it needs because it is what
165
158
  // identifies the hit.
166
159
  export function find(repo, { query = '', limit = 12 } = {}) {
167
- if (!String(query).trim()) {
168
- return dim(wrapText('Try: find resolveImport · find ext:rs -test · find "exact phrase" · find /regex/', ' '));
169
- }
170
- const res = searchRepo(repo, query, { limit: Math.min(Number(limit) || 12, MAX) });
171
- if (res.error) return bad(' ' + res.error);
172
- if (!res.results.length) return dim(wrapText(`No match for "${query}" in ${res.indexed} indexed files.`, ' '));
173
-
174
- const w = termWidth();
175
- // Path, line number and snippet share the terminal. The snippet is the least
176
- // identifying part, so it goes first on a narrow terminal; `labelled` owns the
177
- // indent, which is why the row cannot be `indent + name + count` glued
178
- // together and then be a few cells too wide.
179
- const lineRoom = 5;
180
- const lines = res.results.map((r) => {
181
- const snippetRoom = w - 2 - lineRoom - 2;
182
- const snip = snippetRoom > 10 ? dim(' ' + fit(r.snippet, snippetRoom, { tail: false })) : '';
183
- return labelled('', r.path, dim(String(r.line).padStart(lineRoom)) + snip, { pad: 2 });
184
- });
185
- const kind = res.advanced ? 'filtered query' : 'query';
186
- const head = dim(fit(` ${res.total} match${res.total === 1 ? '' : 'es'} (${kind}) across ${res.indexed} indexed files`, Math.max(1, w - 2)));
187
- return [head, ...lines, hintBar(['show <path> to read one', 'deps <path> to trace it'])].join('\n');
160
+ if (!String(query).trim()) {
161
+ return dim(wrapText('Try: find resolveImport · find ext:rs -test · find "exact phrase" · find /regex/', ' '));
162
+ }
163
+ const res = searchRepo(repo, query, { limit: Math.min(Number(limit) || 12, MAX) });
164
+ if (res.error)
165
+ return bad(' ' + res.error);
166
+ if (!res.results.length)
167
+ return dim(wrapText(`No match for "${query}" in ${res.indexed} indexed files.`, ' '));
168
+ const w = termWidth();
169
+ // Path, line number and snippet share the terminal. The snippet is the least
170
+ // identifying part, so it goes first on a narrow terminal; `labelled` owns the
171
+ // indent, which is why the row cannot be `indent + name + count` glued
172
+ // together and then be a few cells too wide.
173
+ const lineRoom = 5;
174
+ const lines = res.results.map((r) => {
175
+ const snippetRoom = w - 2 - lineRoom - 2;
176
+ const snip = snippetRoom > 10 ? dim(' ' + fit(r.snippet, snippetRoom, { tail: false })) : '';
177
+ return labelled('', r.path, dim(String(r.line).padStart(lineRoom)) + snip, { pad: 2 });
178
+ });
179
+ const kind = res.advanced ? 'filtered query' : 'query';
180
+ const head = dim(fit(` ${res.total} match${res.total === 1 ? '' : 'es'} (${kind}) across ${res.indexed} indexed files`, Math.max(1, w - 2)));
181
+ return [head, ...lines, hintBar(['show <path> to read one', 'deps <path> to trace it'])].join('\n');
188
182
  }
189
-
190
183
  // ------------------------------------------------------------------ show ---
191
-
192
184
  // Read a file in the terminal. This is the one view that cannot be pure — it
193
185
  // touches the disk — so it is the one async view, and it goes through the same
194
186
  // containment check the HTTP route uses rather than reaching for a path itself.
195
187
  export async function show(repo, { target = '', from = 0, count = 0 } = {}) {
196
- const found = resolveTarget(repo, target);
197
- if (found.error) return targetError(found);
198
- if (found.folder) return dim(` ${found.folder.path} is a folder — tree ${found.folder.path} 2, or explain ${found.folder.path}`);
199
-
200
- const file = found.file;
201
- const text = await readRepoFile(repo, file.path);
202
- const lines = text.split('\n');
203
- // `null` here means the argument was present but not a usable line number
204
- // (`show f 1.5`), which the command layer turns into a message rather than a
205
- // silent fallback. A string means a caller passed a bad value, so say so
206
- // instead of coercing it into a nonsensical range.
207
- if (from === null || count === null) {
208
- return bad(wrapText('Line numbers must be whole numbers.', ' ')) + dim(fit(' Try: `show ' + file.name + ' 40 20`', Math.max(1, termWidth() - 2)));
209
- }
210
- const start = Math.max(1, from || 1);
211
- const room = count > 0 ? Math.min(count, 400) : Math.min(lines.length, 40);
212
- const end = Math.min(lines.length, start + room - 1);
213
-
214
- const gutter = String(end).length;
215
- const body = lines.slice(start - 1, end).map((line, i) => {
216
- const n = String(start + i).padStart(gutter);
217
- return dim(n + ' ') + fit(line.replace(/\t/g, ' '), Math.max(20, termWidth() - gutter - 3), { tail: false });
218
- });
219
-
220
- const f = fileFacts(repo, file);
221
- const head = panel(file.path, [
222
- { label: 'role', value: f.role },
223
- { label: 'size', value: `${f.loc} loc` + (f.complexity ? ` · complexity ${f.complexity}` : '') },
224
- { label: 'graph', value: `${f.fanIn} in ${f.fanOut} out` + (f.inCycle ? ' ' + bad('in a cycle') : '') },
225
- ]);
226
- const shown = `${start}–${end} of ${lines.length}`;
227
- return [head, dim(` ${shown}`), ...body, hintBar([`deps ${file.name} for connections`, `explain ${file.name} in prose`])].join('\n');
188
+ const found = resolveTarget(repo, target);
189
+ if (found.error)
190
+ return targetError(found);
191
+ if (found.folder)
192
+ return dim(` ${found.folder.path} is a folder — tree ${found.folder.path} 2, or explain ${found.folder.path}`);
193
+ const file = found.file;
194
+ const text = await readRepoFile(repo, file.path);
195
+ const lines = text.split('\n');
196
+ // `null` here means the argument was present but not a usable line number
197
+ // (`show f 1.5`), which the command layer turns into a message rather than a
198
+ // silent fallback. A string means a caller passed a bad value, so say so
199
+ // instead of coercing it into a nonsensical range.
200
+ if (from === null || count === null) {
201
+ return bad(wrapText('Line numbers must be whole numbers.', ' ')) + dim(fit(' Try: `show ' + file.name + ' 40 20`', Math.max(1, termWidth() - 2)));
202
+ }
203
+ const start = Math.max(1, from || 1);
204
+ const room = count > 0 ? Math.min(count, 400) : Math.min(lines.length, 40);
205
+ const end = Math.min(lines.length, start + room - 1);
206
+ const gutter = String(end).length;
207
+ const body = lines.slice(start - 1, end).map((line, i) => {
208
+ const n = String(start + i).padStart(gutter);
209
+ return dim(n + ' ') + fit(line.replace(/\t/g, ' '), Math.max(20, termWidth() - gutter - 3), { tail: false });
210
+ });
211
+ const f = fileFacts(repo, file);
212
+ const head = panel(file.path, [
213
+ { label: 'role', value: f.role },
214
+ { label: 'size', value: `${f.loc} loc` + (f.complexity ? ` · complexity ${f.complexity}` : '') },
215
+ { label: 'graph', value: `${f.fanIn} in ${f.fanOut} out` + (f.inCycle ? ' ' + bad('in a cycle') : '') },
216
+ ]);
217
+ const shown = `${start}–${end} of ${lines.length}`;
218
+ return [head, dim(` ${shown}`), ...body, hintBar([`deps ${file.name} for connections`, `explain ${file.name} in prose`])].join('\n');
228
219
  }
229
-
230
220
  // Render a resolver miss the same way everywhere: the reason, then the
231
221
  // candidates. A "did you mean" is only useful if it is actionable.
232
222
  //
@@ -234,365 +224,351 @@ export async function show(repo, { target = '', from = 0, count = 0 } = {}) {
234
224
  // filename produces a long sentence, and the commands that surface this
235
225
  // (symbols, blast, graph, blame) do not each remember to wrap it.
236
226
  export function targetError(found) {
237
- // The room is the terminal minus the 2-space lead. The `Math.max(8, …)` that
238
- // used to be here was itself the bug on an 8-column terminal: a floor above the
239
- // available width guarantees an overflow, which is the one thing this function
240
- // exists to prevent. `wrapText` already clamps to a single cell.
241
- const w = termWidth();
242
- const room = Math.max(1, w - 6);
243
- const lines = [bad(' ' + wrapText(found.error, ' ', room))];
244
- if (found.candidates) {
245
- for (const c of found.candidates) lines.push(' ' + cyan(fit(c, Math.max(1, w - 4))));
246
- lines.push(dim(fit(' show <one of these>', Math.max(1, w - 2))));
247
- }
248
- return lines.join('\n');
227
+ // The room is the terminal minus the 2-space lead. The `Math.max(8, …)` that
228
+ // used to be here was itself the bug on an 8-column terminal: a floor above the
229
+ // available width guarantees an overflow, which is the one thing this function
230
+ // exists to prevent. `wrapText` already clamps to a single cell.
231
+ const w = termWidth();
232
+ const room = Math.max(1, w - 6);
233
+ const lines = [bad(' ' + wrapText(found.error, ' ', room))];
234
+ if (found.candidates) {
235
+ for (const c of found.candidates)
236
+ lines.push(' ' + cyan(fit(c, Math.max(1, w - 4))));
237
+ lines.push(dim(fit(' show <one of these>', Math.max(1, w - 2))));
238
+ }
239
+ return lines.join('\n');
249
240
  }
250
-
251
241
  // ------------------------------------------------------------------ deps ---
252
-
253
242
  // What a file connects to, in both directions, plus the verdict the graph has
254
243
  // on it. The two lists are the point: "what does this pull in" and "what breaks
255
244
  // if this breaks" are different questions and a single edge count answers
256
245
  // neither.
257
246
  export function deps(repo, { target = '', limit = 15 } = {}) {
258
- const found = resolveTarget(repo, target);
259
- if (found.error) return targetError(found);
260
- if (found.folder) return dim(` ${found.folder.path} is a folder — try a file inside it.`);
261
-
262
- const p = found.file.path;
263
- const imports = (repo.facts.importsOf[p] || []).slice(0, limit);
264
- const importers = (repo.facts.importers[p] || []).slice(0, limit);
265
- const f = fileFacts(repo, found.file);
266
- const w = termWidth();
267
-
268
- const head = [panel(found.file.path, [
269
- { label: 'role', value: f.role },
270
- { label: 'dependents', value: `${f.fanIn} file${f.fanIn === 1 ? '' : 's'} import this` },
271
- { label: 'imports', value: `${f.fanOut} file${f.fanOut === 1 ? '' : 's'} pulled in` },
272
- ])];
273
- if (f.inCycle) head.push(bad(fit(' ' + GLYPHS.warn + ' part of a circular import — refactoring here ripples', Math.max(1, w - 2))));
274
-
275
- const list = (title, items) => {
276
- if (!items.length) return dim(fit(` ${title}: none`, Math.max(1, w - 2)));
277
- return [dim(fit(` ${title}:`, Math.max(1, w - 2))), ...items.map((x) => ' ' + cyan(fit(x, Math.max(1, w - 4))))].join('\n');
278
- };
279
- return [...head, list('imports', imports), list('imported by', importers)].join('\n');
247
+ const found = resolveTarget(repo, target);
248
+ if (found.error)
249
+ return targetError(found);
250
+ if (found.folder)
251
+ return dim(` ${found.folder.path} is a folder — try a file inside it.`);
252
+ const p = found.file.path;
253
+ const imports = (repo.facts.importsOf[p] || []).slice(0, limit);
254
+ const importers = (repo.facts.importers[p] || []).slice(0, limit);
255
+ const f = fileFacts(repo, found.file);
256
+ const w = termWidth();
257
+ const head = [panel(found.file.path, [
258
+ { label: 'role', value: f.role },
259
+ { label: 'dependents', value: `${f.fanIn} file${f.fanIn === 1 ? '' : 's'} import this` },
260
+ { label: 'imports', value: `${f.fanOut} file${f.fanOut === 1 ? '' : 's'} pulled in` },
261
+ ])];
262
+ if (f.inCycle)
263
+ head.push(bad(fit(' ' + GLYPHS.warn + ' part of a circular import — refactoring here ripples', Math.max(1, w - 2))));
264
+ const list = (title, items) => {
265
+ if (!items.length)
266
+ return dim(fit(` ${title}: none`, Math.max(1, w - 2)));
267
+ return [dim(fit(` ${title}:`, Math.max(1, w - 2))), ...items.map((x) => ' ' + cyan(fit(x, Math.max(1, w - 4))))].join('\n');
268
+ };
269
+ return [...head, list('imports', imports), list('imported by', importers)].join('\n');
280
270
  }
281
-
282
271
  // --------------------------------------------------------------- explain ---
283
-
284
272
  // The prose explanation, straight from `explainLocal.js` — the same notes the
285
273
  // site shows when no AI key is configured, with no key required and nothing
286
274
  // sent anywhere. Markdown backticks are the one bit of markup kept, turned into
287
275
  // terminal emphasis.
288
276
  export function explain(repo, { target = '' } = {}) {
289
- if (!String(target).trim()) return renderProse(explainRepoOverview(repo));
290
- const found = resolveTarget(repo, target);
291
- if (found.error) return targetError(found);
292
- if (found.folder) return renderProse(explainRepoFolder(repo, found.folder));
293
- return renderProse(explainRepoFile(repo, found.file));
277
+ if (!String(target).trim())
278
+ return renderProse(explainRepoOverview(repo));
279
+ const found = resolveTarget(repo, target);
280
+ if (found.error)
281
+ return targetError(found);
282
+ if (found.folder)
283
+ return renderProse(explainRepoFolder(repo, found.folder));
284
+ return renderProse(explainRepoFile(repo, found.file));
294
285
  }
295
-
296
286
  // `**bold**` and `` `code` `` are the only two marks `explainLocal` emits. They
297
287
  // become terminal styling; everything else passes through untouched, so the
298
288
  // prose still reads correctly with color off. Wrapping happens on the plain
299
289
  // text first, then the marks are styled in place — styling after wrapping keeps
300
290
  // the wrap from having to measure escape sequences.
301
291
  function renderProse(md) {
302
- return String(md)
303
- .split('\n\n')
304
- .map((para) => {
305
- const styled = para
306
- .replace(/`([^`]+)`/g, (_, c) => '\u0000' + c + '\u0000')
307
- .replace(/\*\*([^*]+)\*\*/g, (_, c) => '\u0001' + c + '\u0001');
308
- return wrapText(styled)
309
- .replace(/\u0000([^\u0000]+)\u0000/g, (_, c) => cyan(c))
310
- .replace(/\u0001([^\u0001]+)\u0001/g, (_, c) => bold(c));
292
+ return String(md)
293
+ .split('\n\n')
294
+ .map((para) => {
295
+ const styled = para
296
+ .replace(/`([^`]+)`/g, (_, c) => '\u0000' + c + '\u0000')
297
+ .replace(/\*\*([^*]+)\*\*/g, (_, c) => '\u0001' + c + '\u0001');
298
+ return wrapText(styled)
299
+ .replace(/\u0000([^\u0000]+)\u0000/g, (_, c) => cyan(c))
300
+ .replace(/\u0001([^\u0001]+)\u0001/g, (_, c) => bold(c));
311
301
  })
312
- .join('\n\n');
302
+ .join('\n\n');
313
303
  }
314
-
315
304
  // ------------------------------------------------------------------ tour ---
316
-
317
305
  // The guided read. The same eight stops the site and the MCP server hand out,
318
306
  // in the same order, with the same one-line reason for each — a person at the
319
307
  // prompt and an agent asking the MCP server are being told the same thing about
320
308
  // the same repository.
321
309
  export function tour(repo) {
322
- if (!repo.tour.length) return dim(wrapText('No tour stops found — is this a code repository?', ' '));
323
- const lines = repo.tour.map((stop, i) => {
324
- // The number is the marker and the path is the value, so the tour stops use
325
- // the same helper as every other ranked list and cannot outgrow the terminal.
326
- const head = labelled(dim(String(i + 1).padStart(2) + '. '), stop.path, '', { pad: 2 });
327
- return [head, dim(wrapText(stop.why, ' '))].join('\n');
328
- });
329
- return [dim(fit(' reading order', Math.max(4, termWidth() - 2))), ...lines, hintBar(['show <path> to read one'])].join('\n');
310
+ if (!repo.tour.length)
311
+ return dim(wrapText('No tour stops found — is this a code repository?', ' '));
312
+ const lines = repo.tour.map((stop, i) => {
313
+ // The number is the marker and the path is the value, so the tour stops use
314
+ // the same helper as every other ranked list and cannot outgrow the terminal.
315
+ const head = labelled(dim(String(i + 1).padStart(2) + '. '), stop.path, '', { pad: 2 });
316
+ return [head, dim(wrapText(stop.why, ' '))].join('\n');
317
+ });
318
+ return [dim(fit(' reading order', Math.max(4, termWidth() - 2))), ...lines, hintBar(['show <path> to read one'])].join('\n');
330
319
  }
331
-
332
320
  // ---------------------------------------------------------------- health ---
333
-
334
321
  // The health report. The grade leads because that is the question; the
335
322
  // breakdown follows because a letter with no reasons is not actionable.
336
323
  export function health(repo) {
337
- const h = repo.health;
338
- const w = termWidth();
339
- const rows = [
340
- { label: 'grade', value: gradeColor(h.grade) + dim(` score ${h.score}/100`) },
341
- { label: 'debt', value: `${h.totals.crit} critical · ${h.totals.high} high findings` },
342
- { label: 'complexity', value: `avg ${h.totals.avgCx} per file` },
343
- { label: 'orphans', value: `${Math.round(h.totals.orphanRatio * 100)}% of files import nothing` },
344
- { label: 'cycles', value: String(h.totals.cycles) },
345
- ];
346
- if (h.effortHours) rows.push({ label: 'effort', value: `~${h.effortHours}h to address (${h.debtCategory})` });
347
-
348
- const risky = [...(h.perFile || [])]
349
- .filter((f) => (f.risk || 0) > 0)
350
- .sort((a, b) => b.risk - a.risk)
351
- .slice(0, 8);
352
- const out = [panel('health', rows)];
353
- if (risky.length) {
354
- out.push(dim(fit(' riskiest files:', Math.max(4, termWidth() - 2))));
355
- for (const f of risky) {
356
- // Risk is an integer 0–100; printing "58.00" would imply a precision the
357
- // number does not have. The suffix is dropped on a narrow terminal rather
358
- // than allowed to push the path off the edge.
359
- const risk = Number(f.risk);
360
- const tail = dim(` risk ${risk}${f.blast !== undefined ? ` blast ${f.blast}` : ''}`);
361
- out.push(labelled(warn(GLYPHS.warn + ' '), cyan(f.path), tail, { pad: 4 }));
324
+ const h = repo.health;
325
+ const w = termWidth();
326
+ const rows = [
327
+ { label: 'grade', value: gradeColor(h.grade) + dim(` score ${h.score}/100`) },
328
+ { label: 'debt', value: `${h.totals.crit} critical · ${h.totals.high} high findings` },
329
+ { label: 'complexity', value: `avg ${h.totals.avgCx} per file` },
330
+ { label: 'orphans', value: `${Math.round(h.totals.orphanRatio * 100)}% of files import nothing` },
331
+ { label: 'cycles', value: String(h.totals.cycles) },
332
+ ];
333
+ if (h.effortHours)
334
+ rows.push({ label: 'effort', value: `~${h.effortHours}h to address (${h.debtCategory})` });
335
+ const risky = [...(h.perFile || [])]
336
+ .filter((f) => (f.risk || 0) > 0)
337
+ .sort((a, b) => b.risk - a.risk)
338
+ .slice(0, 8);
339
+ const out = [panel('health', rows)];
340
+ if (risky.length) {
341
+ out.push(dim(fit(' riskiest files:', Math.max(4, termWidth() - 2))));
342
+ for (const f of risky) {
343
+ // Risk is an integer 0–100; printing "58.00" would imply a precision the
344
+ // number does not have. The suffix is dropped on a narrow terminal rather
345
+ // than allowed to push the path off the edge.
346
+ const risk = Number(f.risk);
347
+ const tail = dim(` risk ${risk}${f.blast !== undefined ? ` blast ${f.blast}` : ''}`);
348
+ out.push(labelled(warn(GLYPHS.warn + ' '), cyan(f.path), tail, { pad: 4 }));
349
+ }
362
350
  }
363
- }
364
- if (h.breakdown?.length) {
365
- // The breakdown is the score itemized — each entry is what a factor cost.
366
- // Showing the label without the points would explain the grade without
367
- // showing the arithmetic, which is the part that makes it actionable.
368
- out.push(dim(fit(' what moved the score:', Math.max(4, termWidth() - 2))));
369
- for (const b of h.breakdown) {
370
- out.push(labelled(dim('· '), b.label, bad(String(b.points)), { pad: 4 }));
351
+ if (h.breakdown?.length) {
352
+ // The breakdown is the score itemized — each entry is what a factor cost.
353
+ // Showing the label without the points would explain the grade without
354
+ // showing the arithmetic, which is the part that makes it actionable.
355
+ out.push(dim(fit(' what moved the score:', Math.max(4, termWidth() - 2))));
356
+ for (const b of h.breakdown) {
357
+ out.push(labelled(dim('· '), b.label, bad(String(b.points)), { pad: 4 }));
358
+ }
371
359
  }
372
- }
373
- return out.join('\n');
360
+ return out.join('\n');
374
361
  }
375
-
376
362
  // ------------------------------------------------------------------ hubs ---
377
-
378
363
  // The load-bearing files: the ones everything else reaches for. Ranked, because
379
364
  // "which file is load-bearing" only has an answer as an ordering.
380
365
  export function hubs(repo, { limit = 12 } = {}) {
381
- const list = repo.facts.hubs.slice(0, Math.min(Number(limit) || 12, MAX));
382
- if (!list.length) return dim(wrapText('No hubs — nothing here is imported by two or more files.', ' '));
383
- const w = termWidth();
384
- const top = list[0].fanIn || 1;
385
- // The bar is decoration and is the tail, so it is the first thing dropped on a
386
- // narrow terminal. The in/out counts are the reason the file is on the list.
387
- const barRoom = Math.max(0, Math.min(24, w - 30));
388
- return [dim(fit(' most depended-on files', Math.max(4, w - 2))), ...list.map((h) => {
389
- const bar = barRoom > 4 ? dim(' ' + '█'.repeat(Math.max(1, Math.round((h.fanIn / top) * barRoom)))) : '';
390
- return labelled('', h.path, dim(` ${h.fanIn} in / ${h.fanOut} out`) + bar, { pad: 2 });
391
- })].join('\n');
366
+ const list = repo.facts.hubs.slice(0, Math.min(Number(limit) || 12, MAX));
367
+ if (!list.length)
368
+ return dim(wrapText('No hubs — nothing here is imported by two or more files.', ' '));
369
+ const w = termWidth();
370
+ const top = list[0].fanIn || 1;
371
+ // The bar is decoration and is the tail, so it is the first thing dropped on a
372
+ // narrow terminal. The in/out counts are the reason the file is on the list.
373
+ const barRoom = Math.max(0, Math.min(24, w - 30));
374
+ return [dim(fit(' most depended-on files', Math.max(4, w - 2))), ...list.map((h) => {
375
+ const bar = barRoom > 4 ? dim(' ' + '█'.repeat(Math.max(1, Math.round((h.fanIn / top) * barRoom)))) : '';
376
+ return labelled('', h.path, dim(` ${h.fanIn} in / ${h.fanOut} out`) + bar, { pad: 2 });
377
+ })].join('\n');
392
378
  }
393
-
394
379
  // ---------------------------------------------------------------- layers ---
395
-
396
380
  // Import depth, shallowest first. This is the architecture as a staircase:
397
381
  // layer 0 is what runs, and each step down is something it can reach.
398
382
  export function layers(repo, { limit = 12 } = {}) {
399
- const ls = repo.layers.layers;
400
- const w = termWidth();
401
- if (!ls.length) return dim(wrapText('No layers — no import chain starts anywhere we recognize.'));
402
- const out = [dim(fit(' import depth', Math.max(4, termWidth() - 2)))];
403
- ls.slice(0, Math.min(Number(limit) || 12, 40)).forEach((files, i) => {
404
- // The "+N more" note is the tail, so a terminal that cannot hold the path
405
- // *and* the note drops the note rather than overflowing. Same rule as every
406
- // other labelled row, which is why it goes through the same helper.
407
- const more = files.length > 3 ? dim(` +${files.length - 3} more`) : '';
408
- out.push(labelled(dim(` layer ${i} (${files.length}) `), cyan(files.slice(0, 3).join(', ')), more));
409
- });
410
- if (repo.layers.unreachable.length) {
411
- out.push(dim(wrapText(`${repo.layers.unreachable.length} files are not reachable from any entry point`)));
412
- }
413
- return out.join('\n');
383
+ const ls = repo.layers.layers;
384
+ const w = termWidth();
385
+ if (!ls.length)
386
+ return dim(wrapText('No layers — no import chain starts anywhere we recognize.'));
387
+ const out = [dim(fit(' import depth', Math.max(4, termWidth() - 2)))];
388
+ ls.slice(0, Math.min(Number(limit) || 12, 40)).forEach((files, i) => {
389
+ // The "+N more" note is the tail, so a terminal that cannot hold the path
390
+ // *and* the note drops the note rather than overflowing. Same rule as every
391
+ // other labelled row, which is why it goes through the same helper.
392
+ const more = files.length > 3 ? dim(` +${files.length - 3} more`) : '';
393
+ out.push(labelled(dim(` layer ${i} (${files.length}) `), cyan(files.slice(0, 3).join(', ')), more));
394
+ });
395
+ if (repo.layers.unreachable.length) {
396
+ out.push(dim(wrapText(`${repo.layers.unreachable.length} files are not reachable from any entry point`)));
397
+ }
398
+ return out.join('\n');
414
399
  }
415
-
416
400
  // -------------------------------------------------------------- patterns ---
417
-
418
401
  // The architecture, in prose. These are the observations `detectPatterns`
419
402
  // makes, in the order a senior dev would mention them.
420
403
  export function patterns(repo) {
421
- if (!repo.patterns.length) return dim(wrapText('No patterns stood out.', ' '));
422
- const out = [dim(fit(' what this codebase looks like', Math.max(4, termWidth() - 2)))];
423
- for (const p of repo.patterns) {
424
- const mark = p.tone === 'good' ? ok(GLYPHS.good) : p.tone === 'warn' ? bad(GLYPHS.warn) : warn('·');
425
- out.push(' ' + mark + ' ' + dim(fit(p.title, Math.max(4, termWidth() - 4))));
426
- out.push(dim(wrapText(p.detail, ' ')));
427
- if (p.paths?.length) {
428
- out.push(dim(' ' + fit(p.paths.slice(0, 4).join(' '), Math.max(4, termWidth() - 4))));
404
+ if (!repo.patterns.length)
405
+ return dim(wrapText('No patterns stood out.', ' '));
406
+ const out = [dim(fit(' what this codebase looks like', Math.max(4, termWidth() - 2)))];
407
+ for (const p of repo.patterns) {
408
+ const mark = p.tone === 'good' ? ok(GLYPHS.good) : p.tone === 'warn' ? bad(GLYPHS.warn) : warn('·');
409
+ out.push(' ' + mark + ' ' + dim(fit(p.title, Math.max(4, termWidth() - 4))));
410
+ out.push(dim(wrapText(p.detail, ' ')));
411
+ if (p.paths?.length) {
412
+ out.push(dim(' ' + fit(p.paths.slice(0, 4).join(' '), Math.max(4, termWidth() - 4))));
413
+ }
429
414
  }
430
- }
431
- return out.join('\n');
415
+ return out.join('\n');
432
416
  }
433
-
434
417
  // ----------------------------------------------------------------- stats ---
435
-
436
418
  // The numbers, grouped by the question each answers: what is it made of, where
437
419
  // is the bulk of it, and what is the heaviest thing in it.
438
420
  export function stats(repo, { limit = 10 } = {}) {
439
- const n = Math.min(Number(limit) || 10, MAX);
440
- const s = repo.scan.stats;
441
- const top = [...repo.scan.files].sort((a, b) => (b.loc || 0) - (a.loc || 0)).slice(0, n);
442
- const folders = [...repo.scan.folders].sort((a, b) => (b.loc || 0) - (a.loc || 0)).slice(0, n);
443
- const cx = [...repo.scan.files].filter((f) => f.complexity > 0).sort((a, b) => b.complexity - a.complexity).slice(0, 5);
444
-
445
- const totalLoc = repo.languages.reduce((s2, l) => s2 + l.loc, 0);
446
- // One label column for every ranked list, sized from the terminal rather than
447
- // hardcoded at 34. The old fixed padding meant a 20-column terminal got 45
448
- // columns of output — the number was right and the layout was still wrong.
449
- const w = termWidth();
450
- const labels = [
451
- ...repo.languages.slice(0, 8).map((l) => l.label),
452
- ...folders.map((d) => d.path),
453
- ...top.map((f) => f.path),
454
- ...cx.map((f) => f.path),
455
- ];
456
- const labelRoom = Math.max(4, Math.min(34, Math.max(0, ...labels.map((s) => s.length)) + 1, Math.floor(w * 0.45)));
457
- // The number keeps at least 4 cells and the label gets the rest, so the row is
458
- // `indent + label + number` and never exceeds the terminal at any width.
459
- const numRoom = Math.max(4, Math.min(7, w - labelRoom - 5));
460
- const statRow = (s, n) => labelled('', s, dim(String(n).padStart(numRoom)), { pad: 4 });
461
-
462
- const out = [panel('stats', [
463
- { label: 'files', value: `${s.filesParsed} parsed of ${s.filesTotal} seen` },
464
- { label: 'lines', value: `${totalLoc} code` + (s.truncated ? warn(' (partial scan)') : '') },
465
- { label: 'imports', value: `${s.edgeCount} resolved` + (s.imports?.total ? dim(` ${s.imports.confidence}% placed`) : '') },
466
- { label: 'tests', value: `${repo.facts.testCoverage.ratio}% of non-test files are covered by a test import` },
467
- ])];
468
-
469
- out.push(dim(fit(' languages', Math.max(4, w - 2))));
470
- for (const l of repo.languages.slice(0, 8)) out.push(statRow(l.label, l.loc));
471
-
472
- if (folders.length) {
473
- out.push(dim(fit(' biggest folders (loc)', Math.max(4, termWidth() - 2))));
474
- for (const d of folders) out.push(statRow(d.path, d.loc));
475
- }
476
- if (top.length) {
477
- out.push(dim(fit(' biggest files (loc)', Math.max(4, termWidth() - 2))));
478
- for (const f of top) out.push(statRow(f.path, f.loc));
479
- }
480
- if (cx.length) {
481
- out.push(dim(fit(' most complex', Math.max(4, w - 2))));
482
- for (const f of cx) out.push(statRow(f.path, f.complexity));
483
- }
484
- return out.join('\n');
421
+ const n = Math.min(Number(limit) || 10, MAX);
422
+ const s = repo.scan.stats;
423
+ const top = [...repo.scan.files].sort((a, b) => (b.loc || 0) - (a.loc || 0)).slice(0, n);
424
+ const folders = [...repo.scan.folders].sort((a, b) => (b.loc || 0) - (a.loc || 0)).slice(0, n);
425
+ const cx = [...repo.scan.files].filter((f) => f.complexity > 0).sort((a, b) => b.complexity - a.complexity).slice(0, 5);
426
+ const totalLoc = repo.languages.reduce((s2, l) => s2 + l.loc, 0);
427
+ // One label column for every ranked list, sized from the terminal rather than
428
+ // hardcoded at 34. The old fixed padding meant a 20-column terminal got 45
429
+ // columns of output — the number was right and the layout was still wrong.
430
+ const w = termWidth();
431
+ const labels = [
432
+ ...repo.languages.slice(0, 8).map((l) => l.label),
433
+ ...folders.map((d) => d.path),
434
+ ...top.map((f) => f.path),
435
+ ...cx.map((f) => f.path),
436
+ ];
437
+ const labelRoom = Math.max(4, Math.min(34, Math.max(0, ...labels.map((s) => s.length)) + 1, Math.floor(w * 0.45)));
438
+ // The number keeps at least 4 cells and the label gets the rest, so the row is
439
+ // `indent + label + number` and never exceeds the terminal at any width.
440
+ const numRoom = Math.max(4, Math.min(7, w - labelRoom - 5));
441
+ const statRow = (s, n) => labelled('', s, dim(String(n).padStart(numRoom)), { pad: 4 });
442
+ const out = [panel('stats', [
443
+ { label: 'files', value: `${s.filesParsed} parsed of ${s.filesTotal} seen` },
444
+ { label: 'lines', value: `${totalLoc} code` + (s.truncated ? warn(' (partial scan)') : '') },
445
+ { label: 'imports', value: `${s.edgeCount} resolved` + (s.imports?.total ? dim(` ${s.imports.confidence}% placed`) : '') },
446
+ { label: 'tests', value: `${repo.facts.testCoverage.ratio}% of non-test files reachable from test imports` },
447
+ ])];
448
+ out.push(dim(fit(' languages', Math.max(4, w - 2))));
449
+ for (const l of repo.languages.slice(0, 8))
450
+ out.push(statRow(l.label, l.loc));
451
+ if (folders.length) {
452
+ out.push(dim(fit(' biggest folders (loc)', Math.max(4, termWidth() - 2))));
453
+ for (const d of folders)
454
+ out.push(statRow(d.path, d.loc));
455
+ }
456
+ if (top.length) {
457
+ out.push(dim(fit(' biggest files (loc)', Math.max(4, termWidth() - 2))));
458
+ for (const f of top)
459
+ out.push(statRow(f.path, f.loc));
460
+ }
461
+ if (cx.length) {
462
+ out.push(dim(fit(' most complex', Math.max(4, w - 2))));
463
+ for (const f of cx)
464
+ out.push(statRow(f.path, f.complexity));
465
+ }
466
+ return out.join('\n');
485
467
  }
486
-
487
468
  // -------------------------------------------------------------- security ---
488
-
489
469
  export function security(repo, { limit = 12 } = {}) {
490
- const sec = repo.security;
491
- const w = termWidth();
492
- if (!sec.total) return ok(wrapText('No findings from the built-in rules.', ' '));
493
- const order = ['critical', 'high', 'medium', 'low', 'info'];
494
- const counts = order.filter((k) => sec.counts[k]).map((k) => `${sec.counts[k]} ${k}`).join(' · ');
495
- const out = [panel('security', [
496
- { label: 'grade', value: gradeColor(sec.grade) + dim(` score ${sec.score}/100`) },
497
- { label: 'findings', value: counts },
498
- ])];
499
- for (const f of sec.files.slice(0, Math.min(Number(limit) || 12, MAX))) {
500
- const mark = (f.worst === 'critical' || f.worst === 'high' ? bad : warn)(GLYPHS.warn + ' ');
501
- out.push(labelled(mark, cyan(f.path), dim(` ${f.count} · worst ${f.worst}`), { pad: 4 }));
502
- }
503
- out.push(dim(wrapText('Pattern-based heuristics only — not a substitute for a real audit.')));
504
- return out.join('\n');
470
+ const sec = repo.security;
471
+ const w = termWidth();
472
+ if (!sec.total)
473
+ return ok(wrapText('No findings from the built-in rules.', ' '));
474
+ const order = ['critical', 'high', 'medium', 'low', 'info'];
475
+ const counts = order.filter((k) => sec.counts[k]).map((k) => `${sec.counts[k]} ${k}`).join(' · ');
476
+ const out = [panel('security', [
477
+ { label: 'grade', value: gradeColor(sec.grade) + dim(` score ${sec.score}/100`) },
478
+ { label: 'findings', value: counts },
479
+ ])];
480
+ for (const f of sec.files.slice(0, Math.min(Number(limit) || 12, MAX))) {
481
+ const mark = (f.worst === 'critical' || f.worst === 'high' ? bad : warn)(GLYPHS.warn + ' ');
482
+ out.push(labelled(mark, cyan(f.path), dim(` ${f.count} · worst ${f.worst}`), { pad: 4 }));
483
+ }
484
+ out.push(dim(wrapText('Pattern-based heuristics only — not a substitute for a real audit.')));
485
+ return out.join('\n');
505
486
  }
506
-
507
487
  // ----------------------------------------------------------------- stack ---
508
-
509
488
  // The dependency picture the site shows, from the same `analyzeStack`. Columns
510
489
  // are computed from the widest *actual* value rather than fixed padding, so a
511
490
  // long scoped package name shrinks the columns instead of pushing the row off
512
491
  // the right edge.
513
492
  export function stack(repo, { limit = 20 } = {}) {
514
- const st = repo.stack;
515
- const w = termWidth();
516
- if (!st.items.length) return dim(wrapText('No recognized dependencies (no package manifest found?).', ' '));
517
- const items = st.items.slice(0, Math.min(Number(limit) || 20, MAX));
518
-
519
- const out = [dim(fit(' stack ' + (st.pm.join(', ') || 'no package manager'), Math.max(1, w - 2)))];
520
- for (const it of items) {
521
- // Same shape as every other row: the name is the value, the category and
522
- // version are the tail, and the tail is what a narrow terminal drops. The
523
- // old version computed its own column budget and was three cells wide.
524
- const tail = dim(it.category) + (it.version ? ' ' + it.version : '') + (it.dev ? ' dev' : '');
525
- out.push(labelled('', it.name, tail, { pad: 4 }));
526
- }
527
- return out.join('\n');
493
+ const st = repo.stack;
494
+ const w = termWidth();
495
+ if (!st.items.length)
496
+ return dim(wrapText('No recognized dependencies (no package manifest found?).', ' '));
497
+ const items = st.items.slice(0, Math.min(Number(limit) || 20, MAX));
498
+ const out = [dim(fit(' stack ' + (st.pm.join(', ') || 'no package manager'), Math.max(1, w - 2)))];
499
+ for (const it of items) {
500
+ // Same shape as every other row: the name is the value, the category and
501
+ // version are the tail, and the tail is what a narrow terminal drops. The
502
+ // old version computed its own column budget and was three cells wide.
503
+ const tail = dim(it.category) + (it.version ? ' ' + it.version : '') + (it.dev ? ' dev' : '');
504
+ out.push(labelled('', it.name, tail, { pad: 4 }));
505
+ }
506
+ return out.join('\n');
528
507
  }
529
-
530
508
  // ----------------------------------------------------------------- entry ---
531
-
532
509
  export function entry(repo) {
533
- if (!repo.facts.entries.length) {
534
- return dim(wrapText('No entry points recognized. tour falls back to the most depended-on files.'));
535
- }
536
- const w = termWidth();
537
- return [dim(fit(' entry points', Math.max(4, termWidth() - 2))), ...repo.facts.entries.map((p) => {
538
- const out = repo.facts.fanOut[p] || 0;
539
- return labelled(ok(GLYPHS.entry + ' '), cyan(p), dim(` reaches ${out} files`), { pad: 4 });
540
- })].join('\n');
510
+ if (!repo.facts.entries.length) {
511
+ return dim(wrapText('No entry points recognized. tour falls back to the most depended-on files.'));
512
+ }
513
+ const w = termWidth();
514
+ return [dim(fit(' entry points', Math.max(4, termWidth() - 2))), ...repo.facts.entries.map((p) => {
515
+ const out = repo.facts.fanOut[p] || 0;
516
+ return labelled(ok(GLYPHS.entry + ' '), cyan(p), dim(` reaches ${out} files`), { pad: 4 });
517
+ })].join('\n');
541
518
  }
542
-
543
519
  // ------------------------------------------------------------- externals ---
544
-
545
520
  // Outside packages, and who pulls them in. Dependency drift is the useful part:
546
521
  // declared-but-unused and used-but-undeclared are the two lists a dependency
547
522
  // audit actually acts on.
548
523
  export function externals(repo) {
549
- const ext = repo.scan.externals || [];
550
- if (!ext.length) return dim(wrapText('No external imports found.'));
551
- const w = termWidth();
552
- const sorted = [...ext].sort((a, b) => (b.usedBy?.length || 0) - (a.usedBy?.length || 0));
553
- const shown = sorted.slice(0, 30);
554
- // Two columns when they fit, one per line when they do not. A scoped package
555
- // name is long enough on its own to push a fixed-width column off a narrow
556
- // terminal, so the column width follows the data.
557
- const nameRoom = Math.max(8, Math.min(28, Math.max(...shown.map((x) => String(x.name).length))));
558
- const out = [dim(fit(' external packages', Math.max(4, w - 2)))];
559
- if (w < nameRoom + 16) {
560
- for (const x of shown) {
561
- out.push(labelled('', `${x.name} · ${x.usedBy?.length || 0} files`, '', { pad: 4 }));
524
+ const ext = repo.scan.externals || [];
525
+ if (!ext.length)
526
+ return dim(wrapText('No external imports found.'));
527
+ const w = termWidth();
528
+ const sorted = [...ext].sort((a, b) => (b.usedBy?.length || 0) - (a.usedBy?.length || 0));
529
+ const shown = sorted.slice(0, 30);
530
+ // Two columns when they fit, one per line when they do not. A scoped package
531
+ // name is long enough on its own to push a fixed-width column off a narrow
532
+ // terminal, so the column width follows the data.
533
+ const nameRoom = Math.max(8, Math.min(28, Math.max(...shown.map((x) => String(x.name).length))));
534
+ const out = [dim(fit(' external packages', Math.max(4, w - 2)))];
535
+ if (w < nameRoom + 16) {
536
+ for (const x of shown) {
537
+ out.push(labelled('', `${x.name} · ${x.usedBy?.length || 0} files`, '', { pad: 4 }));
538
+ }
562
539
  }
563
- } else {
564
- for (const x of shown) {
565
- out.push(labelled('', x.name, dim(`${x.usedBy?.length || 0} files`), { pad: 4 }));
540
+ else {
541
+ for (const x of shown) {
542
+ out.push(labelled('', x.name, dim(`${x.usedBy?.length || 0} files`), { pad: 4 }));
543
+ }
566
544
  }
567
- }
568
- const drift = repo.facts.depsDrift;
569
- if (drift?.undeclaredImported?.length) {
570
- out.push(dim(fit(' imported but not declared', Math.max(4, termWidth() - 2))));
571
- out.push(wrapText(drift.undeclaredImported.join(', '), ' ').split('\n').map(warn).join('\n'));
572
- }
573
- if (drift?.unusedDeclared?.length) {
574
- out.push(dim(fit(' declared but never imported', Math.max(4, termWidth() - 2))));
575
- out.push(dim(wrapText(drift.unusedDeclared.join(', '), ' ')));
576
- }
577
- return out.join('\n');
545
+ const drift = repo.facts.depsDrift;
546
+ if (drift?.undeclaredImported?.length) {
547
+ out.push(dim(fit(' imported but not declared', Math.max(4, termWidth() - 2))));
548
+ out.push(wrapText(drift.undeclaredImported.join(', '), ' ').split('\n').map(warn).join('\n'));
549
+ }
550
+ if (drift?.unusedDeclared?.length) {
551
+ out.push(dim(fit(' declared but never imported', Math.max(4, termWidth() - 2))));
552
+ out.push(dim(wrapText(drift.unusedDeclared.join(', '), ' ')));
553
+ }
554
+ return out.join('\n');
578
555
  }
579
-
580
556
  // ----------------------------------------------------------------- about ---
581
-
582
557
  // Which binary is answering. When the terminal and the website disagree, this
583
558
  // is the first thing to ask.
584
559
  export function about(repo, version = '') {
585
- const rows = [
586
- { label: 'version', value: version },
587
- { label: 'repo', value: repo.root },
588
- { label: 'indexed', value: `${repo.searchIndex?.totalDocs ?? 0} files for find` },
589
- { label: 'scanned', value: `${repo.scan.scannedAt}` },
590
- ];
591
- if (repo.gitUrl) rows.push({ label: 'from', value: repo.gitUrl });
592
- if (repo.cloneDir) rows.push({ label: 'clone', value: 'a temp clone — removed when you leave' });
593
- return panel('onboarder', rows);
560
+ const rows = [
561
+ { label: 'version', value: version },
562
+ { label: 'repo', value: repo.root },
563
+ { label: 'indexed', value: `${repo.searchIndex?.totalDocs ?? 0} files for find` },
564
+ { label: 'scanned', value: `${repo.scan.scannedAt}` },
565
+ ];
566
+ if (repo.gitUrl)
567
+ rows.push({ label: 'from', value: repo.gitUrl });
568
+ if (repo.cloneDir)
569
+ rows.push({ label: 'clone', value: 'a temp clone — removed when you leave' });
570
+ return panel('onboarder', rows);
594
571
  }
595
-
596
572
  // `github` — the terminal's answer to the site's "From the remote" section:
597
573
  // what GitHub says about this repository, as opposed to what reading the code
598
574
  // says about it. The distinction is the point. Stars and issues are about the
@@ -602,49 +578,56 @@ export function about(repo, version = '') {
602
578
  // The result is passed in rather than fetched here, because every function in
603
579
  // this file is pure and testable with no network. `ctx.github()` does the asking.
604
580
  export function github(repo, result) {
605
- if (!result) {
606
- return dim(wrapText('Not a GitHub repository, so there is nothing to ask GitHub about.', ' '));
607
- }
608
- if (!result.ok) {
609
- return [bad(fit(' ' + result.reason, Math.max(1, termWidth() - 2))), dim(wrapText(githubHintText(repo), ' '))].join('\n');
610
- }
611
-
612
- const f = result.facts;
613
- const rows = [
614
- row('repo', result.repoPath),
615
- row('stars', String(f.stars)),
616
- row('forks', String(f.forks)),
617
- row('watching', String(f.watching)),
618
- row('open issues', String(f.issues)),
619
- ];
620
- if (f.license) rows.push(row('license', f.license));
621
- if (f.branch) rows.push(row('branch', f.branch));
622
- if (f.created) rows.push(row('created', shortDate(f.created)));
623
- if (f.pushed) rows.push(row('last push', shortDate(f.pushed)));
624
- if (f.archived) rows.push(row('note', 'this repository is archived'));
625
-
626
- const out = [panel(result.repoPath, rows)];
627
- if (f.description) out.push(dim(wrapText(f.description)));
628
- if (f.topics.length) out.push(dim(fit(' topics ' + f.topics.join(' '), Math.max(1, termWidth() - 2))));
629
- if (f.homepage) out.push(dim(fit(' site ' + f.homepage, Math.max(1, termWidth() - 2))));
630
- // Double quotes rather than an escaped apostrophe: the previous spelling of
631
- // this line was a `\\'` inside a single-quoted string, which is a syntax error
632
- // waiting for the next person to touch the file.
633
- out.push(dim(wrapText("These are GitHub's numbers, not this repo's. `health` and `risks` are about the code.", ' ')));
634
- return out.join('\n');
581
+ if (!result) {
582
+ return dim(wrapText('Not a GitHub repository, so there is nothing to ask GitHub about.', ' '));
583
+ }
584
+ if (!result.ok) {
585
+ return [bad(fit(' ' + result.reason, Math.max(1, termWidth() - 2))), dim(wrapText(githubHintText(repo), ' '))].join('\n');
586
+ }
587
+ const f = result.facts;
588
+ const rows = [
589
+ row('repo', result.repoPath),
590
+ row('stars', String(f.stars)),
591
+ row('forks', String(f.forks)),
592
+ row('watching', String(f.watching)),
593
+ row('open issues', String(f.issues)),
594
+ ];
595
+ if (f.license)
596
+ rows.push(row('license', f.license));
597
+ if (f.branch)
598
+ rows.push(row('branch', f.branch));
599
+ if (f.created)
600
+ rows.push(row('created', shortDate(f.created)));
601
+ if (f.pushed)
602
+ rows.push(row('last push', shortDate(f.pushed)));
603
+ if (f.archived)
604
+ rows.push(row('note', 'this repository is archived'));
605
+ const out = [panel(result.repoPath, rows)];
606
+ if (f.description)
607
+ out.push(dim(wrapText(f.description)));
608
+ if (f.topics.length)
609
+ out.push(dim(fit(' topics ' + f.topics.join(' '), Math.max(1, termWidth() - 2))));
610
+ if (f.homepage)
611
+ out.push(dim(fit(' site ' + f.homepage, Math.max(1, termWidth() - 2))));
612
+ // Double quotes rather than an escaped apostrophe: the previous spelling of
613
+ // this line was a `\\'` inside a single-quoted string, which is a syntax error
614
+ // waiting for the next person to touch the file.
615
+ out.push(dim(wrapText("These are GitHub's numbers, not this repo's. `health` and `risks` are about the code.", ' ')));
616
+ return out.join('\n');
635
617
  }
636
-
637
618
  // Returned as plain text and wrapped by the caller: a hint line is exactly the
638
619
  // kind of prose that overflows, because it is written once and never measured.
639
620
  function githubHintText(repo) {
640
- if (repo.gitUrl) return 'It is a clone, so the URL is known — the ask itself failed.';
641
- return 'No origin remote here, so there is no URL to ask about.';
621
+ if (repo.gitUrl)
622
+ return 'It is a clone, so the URL is known — the ask itself failed.';
623
+ return 'No origin remote here, so there is no URL to ask about.';
642
624
  }
643
-
644
625
  // `2024-01-05T…` → `Jan 2024`. A day is noise next to a five-year-old project, and
645
626
  // a full timestamp is four columns wider than the label it sits beside.
646
627
  function shortDate(iso) {
647
- const d = new Date(iso);
648
- if (Number.isNaN(d.getTime())) return String(iso);
649
- return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short' });
628
+ const d = new Date(iso);
629
+ if (Number.isNaN(d.getTime()))
630
+ return String(iso);
631
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short' });
650
632
  }
633
+ //# sourceMappingURL=views.js.map