codebase-onboarder 0.5.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 +196 -47
  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 +449 -0
  8. package/cli/explorer/advanced.js.map +1 -0
  9. package/cli/explorer/app.d.ts +1 -0
  10. package/cli/explorer/app.js +396 -229
  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 +400 -162
  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 +73 -0
  23. package/cli/explorer/github.js.map +1 -0
  24. package/cli/explorer/graphs.d.ts +7 -0
  25. package/cli/explorer/graphs.js +173 -0
  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 +184 -95
  35. package/cli/explorer/session.js.map +1 -0
  36. package/cli/explorer/views.d.ts +19 -0
  37. package/cli/explorer/views.js +496 -412
  38. package/cli/explorer/views.js.map +1 -0
  39. package/cli/explorer/wrap.d.ts +1 -0
  40. package/cli/explorer/wrap.js +33 -0
  41. package/cli/explorer/wrap.js.map +1 -0
  42. package/cli/main.d.ts +1 -0
  43. package/cli/main.js +235 -138
  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 +55 -35
  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 +165 -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 +60 -0
  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 -37
  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 +79 -0
  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,106 +5,106 @@
5
5
  // downloaded tarball has no .git, and a plain folder may simply not be a
6
6
  // checkout. All of those come back as { available: false, reason } so the UI
7
7
  // can say why instead of showing zeros.
8
-
9
8
  import { spawn } from 'node:child_process';
10
-
11
9
  // How far back the window goes. Two thousand commits is years of work on most
12
10
  // repos and keeps the log parse bounded; `totalCommits` travels alongside so
13
11
  // truncation is stated, not silent.
14
12
  const LOG_CAP = 2000;
15
13
  const TIMEOUT_MS = 30_000;
16
-
17
14
  // Record separator \x1e between commits, unit separator \x1f between fields.
18
15
  // Filenames are one per line after the header. A filename containing \x1e or
19
16
  // \x1f is legal on paper and does not happen in practice — the same trade every
20
17
  // machine-readable git format makes (documented limitation, like .gitignore
21
18
  // negation in scan.js).
22
19
  const LOG_FORMAT = '%x1e%H%x1f%an%x1f%ae%x1f%aI';
23
-
24
20
  export async function gitLog(root) {
25
- let countText;
26
- try {
27
- countText = await run(['-C', root, 'rev-list', '--count', 'HEAD']);
28
- } catch (err) {
29
- return { ok: false, reason: explain(err) };
30
- }
31
- const totalCommits = parseInt(countText.trim(), 10) || 0;
32
- if (!totalCommits) {
33
- return { ok: false, reason: 'The repository exists but has no commits yet.' };
34
- }
35
-
36
- let text;
37
- try {
38
- text = await run([
39
- '-C', root, 'log',
40
- '--max-count=' + LOG_CAP,
41
- '--pretty=format:' + LOG_FORMAT,
42
- '--name-only',
43
- '--no-renames',
44
- 'HEAD',
45
- ]);
46
- } catch (err) {
47
- return { ok: false, reason: explain(err) };
48
- }
49
-
50
- return { ok: true, text, totalCommits };
21
+ let countText;
22
+ try {
23
+ countText = await run(['-C', root, 'rev-list', '--count', 'HEAD']);
24
+ }
25
+ catch (err) {
26
+ return { ok: false, reason: explain(err) };
27
+ }
28
+ const totalCommits = parseInt(countText.trim(), 10) || 0;
29
+ if (!totalCommits) {
30
+ return { ok: false, reason: 'The repository exists but has no commits yet.' };
31
+ }
32
+ let text;
33
+ try {
34
+ text = await run([
35
+ '-C', root, 'log',
36
+ '--max-count=' + LOG_CAP,
37
+ '--pretty=format:' + LOG_FORMAT,
38
+ '--name-only',
39
+ '--no-renames',
40
+ 'HEAD',
41
+ ]);
42
+ }
43
+ catch (err) {
44
+ return { ok: false, reason: explain(err) };
45
+ }
46
+ return { ok: true, text, totalCommits };
51
47
  }
52
-
53
48
  // Pure, so the format contract is testable without a git binary.
