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.
- package/README.md +196 -47
- package/bin/build-if-source.js +23 -0
- package/cli/commands.d.ts +26 -0
- package/cli/commands.js +836 -811
- package/cli/commands.js.map +1 -0
- package/cli/explorer/advanced.d.ts +14 -0
- package/cli/explorer/advanced.js +449 -0
- package/cli/explorer/advanced.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +396 -229
- package/cli/explorer/app.js.map +1 -0
- package/cli/explorer/assistant.d.ts +9 -0
- package/cli/explorer/assistant.js +58 -0
- package/cli/explorer/assistant.js.map +1 -0
- package/cli/explorer/commands.d.ts +17 -0
- package/cli/explorer/commands.js +400 -162
- package/cli/explorer/commands.js.map +1 -0
- package/cli/explorer/featureViews.d.ts +124 -0
- package/cli/explorer/featureViews.js +179 -0
- package/cli/explorer/featureViews.js.map +1 -0
- package/cli/explorer/github.d.ts +21 -0
- package/cli/explorer/github.js +73 -0
- package/cli/explorer/github.js.map +1 -0
- package/cli/explorer/graphs.d.ts +7 -0
- package/cli/explorer/graphs.js +173 -0
- package/cli/explorer/graphs.js.map +1 -0
- package/cli/explorer/picker.d.ts +22 -0
- package/cli/explorer/picker.js +150 -0
- package/cli/explorer/picker.js.map +1 -0
- package/cli/explorer/report.d.ts +85 -0
- package/cli/explorer/report.js +60 -0
- package/cli/explorer/report.js.map +1 -0
- package/cli/explorer/session.d.ts +449 -0
- package/cli/explorer/session.js +184 -95
- package/cli/explorer/session.js.map +1 -0
- package/cli/explorer/views.d.ts +19 -0
- package/cli/explorer/views.js +496 -412
- package/cli/explorer/views.js.map +1 -0
- package/cli/explorer/wrap.d.ts +1 -0
- package/cli/explorer/wrap.js +33 -0
- package/cli/explorer/wrap.js.map +1 -0
- package/cli/main.d.ts +1 -0
- package/cli/main.js +235 -138
- package/cli/main.js.map +1 -0
- package/cli/prompt.d.ts +5 -0
- package/cli/prompt.js +84 -80
- package/cli/prompt.js.map +1 -0
- package/cli/scanOptions.d.ts +2 -0
- package/cli/scanOptions.js +11 -0
- package/cli/scanOptions.js.map +1 -0
- package/cli/ui.d.ts +33 -0
- package/cli/ui.js +55 -35
- package/cli/ui.js.map +1 -0
- package/cli/wizard.d.ts +187 -0
- package/cli/wizard.js +304 -284
- package/cli/wizard.js.map +1 -0
- package/package.json +15 -4
- package/public/app.d.ts +1 -0
- package/public/app.js +1057 -1009
- package/public/app.js.map +1 -0
- package/public/browser.d.ts +68 -0
- package/public/browser.js +2 -0
- package/public/browser.js.map +1 -0
- package/public/index.html +30 -13
- package/public/js/about.d.ts +6 -0
- package/public/js/about.js +165 -172
- package/public/js/about.js.map +1 -0
- package/public/js/aiDraft.d.ts +3 -0
- package/public/js/aiDraft.js +116 -124
- package/public/js/aiDraft.js.map +1 -0
- package/public/js/analysisPanel.d.ts +7 -0
- package/public/js/analysisPanel.js +166 -166
- package/public/js/analysisPanel.js.map +1 -0
- package/public/js/analysisReport.d.ts +38 -0
- package/public/js/analysisReport.js +105 -110
- package/public/js/analysisReport.js.map +1 -0
- package/public/js/api.d.ts +26 -0
- package/public/js/api.js +176 -167
- package/public/js/api.js.map +1 -0
- package/public/js/atlas.d.ts +6 -0
- package/public/js/atlas.js +50 -58
- package/public/js/atlas.js.map +1 -0
- package/public/js/blameView.d.ts +2 -0
- package/public/js/blameView.js +36 -37
- package/public/js/blameView.js.map +1 -0
- package/public/js/codeTab.d.ts +3 -0
- package/public/js/codeTab.js +175 -180
- package/public/js/codeTab.js.map +1 -0
- package/public/js/codeViewer.d.ts +5 -0
- package/public/js/codeViewer.js +162 -150
- package/public/js/codeViewer.js.map +1 -0
- package/public/js/components/FileChip.d.ts +9 -0
- package/public/js/components/FileChip.js +45 -49
- package/public/js/components/FileChip.js.map +1 -0
- package/public/js/components/MetricSparkline.d.ts +7 -0
- package/public/js/components/MetricSparkline.js +35 -44
- package/public/js/components/MetricSparkline.js.map +1 -0
- package/public/js/components/RiskBadge.d.ts +9 -0
- package/public/js/components/RiskBadge.js +55 -63
- package/public/js/components/RiskBadge.js.map +1 -0
- package/public/js/deepAnalysisView.d.ts +99 -0
- package/public/js/deepAnalysisView.js +282 -302
- package/public/js/deepAnalysisView.js.map +1 -0
- package/public/js/diagramPane.d.ts +8 -0
- package/public/js/diagramPane.js +133 -135
- package/public/js/diagramPane.js.map +1 -0
- package/public/js/diffView.d.ts +6 -0
- package/public/js/diffView.js +222 -209
- package/public/js/diffView.js.map +1 -0
- package/public/js/docsView.d.ts +2 -0
- package/public/js/docsView.js +441 -442
- package/public/js/docsView.js.map +1 -0
- package/public/js/dom.d.ts +189 -0
- package/public/js/dom.js +5 -0
- package/public/js/dom.js.map +1 -0
- package/public/js/fileSourceBrowser.d.ts +5 -0
- package/public/js/fileSourceBrowser.js +33 -41
- package/public/js/fileSourceBrowser.js.map +1 -0
- package/public/js/flameGraph.d.ts +5 -0
- package/public/js/flameGraph.js +62 -72
- package/public/js/flameGraph.js.map +1 -0
- package/public/js/forceGraph.d.ts +10 -0
- package/public/js/forceGraph.js +581 -556
- package/public/js/forceGraph.js.map +1 -0
- package/public/js/heatmap.d.ts +4 -0
- package/public/js/heatmap.js +142 -164
- package/public/js/heatmap.js.map +1 -0
- package/public/js/highlight.d.ts +2 -0
- package/public/js/highlight.js +116 -107
- package/public/js/highlight.js.map +1 -0
- package/public/js/html.d.ts +2 -0
- package/public/js/html.js +14 -44
- package/public/js/html.js.map +1 -0
- package/public/js/insightsView.d.ts +5 -0
- package/public/js/insightsView.js +93 -111
- package/public/js/insightsView.js.map +1 -0
- package/public/js/inspector.d.ts +37 -0
- package/public/js/inspector.js +416 -445
- package/public/js/inspector.js.map +1 -0
- package/public/js/llm.d.ts +113 -0
- package/public/js/llm.js +233 -272
- package/public/js/llm.js.map +1 -0
- package/public/js/mapView.d.ts +3 -0
- package/public/js/mapView.js +121 -120
- package/public/js/mapView.js.map +1 -0
- package/public/js/markdown.d.ts +3 -0
- package/public/js/markdown.js +84 -91
- package/public/js/markdown.js.map +1 -0
- package/public/js/mindmap.d.ts +32 -0
- package/public/js/mindmap.js +244 -259
- package/public/js/mindmap.js.map +1 -0
- package/public/js/mobilePanels.d.ts +5 -0
- package/public/js/mobilePanels.js +50 -0
- package/public/js/mobilePanels.js.map +1 -0
- package/public/js/repoFiles.d.ts +12 -0
- package/public/js/repoFiles.js +28 -27
- package/public/js/repoFiles.js.map +1 -0
- package/public/js/reviewView.d.ts +28 -0
- package/public/js/reviewView.js +256 -0
- package/public/js/reviewView.js.map +1 -0
- package/public/js/sbomView.d.ts +3 -0
- package/public/js/sbomView.js +59 -67
- package/public/js/sbomView.js.map +1 -0
- package/public/js/scanCache.d.ts +16 -0
- package/public/js/scanCache.js +67 -65
- package/public/js/scanCache.js.map +1 -0
- package/public/js/search.d.ts +7 -0
- package/public/js/search.js +868 -800
- package/public/js/search.js.map +1 -0
- package/public/js/searchLocal.d.ts +42 -0
- package/public/js/searchLocal.js +87 -0
- package/public/js/searchLocal.js.map +1 -0
- package/public/js/serverSettings.d.ts +5 -0
- package/public/js/serverSettings.js +153 -164
- package/public/js/serverSettings.js.map +1 -0
- package/public/js/state.d.ts +798 -0
- package/public/js/state.js +114 -122
- package/public/js/state.js.map +1 -0
- package/public/js/tour.d.ts +6 -0
- package/public/js/tour.js +5 -7
- package/public/js/tour.js.map +1 -0
- package/public/js/transitions.d.ts +2 -0
- package/public/js/transitions.js +9 -10
- package/public/js/transitions.js.map +1 -0
- package/public/js/tree.d.ts +12 -0
- package/public/js/tree.js +157 -154
- package/public/js/tree.js.map +1 -0
- package/public/js/workflowsView.d.ts +3 -0
- package/public/js/workflowsView.js +15 -22
- package/public/js/workflowsView.js.map +1 -0
- package/public/search.css +495 -0
- package/public/styles.css +163 -346
- package/public/workbench.css +166 -0
- package/server/apiAuth.d.ts +3 -0
- package/server/apiAuth.js +18 -20
- package/server/apiAuth.js.map +1 -0
- package/server/apiDiff.d.ts +2 -0
- package/server/apiDiff.js +20 -18
- package/server/apiDiff.js.map +1 -0
- package/server/apiDocs.d.ts +2 -0
- package/server/apiDocs.js +38 -40
- package/server/apiDocs.js.map +1 -0
- package/server/apiFile.d.ts +1 -0
- package/server/apiFile.js +14 -17
- package/server/apiFile.js.map +1 -0
- package/server/apiGitBlame.d.ts +2 -0
- package/server/apiGitBlame.js +50 -51
- package/server/apiGitBlame.js.map +1 -0
- package/server/apiMcp.d.ts +4 -0
- package/server/apiMcp.js +64 -66
- package/server/apiMcp.js.map +1 -0
- package/server/apiReview.d.ts +2 -0
- package/server/apiReview.js +45 -0
- package/server/apiReview.js.map +1 -0
- package/server/apiScan.d.ts +5 -0
- package/server/apiScan.js +90 -71
- package/server/apiScan.js.map +1 -0
- package/server/apiSearch.d.ts +33 -0
- package/server/apiSearch.js +184 -200
- package/server/apiSearch.js.map +1 -0
- package/server/apiSettings.d.ts +4 -0
- package/server/apiSettings.js +84 -85
- package/server/apiSettings.js.map +1 -0
- package/server/apiTools.d.ts +3 -0
- package/server/apiTools.js +55 -56
- package/server/apiTools.js.map +1 -0
- package/server/auth.d.ts +5 -0
- package/server/auth.js +54 -51
- package/server/auth.js.map +1 -0
- package/server/config.d.ts +42 -0
- package/server/config.js +189 -194
- package/server/config.js.map +1 -0
- package/server/daemon.d.ts +12 -0
- package/server/daemon.js +124 -114
- package/server/daemon.js.map +1 -0
- package/server/fileSourceNode.d.ts +2 -0
- package/server/fileSourceNode.js +34 -36
- package/server/fileSourceNode.js.map +1 -0
- package/server/gitClone.d.ts +9 -0
- package/server/gitClone.js +59 -61
- package/server/gitClone.js.map +1 -0
- package/server/gitDiff.d.ts +36 -0
- package/server/gitDiff.js +271 -169
- package/server/gitDiff.js.map +1 -0
- package/server/gitHistory.d.ts +12 -0
- package/server/gitHistory.js +80 -80
- package/server/gitHistory.js.map +1 -0
- package/server/gitRemote.d.ts +12 -0
- package/server/gitRemote.js +60 -0
- package/server/gitRemote.js.map +1 -0
- package/server/htmlText.d.ts +4 -0
- package/server/htmlText.js +16 -23
- package/server/htmlText.js.map +1 -0
- package/server/http.d.ts +5 -0
- package/server/http.js +27 -30
- package/server/http.js.map +1 -0
- package/server/httpGuards.d.ts +2 -0
- package/server/httpGuards.js +47 -43
- package/server/httpGuards.js.map +1 -0
- package/server/https.d.ts +34 -0
- package/server/https.js +121 -111
- package/server/https.js.map +1 -0
- package/server/index.d.ts +40 -0
- package/server/index.js +139 -147
- package/server/index.js.map +1 -0
- package/server/layout.d.ts +26 -0
- package/server/layout.js +59 -37
- package/server/layout.js.map +1 -0
- package/server/llmProxy.d.ts +3 -0
- package/server/llmProxy.js +137 -135
- package/server/llmProxy.js.map +1 -0
- package/server/logger.d.ts +38 -0
- package/server/logger.js +198 -211
- package/server/logger.js.map +1 -0
- package/server/mcp/analysis.d.ts +29 -0
- package/server/mcp/analysis.js +99 -120
- package/server/mcp/analysis.js.map +1 -0
- package/server/mcp/http.d.ts +9 -0
- package/server/mcp/http.js +128 -137
- package/server/mcp/http.js.map +1 -0
- package/server/mcp/runner.d.ts +68 -0
- package/server/mcp/runner.js +208 -218
- package/server/mcp/runner.js.map +1 -0
- package/server/mcp/server.d.ts +29 -0
- package/server/mcp/server.js +166 -179
- package/server/mcp/server.js.map +1 -0
- package/server/mcp/standalone.d.ts +1 -0
- package/server/mcp/standalone.js +8 -10
- package/server/mcp/standalone.js.map +1 -0
- package/server/mcp/tools.d.ts +2020 -0
- package/server/mcp/tools.js +523 -574
- package/server/mcp/tools.js.map +1 -0
- package/server/paths.d.ts +3 -0
- package/server/paths.js +17 -15
- package/server/paths.js.map +1 -0
- package/server/pidfile.d.ts +9 -0
- package/server/pidfile.js +43 -45
- package/server/pidfile.js.map +1 -0
- package/server/review.d.ts +5 -0
- package/server/review.js +35 -0
- package/server/review.js.map +1 -0
- package/server/router.d.ts +33 -0
- package/server/router.js +191 -197
- package/server/router.js.map +1 -0
- package/server/scanRequest.d.ts +3 -0
- package/server/scanRequest.js +36 -0
- package/server/scanRequest.js.map +1 -0
- package/server/searchIndex.d.ts +17 -0
- package/server/searchIndex.js +83 -80
- package/server/searchIndex.js.map +1 -0
- package/server/sessions.d.ts +14 -0
- package/server/sessions.js +62 -70
- package/server/sessions.js.map +1 -0
- package/server/startup.d.ts +118 -0
- package/server/startup.js +143 -148
- package/server/startup.js.map +1 -0
- package/server/static.d.ts +6 -0
- package/server/static.js +28 -31
- package/server/static.js.map +1 -0
- package/server/tools/install.d.ts +4 -0
- package/server/tools/install.js +176 -185
- package/server/tools/install.js.map +1 -0
- package/server/tools/parse.d.ts +8 -0
- package/server/tools/parse.js +112 -114
- package/server/tools/parse.js.map +1 -0
- package/server/tools/platform.d.ts +9 -0
- package/server/tools/platform.js +49 -47
- package/server/tools/platform.js.map +1 -0
- package/server/tools/registry.d.ts +137 -0
- package/server/tools/registry.js +173 -172
- package/server/tools/registry.js.map +1 -0
- package/server/tools/scan.d.ts +34 -0
- package/server/tools/scan.js +89 -98
- package/server/tools/scan.js.map +1 -0
- package/server/tools.d.ts +34 -0
- package/server/tools.js +94 -107
- package/server/tools.js.map +1 -0
- package/server/tunnel.d.ts +29 -0
- package/server/tunnel.js +61 -66
- package/server/tunnel.js.map +1 -0
- package/shared/analyzer/docs.d.ts +16 -0
- package/shared/analyzer/docs.js +99 -93
- package/shared/analyzer/docs.js.map +1 -0
- package/shared/analyzer/explainLocal.d.ts +4 -0
- package/shared/analyzer/explainLocal.js +122 -142
- package/shared/analyzer/explainLocal.js.map +1 -0
- package/shared/analyzer/github.d.ts +37 -0
- package/shared/analyzer/github.js +79 -0
- package/shared/analyzer/github.js.map +1 -0
- package/shared/analyzer/graph.d.ts +41 -0
- package/shared/analyzer/graph.js +393 -400
- package/shared/analyzer/graph.js.map +1 -0
- package/shared/analyzer/health.d.ts +33 -0
- package/shared/analyzer/health.js +197 -193
- package/shared/analyzer/health.js.map +1 -0
- package/shared/analyzer/history.d.ts +29 -0
- package/shared/analyzer/history.js +110 -111
- package/shared/analyzer/history.js.map +1 -0
- package/shared/analyzer/jsonConfig.d.ts +2 -0
- package/shared/analyzer/jsonConfig.js +61 -0
- package/shared/analyzer/jsonConfig.js.map +1 -0
- package/shared/analyzer/languages/csharp.d.ts +26 -0
- package/shared/analyzer/languages/csharp.js +83 -88
- package/shared/analyzer/languages/csharp.js.map +1 -0
- package/shared/analyzer/languages/generic.d.ts +23 -0
- package/shared/analyzer/languages/generic.js +94 -105
- package/shared/analyzer/languages/generic.js.map +1 -0
- package/shared/analyzer/languages/go.d.ts +24 -0
- package/shared/analyzer/languages/go.js +45 -54
- package/shared/analyzer/languages/go.js.map +1 -0
- package/shared/analyzer/languages/index.d.ts +6 -0
- package/shared/analyzer/languages/index.js +19 -20
- package/shared/analyzer/languages/index.js.map +1 -0
- package/shared/analyzer/languages/java.d.ts +20 -0
- package/shared/analyzer/languages/java.js +73 -78
- package/shared/analyzer/languages/java.js.map +1 -0
- package/shared/analyzer/languages/javascript.d.ts +12 -0
- package/shared/analyzer/languages/javascript.js +258 -209
- package/shared/analyzer/languages/javascript.js.map +1 -0
- package/shared/analyzer/languages/python.d.ts +23 -0
- package/shared/analyzer/languages/python.js +92 -100
- package/shared/analyzer/languages/python.js.map +1 -0
- package/shared/analyzer/languages/rust.d.ts +17 -0
- package/shared/analyzer/languages/rust.js +96 -98
- package/shared/analyzer/languages/rust.js.map +1 -0
- package/shared/analyzer/languages/typescript.d.ts +11 -0
- package/shared/analyzer/languages/typescript.js +117 -116
- package/shared/analyzer/languages/typescript.js.map +1 -0
- package/shared/analyzer/licenses.d.ts +18 -0
- package/shared/analyzer/licenses.js +130 -133
- package/shared/analyzer/licenses.js.map +1 -0
- package/shared/analyzer/metrics.d.ts +20 -0
- package/shared/analyzer/metrics.js +112 -104
- package/shared/analyzer/metrics.js.map +1 -0
- package/shared/analyzer/pathUtil.d.ts +10 -0
- package/shared/analyzer/pathUtil.js +29 -35
- package/shared/analyzer/pathUtil.js.map +1 -0
- package/shared/analyzer/patterns.d.ts +11 -0
- package/shared/analyzer/patterns.js +231 -253
- package/shared/analyzer/patterns.js.map +1 -0
- package/shared/analyzer/scan.d.ts +62 -0
- package/shared/analyzer/scan.js +451 -442
- package/shared/analyzer/scan.js.map +1 -0
- package/shared/analyzer/scanControl.d.ts +11 -0
- package/shared/analyzer/scanControl.js +62 -0
- package/shared/analyzer/scanControl.js.map +1 -0
- package/shared/analyzer/security.d.ts +18 -0
- package/shared/analyzer/security.js +159 -159
- package/shared/analyzer/security.js.map +1 -0
- package/shared/analyzer/services.d.ts +5 -0
- package/shared/analyzer/services.js +141 -138
- package/shared/analyzer/services.js.map +1 -0
- package/shared/analyzer/stack.d.ts +6 -0
- package/shared/analyzer/stack.js +134 -141
- package/shared/analyzer/stack.js.map +1 -0
- package/shared/analyzer/tour.d.ts +2 -0
- package/shared/analyzer/tour.js +45 -53
- package/shared/analyzer/tour.js.map +1 -0
- package/shared/analyzer/util.d.ts +8 -0
- package/shared/analyzer/util.js +130 -76
- package/shared/analyzer/util.js.map +1 -0
- package/shared/analyzer/workflows.d.ts +8 -0
- package/shared/analyzer/workflows.js +139 -134
- package/shared/analyzer/workflows.js.map +1 -0
- package/shared/contracts.d.ts +250 -0
- package/shared/contracts.js +2 -0
- package/shared/contracts.js.map +1 -0
- package/shared/diagram/aiFacts.d.ts +35 -0
- package/shared/diagram/aiFacts.js +102 -105
- package/shared/diagram/aiFacts.js.map +1 -0
- package/shared/diagram/aiMermaid.d.ts +5 -0
- package/shared/diagram/aiMermaid.js +46 -45
- package/shared/diagram/aiMermaid.js.map +1 -0
- package/shared/diagram/atlas.d.ts +16 -0
- package/shared/diagram/atlas.js +51 -60
- package/shared/diagram/atlas.js.map +1 -0
- package/shared/diagram/mermaid.d.ts +43 -0
- package/shared/diagram/mermaid.js +348 -370
- package/shared/diagram/mermaid.js.map +1 -0
- package/shared/review/contracts.d.ts +95 -0
- package/shared/review/contracts.js +2 -0
- package/shared/review/contracts.js.map +1 -0
- package/shared/review/review.d.ts +10 -0
- package/shared/review/review.js +184 -0
- package/shared/review/review.js.map +1 -0
- package/shared/search/query.d.ts +32 -0
- package/shared/search/query.js +395 -388
- package/shared/search/query.js.map +1 -0
package/cli/explorer/views.js
CHANGED
|
@@ -9,541 +9,625 @@
|
|
|
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';
|
|
13
|
+
import { labelled } from './graphs.js';
|
|
14
14
|
import { fileFacts, readRepoFile, resolveTarget, searchRepo, explainRepoFile, explainRepoFolder, explainRepoOverview } from './session.js';
|
|
15
15
|
import { scanCaveats } from '../../shared/analyzer/explainLocal.js';
|
|
16
|
-
|
|
16
|
+
import { wrapText } from './wrap.js';
|
|
17
17
|
const MAX = 200; // a ceiling on any list, so one command cannot flood a terminal
|
|
18
|
-
|
|
19
18
|
// Glyphs degrade for terminals that cannot show them. TUIKit's rule: a missing
|
|
20
19
|
// glyph is a rendering bug the user should never have to report, so the ASCII
|
|
21
20
|
// set is a first-class path, not a fallback we hope nobody needs.
|
|
22
21
|
const GLYPHS = process.platform === 'win32' && !process.env.WT_SESSION
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
? { dir: '+', file: '-', entry: '>', hub: '*', arrow: '->', warn: '!', good: '+' }
|
|
23
|
+
: { dir: '▸', file: '·', entry: '▶', hub: '◆', arrow: '→', warn: '⚠', good: '✓' };
|
|
26
24
|
// The footer is a hint bar, and a hint bar that wraps is worse than no hint bar
|
|
27
25
|
// — it is the first thing printed, so it sets the impression of everything else.
|
|
28
26
|
// On a narrow terminal it drops hints rather than wrapping.
|
|
29
27
|
function hintBar(hints) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// lines. Applied *before* painting, so the wrap never has to understand ANSI —
|
|
43
|
-
// the styled string is built from already-wrapped plain text.
|
|
44
|
-
//
|
|
45
|
-
// This is what keeps a long caveat or a paragraph of explanation from running
|
|
46
|
-
// off the right edge. The engine's own messages are written as sentences for a
|
|
47
|
-
// browser panel; a terminal needs them folded.
|
|
48
|
-
function wrapText(text, indent = ' ', room = termWidth() - indent.length) {
|
|
49
|
-
const out = [];
|
|
50
|
-
for (const para of String(text).split('\n')) {
|
|
51
|
-
if (!para.trim()) {
|
|
52
|
-
out.push('');
|
|
53
|
-
continue;
|
|
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);
|
|
54
40
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!line) {
|
|
58
|
-
line = word;
|
|
59
|
-
} else if (line.length + 1 + word.length <= room) {
|
|
60
|
-
line += ' ' + word;
|
|
61
|
-
} else {
|
|
62
|
-
out.push(indent + line);
|
|
63
|
-
line = word;
|
|
64
|
-
}
|
|
65
|
-
// A single word longer than the room is hard-split rather than allowed to
|
|
66
|
-
// overflow — a long import specifier is exactly the case that shows up.
|
|
67
|
-
while (line.length > room) {
|
|
68
|
-
out.push(indent + line.slice(0, room));
|
|
69
|
-
line = line.slice(room);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
out.push(indent + line);
|
|
73
|
-
}
|
|
74
|
-
return out.join('\n');
|
|
41
|
+
const line = parts.join(' · ') || hints[0].slice(0, room);
|
|
42
|
+
return dim(' ' + line);
|
|
75
43
|
}
|
|
76
|
-
|
|
44
|
+
// `wrapText` now lives in `./wrap.js`, shared with the command table's help screen.
|
|
77
45
|
// -------------------------------------------------------------- overview ---
|
|
78
|
-
|
|
79
46
|
// The landing view. Answers "what am I looking at, and where do I start" in one
|
|
80
47
|
// screen — the same three questions the site's first paint answers.
|
|
81
48
|
export function overview(repo) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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');
|
|
106
74
|
}
|
|
107
|
-
|
|
108
75
|
function gradeColor(grade) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
76
|
+
if (grade === 'A' || grade === 'B')
|
|
77
|
+
return ok(grade);
|
|
78
|
+
if (grade === 'C')
|
|
79
|
+
return warn(grade);
|
|
80
|
+
return bad(grade);
|
|
112
81
|
}
|
|
113
|
-
|
|
114
82
|
// ------------------------------------------------------------------ tree ---
|
|
115
|
-
|
|
116
83
|
// The file tree. Directories first, then files, each marked with the role that
|
|
117
84
|
// makes it worth noticing — an entry point or a hub is the two things a new
|
|
118
85
|
// reader is looking for, and marking them here saves a separate `hubs` command
|
|
119
86
|
// on every repo.
|
|
120
87
|
export function tree(repo, { sub = '', depth = 2, limit = MAX } = {}) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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);
|
|
142
111
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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');
|
|
167
136
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
137
|
+
function fileLine(repo, f, indent = 2) {
|
|
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 });
|
|
174
147
|
}
|
|
175
|
-
|
|
176
148
|
function countUnder(node) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
149
|
+
let n = node.files.length;
|
|
150
|
+
for (const d of node.dirs.values())
|
|
151
|
+
n += countUnder(d);
|
|
152
|
+
return n;
|
|
180
153
|
}
|
|
181
|
-
|
|
182
154
|
// ------------------------------------------------------------------ find ---
|
|
183
|
-
|
|
184
155
|
// Search, using the site's query language. The result rows carry the same three
|
|
185
156
|
// things the site's palette shows — path, line, snippet — fitted to the
|
|
186
157
|
// terminal, with the path keeping the room it needs because it is what
|
|
187
158
|
// identifies the hit.
|
|
188
159
|
export function find(repo, { query = '', limit = 12 } = {}) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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');
|
|
206
182
|
}
|
|
207
|
-
|
|
208
183
|
// ------------------------------------------------------------------ show ---
|
|
209
|
-
|
|
210
184
|
// Read a file in the terminal. This is the one view that cannot be pure — it
|
|
211
185
|
// touches the disk — so it is the one async view, and it goes through the same
|
|
212
186
|
// containment check the HTTP route uses rather than reaching for a path itself.
|
|
213
187
|
export async function show(repo, { target = '', from = 0, count = 0 } = {}) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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');
|
|
239
219
|
}
|
|
240
|
-
|
|
241
220
|
// Render a resolver miss the same way everywhere: the reason, then the
|
|
242
221
|
// candidates. A "did you mean" is only useful if it is actionable.
|
|
222
|
+
//
|
|
223
|
+
// The reason is word-wrapped, because a miss on a long path or an unfamiliar
|
|
224
|
+
// filename produces a long sentence, and the commands that surface this
|
|
225
|
+
// (symbols, blast, graph, blame) do not each remember to wrap it.
|
|
243
226
|
export function targetError(found) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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');
|
|
250
240
|
}
|
|
251
|
-
|
|
252
241
|
// ------------------------------------------------------------------ deps ---
|
|
253
|
-
|
|
254
242
|
// What a file connects to, in both directions, plus the verdict the graph has
|
|
255
243
|
// on it. The two lists are the point: "what does this pull in" and "what breaks
|
|
256
244
|
// if this breaks" are different questions and a single edge count answers
|
|
257
245
|
// neither.
|
|
258
246
|
export function deps(repo, { target = '', limit = 15 } = {}) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
};
|
|
280
|
-
|
|
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');
|
|
281
270
|
}
|
|
282
|
-
|
|
283
271
|
// --------------------------------------------------------------- explain ---
|
|
284
|
-
|
|
285
272
|
// The prose explanation, straight from `explainLocal.js` — the same notes the
|
|
286
273
|
// site shows when no AI key is configured, with no key required and nothing
|
|
287
274
|
// sent anywhere. Markdown backticks are the one bit of markup kept, turned into
|
|
288
275
|
// terminal emphasis.
|
|
289
276
|
export function explain(repo, { target = '' } = {}) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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));
|
|
295
285
|
}
|
|
296
|
-
|
|
297
286
|
// `**bold**` and `` `code` `` are the only two marks `explainLocal` emits. They
|
|
298
287
|
// become terminal styling; everything else passes through untouched, so the
|
|
299
288
|
// prose still reads correctly with color off. Wrapping happens on the plain
|
|
300
289
|
// text first, then the marks are styled in place — styling after wrapping keeps
|
|
301
290
|
// the wrap from having to measure escape sequences.
|
|
302
291
|
function renderProse(md) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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));
|
|
312
301
|
})
|
|
313
|
-
|
|
302
|
+
.join('\n\n');
|
|
314
303
|
}
|
|
315
|
-
|
|
316
304
|
// ------------------------------------------------------------------ tour ---
|
|
317
|
-
|
|
318
305
|
// The guided read. The same eight stops the site and the MCP server hand out,
|
|
319
306
|
// in the same order, with the same one-line reason for each — a person at the
|
|
320
307
|
// prompt and an agent asking the MCP server are being told the same thing about
|
|
321
308
|
// the same repository.
|
|
322
309
|
export function tour(repo) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
350
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const room = Math.max(12, termWidth() - 10);
|
|
370
|
-
out.push(' ' + dim('· ') + fit(b.label, room) + ' ' + pts);
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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');
|
|
390
378
|
}
|
|
391
|
-
|
|
392
379
|
// ---------------------------------------------------------------- layers ---
|
|
393
|
-
|
|
394
380
|
// Import depth, shallowest first. This is the architecture as a staircase:
|
|
395
381
|
// layer 0 is what runs, and each step down is something it can reach.
|
|
396
382
|
export function layers(repo, { limit = 12 } = {}) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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');
|
|
410
399
|
}
|
|
411
|
-
|
|
412
400
|
// -------------------------------------------------------------- patterns ---
|
|
413
|
-
|
|
414
401
|
// The architecture, in prose. These are the observations `detectPatterns`
|
|
415
402
|
// makes, in the order a senior dev would mention them.
|
|
416
403
|
export function patterns(repo) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
}
|
|
425
414
|
}
|
|
426
|
-
|
|
427
|
-
return out.join('\n');
|
|
415
|
+
return out.join('\n');
|
|
428
416
|
}
|
|
429
|
-
|
|
430
417
|
// ----------------------------------------------------------------- stats ---
|
|
431
|
-
|
|
432
418
|
// The numbers, grouped by the question each answers: what is it made of, where
|
|
433
419
|
// is the bulk of it, and what is the heaviest thing in it.
|
|
434
420
|
export function stats(repo, { limit = 10 } = {}) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
out.push(
|
|
463
|
-
for (const
|
|
464
|
-
|
|
465
|
-
|
|
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');
|
|
466
467
|
}
|
|
467
|
-
|
|
468
468
|
// -------------------------------------------------------------- security ---
|
|
469
|
-
|
|
470
469
|
export function security(repo, { limit = 12 } = {}) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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');
|
|
485
486
|
}
|
|
486
|
-
|
|
487
487
|
// ----------------------------------------------------------------- stack ---
|
|
488
|
-
|
|
489
|
-
//
|
|
488
|
+
// The dependency picture the site shows, from the same `analyzeStack`. Columns
|
|
489
|
+
// are computed from the widest *actual* value rather than fixed padding, so a
|
|
490
|
+
// long scoped package name shrinks the columns instead of pushing the row off
|
|
491
|
+
// the right edge.
|
|
490
492
|
export function stack(repo, { limit = 20 } = {}) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
out
|
|
497
|
-
|
|
498
|
-
|
|
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');
|
|
499
507
|
}
|
|
500
|
-
|
|
501
508
|
// ----------------------------------------------------------------- entry ---
|
|
502
|
-
|
|
503
509
|
export function entry(repo) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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');
|
|
511
518
|
}
|
|
512
|
-
|
|
513
519
|
// ------------------------------------------------------------- externals ---
|
|
514
|
-
|
|
515
520
|
// Outside packages, and who pulls them in. Dependency drift is the useful part:
|
|
516
521
|
// declared-but-unused and used-but-undeclared are the two lists a dependency
|
|
517
522
|
// audit actually acts on.
|
|
518
523
|
export function externals(repo) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
out
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
for (const x of shown) {
|
|
542
|
+
out.push(labelled('', x.name, dim(`${x.usedBy?.length || 0} files`), { pad: 4 }));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
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');
|
|
536
555
|
}
|
|
537
|
-
|
|
538
556
|
// ----------------------------------------------------------------- about ---
|
|
539
|
-
|
|
540
557
|
// Which binary is answering. When the terminal and the website disagree, this
|
|
541
558
|
// is the first thing to ask.
|
|
542
559
|
export function about(repo, version = '') {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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);
|
|
571
|
+
}
|
|
572
|
+
// `github` — the terminal's answer to the site's "From the remote" section:
|
|
573
|
+
// what GitHub says about this repository, as opposed to what reading the code
|
|
574
|
+
// says about it. The distinction is the point. Stars and issues are about the
|
|
575
|
+
// project's standing; `health` and `risks` are about the code in front of you,
|
|
576
|
+
// and neither can tell you the other.
|
|
577
|
+
//
|
|
578
|
+
// The result is passed in rather than fetched here, because every function in
|
|
579
|
+
// this file is pure and testable with no network. `ctx.github()` does the asking.
|
|
580
|
+
export function github(repo, result) {
|
|
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');
|
|
617
|
+
}
|
|
618
|
+
// Returned as plain text and wrapped by the caller: a hint line is exactly the
|
|
619
|
+
// kind of prose that overflows, because it is written once and never measured.
|
|
620
|
+
function githubHintText(repo) {
|
|
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.';
|
|
624
|
+
}
|
|
625
|
+
// `2024-01-05T…` → `Jan 2024`. A day is noise next to a five-year-old project, and
|
|
626
|
+
// a full timestamp is four columns wider than the label it sits beside.
|
|
627
|
+
function shortDate(iso) {
|
|
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' });
|
|
549
632
|
}
|
|
633
|
+
//# sourceMappingURL=views.js.map
|