54
49
  export function parseGitLog(text) {
55
- const commits = [];
56
- for (const record of String(text).split('\x1e')) {
57
- if (!record.trim()) continue;
58
- const lines = record.split('\n');
59
- const [hash, name, email, date] = lines[0].split('\x1f');
60
- if (!hash) continue;
61
- const files = [];
62
- for (let i = 1; i < lines.length; i++) {
63
- const f = lines[i].trim();
64
- if (f) files.push(f);
50
+ const commits = [];
51
+ for (const record of String(text).split('\x1e')) {
52
+ if (!record.trim())
53
+ continue;
54
+ const lines = record.split('\n');
55
+ const [hash, name, email, date] = lines[0].split('\x1f');
56
+ if (!hash)
57
+ continue;
58
+ const files = [];
59
+ for (let i = 1; i < lines.length; i++) {
60
+ const f = lines[i].trim();
61
+ if (f)
62
+ files.push(f);
63
+ }
64
+ commits.push({ hash, author: { name: name || '', email: email || '' }, date: date || '', files });
65
65
  }
66
- commits.push({ hash, author: { name: name || '', email: email || '' }, date: date || '', files });
67
- }
68
- return commits;
66
+ return commits;
69
67
  }
70
-
71
68
  function explain(err) {
72
- const message = err.message || '';
73
- if (err.code === 'ENOENT' || /spawn git ENOENT/.test(message)) {
74
- return 'git is not installed or not on PATH, so there is no history to read.';
75
- }
76
- if (/not a git repository/i.test(message)) {
77
- return 'No .git here — this folder is not a checkout, so there is no history to read.';
78
- }
79
- if (/does not have any commits yet/i.test(message)) {
80
- return 'The repository exists but has no commits yet.';
81
- }
82
- return 'git would not say: ' + message.split('\n').pop();
69
+ const message = err.message || '';
70
+ if (err.code === 'ENOENT' || /spawn git ENOENT/.test(message)) {
71
+ return 'git is not installed or not on PATH, so there is no history to read.';
72
+ }
73
+ if (/not a git repository/i.test(message)) {
74
+ return 'No .git here — this folder is not a checkout, so there is no history to read.';
75
+ }
76
+ if (/does not have any commits yet/i.test(message)) {
77
+ return 'The repository exists but has no commits yet.';
78
+ }
79
+ return 'git would not say: ' + message.split('\n').pop();
83
80
  }
84
-
85
81
  function run(args) {
86
- return new Promise((resolve, reject) => {
87
- const child = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });
88
- let out = '';
89
- let stderr = '';
90
- child.stdout.on('data', (chunk) => { out += chunk; });
91
- child.stderr.on('data', (chunk) => {
92
- stderr += chunk;
93
- if (stderr.length > 2000) stderr = stderr.slice(-2000);
94
- });
95
- const timer = setTimeout(() => {
96
- child.kill('SIGKILL');
97
- reject(new Error('git took too long and was stopped.'));
98
- }, TIMEOUT_MS);
99
- child.on('error', (err) => {
100
- clearTimeout(timer);
101
- err.message = 'Could not run git: ' + err.message;
102
- reject(err);
103
- });
104
- child.on('close', (code) => {
105
- clearTimeout(timer);
106
- if (code === 0) resolve(out);
107
- else reject(new Error(stderr.trim() || ('git exited ' + code)));
82
+ return new Promise((resolve, reject) => {
83
+ const child = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });
84
+ let out = '';
85
+ let stderr = '';
86
+ child.stdout.on('data', (chunk) => { out += chunk; });
87
+ child.stderr.on('data', (chunk) => {
88
+ stderr += chunk;
89
+ if (stderr.length > 2000)
90
+ stderr = stderr.slice(-2000);
91
+ });
92
+ const timer = setTimeout(() => {
93
+ child.kill('SIGKILL');
94
+ reject(new Error('git took too long and was stopped.'));
95
+ }, TIMEOUT_MS);
96
+ child.on('error', (err) => {
97
+ clearTimeout(timer);
98
+ err.message = 'Could not run git: ' + err.message;
99
+ reject(err);
100
+ });
101
+ child.on('close', (code) => {
102
+ clearTimeout(timer);
103
+ if (code === 0)
104
+ resolve(out);
105
+ else
106
+ reject(new Error(stderr.trim() || ('git exited ' + code)));
107
+ });
108
108
  });
109
- });
110
109
  }
110
+ //# sourceMappingURL=gitHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitHistory.js","sourceRoot":"","sources":["../src/server/gitHistory.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading git history for a scanned root. The same discipline as gitClone.js:\n// git is spawned with an argument array — never through a shell — and its\n// output is parsed, not interpreted. A root without history is an expected\n// outcome, not an error: the browser folder-pick flow never touches git, a\n// downloaded tarball has no .git, and a plain folder may simply not be a\n// checkout. All of those come back as { available: false, reason } so the UI\n// can say why instead of showing zeros.\n\nimport { spawn } from 'node:child_process';\n\n// How far back the window goes. Two thousand commits is years of work on most\n// repos and keeps the log parse bounded; `totalCommits` travels alongside so\n// truncation is stated, not silent.\nconst LOG_CAP = 2000;\nconst TIMEOUT_MS = 30_000;\n\n// Record separator \\x1e between commits, unit separator \\x1f between fields.\n// Filenames are one per line after the header. A filename containing \\x1e or\n// \\x1f is legal on paper and does not happen in practice — the same trade every\n// machine-readable git format makes (documented limitation, like .gitignore\n// negation in scan.js).\nconst LOG_FORMAT = '%x1e%H%x1f%an%x1f%ae%x1f%aI';\n\nexport async function gitLog(root) {\n let countText;\n try {\n countText = await run(['-C', root, 'rev-list', '--count', 'HEAD']);\n } catch (err) {\n return { ok: false, reason: explain(err) };\n }\n const totalCommits = parseInt(countText.trim(), 10) || 0;\n if (!totalCommits) {\n return { ok: false, reason: 'The repository exists but has no commits yet.' };\n }\n\n let text;\n try {\n text = await run([\n '-C', root, 'log',\n '--max-count=' + LOG_CAP,\n '--pretty=format:' + LOG_FORMAT,\n '--name-only',\n '--no-renames',\n 'HEAD',\n ]);\n } catch (err) {\n return { ok: false, reason: explain(err) };\n }\n\n return { ok: true, text, totalCommits };\n}\n\n// Pure, so the format contract is testable without a git binary.\nexport function parseGitLog(text) {\n const commits = [];\n for (const record of String(text).split('\\x1e')) {\n if (!record.trim()) continue;\n const lines = record.split('\\n');\n const [hash, name, email, date] = lines[0].split('\\x1f');\n if (!hash) continue;\n const files = [];\n for (let i = 1; i < lines.length; i++) {\n const f = lines[i].trim();\n if (f) files.push(f);\n }\n commits.push({ hash, author: { name: name || '', email: email || '' }, date: date || '', files });\n }\n return commits;\n}\n\nfunction explain(err) {\n const message = err.message || '';\n if (err.code === 'ENOENT' || /spawn git ENOENT/.test(message)) {\n return 'git is not installed or not on PATH, so there is no history to read.';\n }\n if (/not a git repository/i.test(message)) {\n return 'No .git here — this folder is not a checkout, so there is no history to read.';\n }\n if (/does not have any commits yet/i.test(message)) {\n return 'The repository exists but has no commits yet.';\n }\n return 'git would not say: ' + message.split('\\n').pop();\n}\n\nfunction run(args) {\n return new Promise((resolve, reject) => {\n const child = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let out = '';\n let stderr = '';\n child.stdout.on('data', (chunk) => { out += chunk; });\n child.stderr.on('data', (chunk) => {\n stderr += chunk;\n if (stderr.length > 2000) stderr = stderr.slice(-2000);\n });\n const timer = setTimeout(() => {\n child.kill('SIGKILL');\n reject(new Error('git took too long and was stopped.'));\n }, TIMEOUT_MS);\n child.on('error', (err) => {\n clearTimeout(timer);\n err.message = 'Could not run git: ' + err.message;\n reject(err);\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n if (code === 0) resolve(out);\n else reject(new Error(stderr.trim() || ('git exited ' + code)));\n });\n });\n}\n"]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The `origin` remote of a checkout, or null with a reason.
3
+ *
4
+ * `origin` specifically, not "the first remote": origin is the one that means
5
+ * "where this came from". A fork's `upstream` is a different repository, and
6
+ * reporting that repo's stars for a fork is a confident wrong answer.
7
+ */
8
+ export declare function gitRemoteOrigin(root: any): Promise<{
9
+ ok: boolean;
10
+ url?: string;
11
+ reason?: string;
12
+ }>;
@@ -0,0 +1,60 @@
1
+ // The URL a local checkout came from — `git remote get-url origin`.
2
+ //
3
+ // The web app never needs this: it only shows remote facts for a repo it cloned
4
+ // itself, where it already knows the URL it was given. A terminal session is
5
+ // usually started *inside* somebody's checkout, so `github` there has no URL to
6
+ // work from and has to ask git. That is the one place this surface is a strict
7
+ // improvement on the site rather than a copy of it.
8
+ //
9
+ // Same discipline as `gitHistory.js`: git is spawned with an argument array,
10
+ // never through a shell, and a repo with no remote is an expected outcome rather
11
+ // than an error — a plain folder, a tarball, and a checkout with the remote
12
+ // removed are all normal.
13
+ import { spawn } from 'node:child_process';
14
+ const TIMEOUT_MS = 10_000;
15
+ /**
16
+ * The `origin` remote of a checkout, or null with a reason.
17
+ *
18
+ * `origin` specifically, not "the first remote": origin is the one that means
19
+ * "where this came from". A fork's `upstream` is a different repository, and
20
+ * reporting that repo's stars for a fork is a confident wrong answer.
21
+ */
22
+ export function gitRemoteOrigin(root) {
23
+ return new Promise((resolve) => {
24
+ let child;
25
+ try {
26
+ child = spawn('git', ['-C', root, 'remote', 'get-url', 'origin'], {
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ });
29
+ }
30
+ catch (err) {
31
+ resolve({ ok: false, reason: 'Could not run git: ' + err.message });
32
+ return;
33
+ }
34
+ let out = '';
35
+ let stderr = '';
36
+ const cap = (buf, max) => (buf.length > max ? buf.slice(-max) : buf);
37
+ child.stdout.on('data', (c) => { out = cap(out + c, 2000); });
38
+ child.stderr.on('data', (c) => { stderr = cap(stderr + c, 2000); });
39
+ const timer = setTimeout(() => {
40
+ child.kill('SIGKILL');
41
+ resolve({ ok: false, reason: 'git took too long to say where this came from.' });
42
+ }, TIMEOUT_MS);
43
+ // `once`, and the promise is only ever settled once, so a timeout that
44
+ // races a late `close` cannot resolve an already-resolved promise.
45
+ const settle = (value) => { clearTimeout(timer); resolve(value); };
46
+ child.on('error', (err) => settle({ ok: false, reason: 'Could not run git: ' + err.message }));
47
+ child.on('close', (code) => {
48
+ const url = out.trim();
49
+ if (code === 0 && url)
50
+ settle({ ok: true, url });
51
+ else if (/not a git repository/i.test(stderr))
52
+ settle({ ok: false, reason: 'This folder is not a git checkout, so it has no remote.' });
53
+ else if (code === 0)
54
+ settle({ ok: false, reason: 'This checkout has no origin remote.' });
55
+ else
56
+ settle({ ok: false, reason: 'git would not say: ' + (stderr.trim().split('\n').pop() || ('exited ' + code)) });
57
+ });
58
+ });
59
+ }
60
+ //# sourceMappingURL=gitRemote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitRemote.js","sourceRoot":"","sources":["../src/server/gitRemote.ts"],"names":[],"mappings":"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","sourcesContent":["// The URL a local checkout came from — `git remote get-url origin`.\n//\n// The web app never needs this: it only shows remote facts for a repo it cloned\n// itself, where it already knows the URL it was given. A terminal session is\n// usually started *inside* somebody's checkout, so `github` there has no URL to\n// work from and has to ask git. That is the one place this surface is a strict\n// improvement on the site rather than a copy of it.\n//\n// Same discipline as `gitHistory.js`: git is spawned with an argument array,\n// never through a shell, and a repo with no remote is an expected outcome rather\n// than an error — a plain folder, a tarball, and a checkout with the remote\n// removed are all normal.\n\nimport { spawn } from 'node:child_process';\n\nconst TIMEOUT_MS = 10_000;\n\n/**\n * The `origin` remote of a checkout, or null with a reason.\n *\n * `origin` specifically, not \"the first remote\": origin is the one that means\n * \"where this came from\". A fork's `upstream` is a different repository, and\n * reporting that repo's stars for a fork is a confident wrong answer.\n */\nexport function gitRemoteOrigin(root) {\n return new Promise<{ ok: boolean; url?: string; reason?: string }>((resolve) => {\n let child;\n try {\n child = spawn('git', ['-C', root, 'remote', 'get-url', 'origin'], {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n } catch (err) {\n resolve({ ok: false, reason: 'Could not run git: ' + err.message });\n return;\n }\n\n let out = '';\n let stderr = '';\n const cap = (buf, max) => (buf.length > max ? buf.slice(-max) : buf);\n child.stdout.on('data', (c) => { out = cap(out + c, 2000); });\n child.stderr.on('data', (c) => { stderr = cap(stderr + c, 2000); });\n\n const timer = setTimeout(() => {\n child.kill('SIGKILL');\n resolve({ ok: false, reason: 'git took too long to say where this came from.' });\n }, TIMEOUT_MS);\n\n // `once`, and the promise is only ever settled once, so a timeout that\n // races a late `close` cannot resolve an already-resolved promise.\n const settle = (value) => { clearTimeout(timer); resolve(value); };\n\n child.on('error', (err) => settle({ ok: false, reason: 'Could not run git: ' + err.message }));\n child.on('close', (code) => {\n const url = out.trim();\n if (code === 0 && url) settle({ ok: true, url });\n else if (/not a git repository/i.test(stderr)) settle({ ok: false, reason: 'This folder is not a git checkout, so it has no remote.' });\n else if (code === 0) settle({ ok: false, reason: 'This checkout has no origin remote.' });\n else settle({ ok: false, reason: 'git would not say: ' + (stderr.trim().split('\\n').pop() || ('exited ' + code)) });\n });\n });\n}"]}
@@ -0,0 +1,4 @@
1
+ export declare function htmlToText(html: any, limit?: number): {
2
+ title: string;
3
+ text: string;
4
+ };
@@ -8,37 +8,30 @@
8
8
  // after it there is no way to tell the title from the first paragraph. Entities
9
9
  // are decoded last, so a `&lt;script&gt;` written *about* HTML in the docs
10
10
  // cannot turn back into markup that the earlier passes would have removed.
11
-
12
11
  // Enough context for a model to summarise from without sending a whole site.
13
12
  const MAX_TEXT = 14000;
14
-
15
13
  const ENTITIES = {
16
- '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'",
14
+ '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'",
17
15
  };
18
-
19
16
  // Horizontal space, newlines excluded. The non-breaking space belongs here:
20
17
  // `&nbsp;` is decoded to a plain space below, but pages carry the raw character
21
18
  // as well and it should not survive as indentation.
22
19
  const RUN_OF_SPACE = /[ \t\u00a0]+/g;
23
20
  const AROUND_NEWLINE = /[ \t\u00a0]*\n[ \t\u00a0]*/g;
24
-
25
21
  export function htmlToText(html, limit = MAX_TEXT) {
26
- let s = String(html);
27
- s = s.replace(/<script[\s\S]*?<\/script>/gi, ' ');
28
- s = s.replace(/<style[\s\S]*?<\/style>/gi, ' ');
29
-
30
- const rawTitle = (s.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [, ''])[1];
31
- const title = rawTitle.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
32
-
33
- s = s.replace(/<[^>]+>/g, ' ');
34
- s = s.replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&#39;/g, (m) => ENTITIES[m]);
35
-
36
- // Newlines survive as paragraph breaks \u2014 a docs page read as one long line
37
- // loses the structure a summary needs \u2014 but everything else collapses. Every
38
- // tag became a space, so a break arrives as " \n\n\n " and the indentation of
39
- // the source becomes indentation of the text; trimming around each newline is
40
- // what makes the break itself clean.
41
- s = s.replace(RUN_OF_SPACE, ' ').replace(AROUND_NEWLINE, '\n').replace(/\n{3,}/g, '\n\n').trim();
42
-
43
- return { title, text: s.slice(0, limit) };
22
+ let s = String(html);
23
+ s = s.replace(/<script[\s\S]*?<\/script>/gi, ' ');
24
+ s = s.replace(/<style[\s\S]*?<\/style>/gi, ' ');
25
+ const rawTitle = (s.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [, ''])[1];
26
+ const title = rawTitle.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
27
+ s = s.replace(/<[^>]+>/g, ' ');
28
+ s = s.replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&#39;/g, (m) => ENTITIES[m]);
29
+ // Newlines survive as paragraph breaks \u2014 a docs page read as one long line
30
+ // loses the structure a summary needs \u2014 but everything else collapses. Every
31
+ // tag became a space, so a break arrives as " \n\n\n " and the indentation of
32
+ // the source becomes indentation of the text; trimming around each newline is
33
+ // what makes the break itself clean.
34
+ s = s.replace(RUN_OF_SPACE, ' ').replace(AROUND_NEWLINE, '\n').replace(/\n{3,}/g, '\n\n').trim();
35
+ return { title, text: s.slice(0, limit) };
44
36
  }
37
+ //# sourceMappingURL=htmlText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"htmlText.js","sourceRoot":"","sources":["../src/server/htmlText.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,gFAAgF;AAChF,4DAA4D;AAC5D,EAAE;AACF,kFAAkF;AAClF,yEAAyE;AACzE,+EAA+E;AAC/E,gFAAgF;AAChF,2EAA2E;AAC3E,2EAA2E;AAE3E,6EAA6E;AAC7E,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB,MAAM,QAAQ,GAAG;IACf,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG;CACnF,CAAC;AAEF,4EAA4E;AAC5E,gFAAgF;AAChF,oDAAoD;AACpD,MAAM,YAAY,GAAG,eAAe,CAAC;AACrC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,MAAM,UAAU,UAAU,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ;IAC/C,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAClD,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAC;IAEhD,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,kCAAkC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAE5E,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,sCAAsC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1E,gFAAgF;IAChF,kFAAkF;IAClF,8EAA8E;IAC9E,8EAA8E;IAC9E,qCAAqC;IACrC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAEjG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["// HTML to readable text, for the docs fetch. Not a parser and not trying to be:\n// the output is fed to a language model as context, so what matters is that the\n// prose survives and the markup, scripts and styles do not.\n//\n// Order is the whole trick. Script and style bodies go first — their contents are\n// text, not tags, so stripping tags first would leave a page of minified\n// JavaScript behind. The title is lifted before the general tag strip, because\n// after it there is no way to tell the title from the first paragraph. Entities\n// are decoded last, so a `&lt;script&gt;` written *about* HTML in the docs\n// cannot turn back into markup that the earlier passes would have removed.\n\n// Enough context for a model to summarise from without sending a whole site.\nconst MAX_TEXT = 14000;\n\nconst ENTITIES = {\n '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '\"', '&#39;': \"'\",\n};\n\n// Horizontal space, newlines excluded. The non-breaking space belongs here:\n// `&nbsp;` is decoded to a plain space below, but pages carry the raw character\n// as well and it should not survive as indentation.\nconst RUN_OF_SPACE = /[ \\t\\u00a0]+/g;\nconst AROUND_NEWLINE = /[ \\t\\u00a0]*\\n[ \\t\\u00a0]*/g;\n\nexport function htmlToText(html, limit = MAX_TEXT) {\n let s = String(html);\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, ' ');\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, ' ');\n\n const rawTitle = (s.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i) || [, ''])[1];\n const title = rawTitle.replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim();\n\n s = s.replace(/<[^>]+>/g, ' ');\n s = s.replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&#39;/g, (m) => ENTITIES[m]);\n\n // Newlines survive as paragraph breaks \\u2014 a docs page read as one long line\n // loses the structure a summary needs \\u2014 but everything else collapses. Every\n // tag became a space, so a break arrives as \" \\n\\n\\n \" and the indentation of\n // the source becomes indentation of the text; trimming around each newline is\n // what makes the break itself clean.\n s = s.replace(RUN_OF_SPACE, ' ').replace(AROUND_NEWLINE, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();\n\n return { title, text: s.slice(0, limit) };\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export declare const MAX_BODY_BYTES: number;
2
+ export declare function readBody(req: any, limit?: number): Promise<unknown>;
3
+ export declare function sendJSON(res: any, status: any, data: any, headers?: Record<string, any>): void;
4
+ export declare function sendError(res: any, status: any, message: any): void;
5
+ export declare function sendText(res: any, status: any, text: any): void;
package/server/http.js CHANGED
@@ -2,54 +2,51 @@
2
2
  // does. Pulled out of the router so a handler can be read on its own, and so
3
3
  // the body limit is one number in one place rather than a check repeated per
4
4
  // route and forgotten on the next one.
5
-
6
5
  // A scan request carries a path or a URL; an explain request carries a prompt
7
6
  // and a slice of facts. Two megabytes is far more than either needs, and the
8
7
  // point is to have a ceiling at all: a local server with no auth should not let
9
8
  // a stray fetch hold memory open.
10
9
  export const MAX_BODY_BYTES = 2 * 1024 * 1024;
11
-
12
10
  // Resolves to the parsed JSON body, or `{}` for an empty one — a POST with no
13
11
  // body is a valid "just the defaults" request and every caller would otherwise
14
12
  // need the same guard. Rejects on oversized or malformed input; the router turns
15
13
  // that into a 500 with the message.
16
14
  export function readBody(req, limit = MAX_BODY_BYTES) {
17
- return new Promise((resolve, reject) => {
18
- let size = 0;
19
- const chunks = [];
20
- req.on('data', (chunk) => {
21
- size += chunk.length;
22
- if (size > limit) {
23
- reject(new Error('Request body too large.'));
24
- req.destroy();
25
- return;
26
- }
27
- chunks.push(chunk);
15
+ return new Promise((resolve, reject) => {
16
+ let size = 0;
17
+ const chunks = [];
18
+ req.on('data', (chunk) => {
19
+ size += chunk.length;
20
+ if (size > limit) {
21
+ reject(new Error('Request body too large.'));
22
+ req.destroy();
23
+ return;
24
+ }
25
+ chunks.push(chunk);
26
+ });
27
+ req.on('end', () => {
28
+ try {
29
+ resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
30
+ }
31
+ catch {
32
+ reject(new Error('The request body was not valid JSON.'));
33
+ }
34
+ });
35
+ req.on('error', reject);
28
36
  });
29
- req.on('end', () => {
30
- try {
31
- resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
32
- } catch {
33
- reject(new Error('The request body was not valid JSON.'));
34
- }
35
- });
36
- req.on('error', reject);
37
- });
38
37
  }
39
-
40
38
  export function sendJSON(res, status, data, headers = {}) {
41
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers });
42
- res.end(JSON.stringify(data));
39
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers });
40
+ res.end(JSON.stringify(data));
43
41
  }
44
-
45
42
  // Every error the UI shows comes back through here, so the shape is fixed:
46
43
  // `{ error }` with a sentence a person can act on. The front end reads `.error`
47
44
  // and nothing else.
48
45
  export function sendError(res, status, message) {
49
- sendJSON(res, status, { error: message });
46
+ sendJSON(res, status, { error: message });
50
47
  }
51
-
52
48
  export function sendText(res, status, text) {
53
- res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
54
- res.end(text);
49
+ res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
50
+ res.end(text);
55
51
  }
52
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/server/http.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,uCAAuC;AAEvC,8EAA8E;AAC9E,6EAA6E;AAC7E,gFAAgF;AAChF,kCAAkC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C,8EAA8E;AAC9E,+EAA+E;AAC/E,iFAAiF;AACjF,oCAAoC;AACpC,MAAM,UAAU,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,cAAc;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,MAAM,MAAM,GAAG,EAAE,CAAC;QAClB,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YACvB,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC;YACrB,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;gBAC7C,GAAG,CAAC,OAAO,EAAE,CAAC;gBACd,OAAO;YACT,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC;gBACH,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnF,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAwB,EAAE;IAC3E,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,iCAAiC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACzF,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,2EAA2E;AAC3E,gFAAgF;AAChF,oBAAoB;AACpB,MAAM,UAAU,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI;IACxC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,CAAC,CAAC;IACvE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC","sourcesContent":["// Reading a request body and writing a response — the two things every route\n// does. Pulled out of the router so a handler can be read on its own, and so\n// the body limit is one number in one place rather than a check repeated per\n// route and forgotten on the next one.\n\n// A scan request carries a path or a URL; an explain request carries a prompt\n// and a slice of facts. Two megabytes is far more than either needs, and the\n// point is to have a ceiling at all: a local server with no auth should not let\n// a stray fetch hold memory open.\nexport const MAX_BODY_BYTES = 2 * 1024 * 1024;\n\n// Resolves to the parsed JSON body, or `{}` for an empty one — a POST with no\n// body is a valid \"just the defaults\" request and every caller would otherwise\n// need the same guard. Rejects on oversized or malformed input; the router turns\n// that into a 500 with the message.\nexport function readBody(req, limit = MAX_BODY_BYTES) {\n return new Promise((resolve, reject) => {\n let size = 0;\n const chunks = [];\n req.on('data', (chunk) => {\n size += chunk.length;\n if (size > limit) {\n reject(new Error('Request body too large.'));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n try {\n resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});\n } catch {\n reject(new Error('The request body was not valid JSON.'));\n }\n });\n req.on('error', reject);\n });\n}\n\nexport function sendJSON(res, status, data, headers: Record<string, any> = {}) {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers });\n res.end(JSON.stringify(data));\n}\n\n// Every error the UI shows comes back through here, so the shape is fixed:\n// `{ error }` with a sentence a person can act on. The front end reads `.error`\n// and nothing else.\nexport function sendError(res, status, message) {\n sendJSON(res, status, { error: message });\n}\n\nexport function sendText(res, status, text) {\n res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });\n res.end(text);\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function rebindingReason(req: any, extraHosts?: any[]): string;
2
+ export declare function crossOriginReason(req: any): string;
@@ -6,22 +6,18 @@
6
6
  // authenticate. Both exist to stop some *other* page the user happens to have
7
7
  // open from driving this server on their behalf. Binding to 127.0.0.1 keeps
8
8
  // the network out; it does nothing about the browser, which is already inside.
9
-
10
9
  import net from 'node:net';
11
-
12
10
  const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
13
-
14
11
  // "localhost:4310" -> "localhost", "[::1]:4310" -> "[::1]"
15
12
  function hostnameOf(hostHeader) {
16
- const raw = String(hostHeader ?? '').trim().toLowerCase();
17
- if (raw.startsWith('[')) {
18
- const close = raw.indexOf(']');
19
- return close === -1 ? raw : raw.slice(0, close + 1);
20
- }
21
- const colon = raw.lastIndexOf(':');
22
- return colon === -1 ? raw : raw.slice(0, colon);
13
+ const raw = String(hostHeader ?? '').trim().toLowerCase();
14
+ if (raw.startsWith('[')) {
15
+ const close = raw.indexOf(']');
16
+ return close === -1 ? raw : raw.slice(0, close + 1);
17
+ }
18
+ const colon = raw.lastIndexOf(':');
19
+ return colon === -1 ? raw : raw.slice(0, colon);
23
20
  }
24
-
25
21
  // DNS rebinding, the attack this server's loopback bind does not cover: a page
26
22
  // on evil.com whose DNS answer flips to 127.0.0.1 reaches us anyway, and from
27
23
  // then on the browser treats evil.com as the same origin as everything we
@@ -38,25 +34,31 @@ function hostnameOf(hostHeader) {
38
34
  //
39
35
  // Returns null when the request is fine, or a short reason when it isn't.
40
36
  export function rebindingReason(req, extraHosts = []) {
41
- const host = req.headers?.host;
42
- if (!host) return 'The request arrived with no Host header.';
43
- const name = hostnameOf(host);
44
- if (LOOPBACK_HOSTS.has(name)) return null;
45
- for (const extra of extraHosts) {
46
- if (extra === 'ip:*' && net.isIP(name.replace(/^\[|\]$/g, '')) > 0) return null;
47
- if (extra === 'ipv4:*' && net.isIP(name) === 4) return null;
48
- if (extra === 'ipv6:*' && net.isIP(name.replace(/^\[|\]$/g, '')) === 6) return null;
49
- if (extra.startsWith('*.')) {
50
- // "*.trycloudflare.com" matches "abc.trycloudflare.com" but not the bare
51
- // suffix itself and not "evil-trycloudflare.com".
52
- if (name.endsWith(extra.slice(1)) && name.length > extra.length) return null;
53
- } else if (name === extra) {
54
- return null;
37
+ const host = req.headers?.host;
38
+ if (!host)
39
+ return 'The request arrived with no Host header.';
40
+ const name = hostnameOf(host);
41
+ if (LOOPBACK_HOSTS.has(name))
42
+ return null;
43
+ for (const extra of extraHosts) {
44
+ if (extra === 'ip:*' && net.isIP(name.replace(/^\[|\]$/g, '')) > 0)
45
+ return null;
46
+ if (extra === 'ipv4:*' && net.isIP(name) === 4)
47
+ return null;
48
+ if (extra === 'ipv6:*' && net.isIP(name.replace(/^\[|\]$/g, '')) === 6)
49
+ return null;
50
+ if (extra.startsWith('*.')) {
51
+ // "*.trycloudflare.com" matches "abc.trycloudflare.com" but not the bare
52
+ // suffix itself and not "evil-trycloudflare.com".
53
+ if (name.endsWith(extra.slice(1)) && name.length > extra.length)
54
+ return null;
55
+ }
56
+ else if (name === extra) {
57
+ return null;
58
+ }
55
59
  }
56
- }
57
- return `It arrived addressed to ${name}.`;
60
+ return `It arrived addressed to ${name}.`;
58
61
  }
59
-
60
62
  // Cross-site requests: any page in the browser can POST here without being
61
63
  // able to read the answer, and "without being able to read the answer" is not
62
64
  // the same as harmless — /api/scan walks a directory the caller names, and
@@ -74,19 +76,21 @@ export function rebindingReason(req, extraHosts = []) {
74
76
  // A request carrying neither header isn't a browser at all — curl, a test, a
75
77
  // script — and there is no cross-site story for those, so they pass.
76
78
  export function crossOriginReason(req) {
77
- const site = req.headers?.['sec-fetch-site'];
78
- if (site && site !== 'same-origin' && site !== 'none') {
79
- return `The browser reported it as ${site}.`;
80
- }
81
- const origin = req.headers?.origin;
82
- if (origin) {
83
- // The host was just vetted by the rebinding check, so the origin only has
84
- // to agree with it. Both schemes are accepted: a self-hosted server behind
85
- // a tunnel is reached over https while the page's fetches still arrive
86
- // here addressed to the same host the tunnel forwarded.
87
- const host = String(req.headers.host ?? '').trim().toLowerCase();
88
- const expected = new Set(['http://' + host, 'https://' + host]);
89
- if (!expected.has(String(origin).trim().toLowerCase())) return `It came from ${origin}.`;
90
- }
91
- return null;
79
+ const site = req.headers?.['sec-fetch-site'];
80
+ if (site && site !== 'same-origin' && site !== 'none') {
81
+ return `The browser reported it as ${site}.`;
82
+ }
83
+ const origin = req.headers?.origin;
84
+ if (origin) {
85
+ // The host was just vetted by the rebinding check, so the origin only has
86
+ // to agree with it. Both schemes are accepted: a self-hosted server behind
87
+ // a tunnel is reached over https while the page's fetches still arrive
88
+ // here addressed to the same host the tunnel forwarded.
89
+ const host = String(req.headers.host ?? '').trim().toLowerCase();
90
+ const expected = new Set(['http://' + host, 'https://' + host]);
91
+ if (!expected.has(String(origin).trim().toLowerCase()))
92
+ return `It came from ${origin}.`;
93
+ }
94
+ return null;
92
95
  }
96
+ //# sourceMappingURL=httpGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpGuards.js","sourceRoot":"","sources":["../src/server/httpGuards.ts"],"names":[],"mappings":"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","sourcesContent":["// Two questions worth asking about a request before it is allowed to do\n// anything: did it arrive at a name that really is this machine, and did it\n// come from this app's own page?\n//\n// Neither is authentication — a single-user local tool has nobody to\n// authenticate. Both exist to stop some *other* page the user happens to have\n// open from driving this server on their behalf. Binding to 127.0.0.1 keeps\n// the network out; it does nothing about the browser, which is already inside.\n\nimport net from 'node:net';\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);\n\n// \"localhost:4310\" -> \"localhost\", \"[::1]:4310\" -> \"[::1]\"\nfunction hostnameOf(hostHeader) {\n const raw = String(hostHeader ?? '').trim().toLowerCase();\n if (raw.startsWith('[')) {\n const close = raw.indexOf(']');\n return close === -1 ? raw : raw.slice(0, close + 1);\n }\n const colon = raw.lastIndexOf(':');\n return colon === -1 ? raw : raw.slice(0, colon);\n}\n\n// DNS rebinding, the attack this server's loopback bind does not cover: a page\n// on evil.com whose DNS answer flips to 127.0.0.1 reaches us anyway, and from\n// then on the browser treats evil.com as the same origin as everything we\n// return — so the origin check below is looking at `same-origin` and sees\n// nothing wrong. What still gives it away is the Host header, which the page\n// cannot set and which still says evil.com.\n//\n// `extraHosts` is the self-hosted escape hatch: the domain and bind address\n// from the settings file, so a server that was deliberately put on the network\n// answers to its own name and nothing else. A local server passes nothing and\n// keeps the loopback-only rule. Entries may be exact names or `*.suffix`\n// wildcards — enabled tunnels put their random upstream names here, which\n// cannot be known ahead of time.\n//\n// Returns null when the request is fine, or a short reason when it isn't.\nexport function rebindingReason(req, extraHosts = []) {\n const host = req.headers?.host;\n if (!host) return 'The request arrived with no Host header.';\n const name = hostnameOf(host);\n if (LOOPBACK_HOSTS.has(name)) return null;\n for (const extra of extraHosts) {\n if (extra === 'ip:*' && net.isIP(name.replace(/^\\[|\\]$/g, '')) > 0) return null;\n if (extra === 'ipv4:*' && net.isIP(name) === 4) return null;\n if (extra === 'ipv6:*' && net.isIP(name.replace(/^\\[|\\]$/g, '')) === 6) return null;\n if (extra.startsWith('*.')) {\n // \"*.trycloudflare.com\" matches \"abc.trycloudflare.com\" but not the bare\n // suffix itself and not \"evil-trycloudflare.com\".\n if (name.endsWith(extra.slice(1)) && name.length > extra.length) return null;\n } else if (name === extra) {\n return null;\n }\n }\n return `It arrived addressed to ${name}.`;\n}\n\n// Cross-site requests: any page in the browser can POST here without being\n// able to read the answer, and \"without being able to read the answer\" is not\n// the same as harmless — /api/scan walks a directory the caller names, and\n// /api/explain will POST to any URL the caller names.\n//\n// Two signals, and a request only has to fail one to be refused:\n//\n// Sec-Fetch-Site set by the browser, unsettable by page script. `none`\n// means the user did it themselves — typed the URL, opened\n// a bookmark — which no attacking page can arrange.\n// Origin sent on every fetch that isn't GET/HEAD. Compared against\n// this server's own origin, which is `http://` plus the Host\n// we just confirmed is loopback.\n//\n// A request carrying neither header isn't a browser at all — curl, a test, a\n// script — and there is no cross-site story for those, so they pass.\nexport function crossOriginReason(req) {\n const site = req.headers?.['sec-fetch-site'];\n if (site && site !== 'same-origin' && site !== 'none') {\n return `The browser reported it as ${site}.`;\n }\n const origin = req.headers?.origin;\n if (origin) {\n // The host was just vetted by the rebinding check, so the origin only has\n // to agree with it. Both schemes are accepted: a self-hosted server behind\n // a tunnel is reached over https while the page's fetches still arrive\n // here addressed to the same host the tunnel forwarded.\n const host = String(req.headers.host ?? '').trim().toLowerCase();\n const expected = new Set(['http://' + host, 'https://' + host]);\n if (!expected.has(String(origin).trim().toLowerCase())) return `It came from ${origin}.`;\n }\n return null;\n}\n"]}