codebase-onboarder 0.1.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/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/onboarder.js +14 -0
- package/bin/postinstall.js +6 -0
- package/cli/commands.js +436 -0
- package/cli/main.js +129 -0
- package/cli/prompt.js +101 -0
- package/cli/ui.js +55 -0
- package/cli/wizard.js +325 -0
- package/package.json +48 -0
- package/public/app.js +1339 -0
- package/public/index.html +455 -0
- package/public/js/about.js +284 -0
- package/public/js/aiDraft.js +163 -0
- package/public/js/analysisPanel.js +270 -0
- package/public/js/analysisReport.js +154 -0
- package/public/js/api.js +264 -0
- package/public/js/atlas.js +98 -0
- package/public/js/blameView.js +41 -0
- package/public/js/codeTab.js +256 -0
- package/public/js/codeViewer.js +184 -0
- package/public/js/components/FileChip.js +102 -0
- package/public/js/components/MetricSparkline.js +89 -0
- package/public/js/components/RiskBadge.js +105 -0
- package/public/js/deepAnalysisView.js +496 -0
- package/public/js/diagramPane.js +154 -0
- package/public/js/diffView.js +352 -0
- package/public/js/docsView.js +566 -0
- package/public/js/fileSourceBrowser.js +47 -0
- package/public/js/flameGraph.js +75 -0
- package/public/js/forceGraph.js +592 -0
- package/public/js/heatmap.js +185 -0
- package/public/js/highlight.js +122 -0
- package/public/js/html.js +50 -0
- package/public/js/insightsView.js +236 -0
- package/public/js/inspector.js +535 -0
- package/public/js/llm.js +345 -0
- package/public/js/mapView.js +159 -0
- package/public/js/markdown.js +119 -0
- package/public/js/mindmap.js +289 -0
- package/public/js/repoFiles.js +44 -0
- package/public/js/sbomView.js +142 -0
- package/public/js/scanCache.js +76 -0
- package/public/js/search.js +874 -0
- package/public/js/serverSettings.js +169 -0
- package/public/js/state.js +192 -0
- package/public/js/tour.js +17 -0
- package/public/js/transitions.js +15 -0
- package/public/js/tree.js +200 -0
- package/public/js/workflowsView.js +95 -0
- package/public/styles.css +2767 -0
- package/public/vendor/mermaid.min.js +3587 -0
- package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/vendor/monaco/vs/loader.js +11 -0
- package/public/vendor/monaco/worker-boot.js +9 -0
- package/server/.fuse_hidden0000000800000001 +36 -0
- package/server/apiDiff.js +25 -0
- package/server/apiDocs.js +67 -0
- package/server/apiFile.js +36 -0
- package/server/apiGitBlame.js +63 -0
- package/server/apiMcp.js +110 -0
- package/server/apiScan.js +119 -0
- package/server/apiSearch.js +253 -0
- package/server/apiSettings.js +118 -0
- package/server/apiTools.js +90 -0
- package/server/config.js +227 -0
- package/server/fileSourceNode.js +44 -0
- package/server/gitClone.js +95 -0
- package/server/gitDiff.js +184 -0
- package/server/gitHistory.js +110 -0
- package/server/htmlText.js +44 -0
- package/server/http.js +55 -0
- package/server/httpGuards.js +87 -0
- package/server/index.js +156 -0
- package/server/llmProxy.js +162 -0
- package/server/logger.js +29 -0
- package/server/mcp/analysis.js +209 -0
- package/server/mcp/http.js +213 -0
- package/server/mcp/runner.js +278 -0
- package/server/mcp/server.js +241 -0
- package/server/mcp/standalone.js +42 -0
- package/server/mcp/tools.js +683 -0
- package/server/paths.js +39 -0
- package/server/router.js +218 -0
- package/server/searchIndex.js +118 -0
- package/server/sessions.js +163 -0
- package/server/static.js +59 -0
- package/server/tools/install.js +246 -0
- package/server/tools/parse.js +170 -0
- package/server/tools/platform.js +91 -0
- package/server/tools/registry.js +212 -0
- package/server/tools/scan.js +136 -0
- package/server/tools.js +212 -0
- package/server/tunnel.js +92 -0
- package/shared/analyzer/docs.js +135 -0
- package/shared/analyzer/explainLocal.js +163 -0
- package/shared/analyzer/graph.js +461 -0
- package/shared/analyzer/health.js +215 -0
- package/shared/analyzer/history.js +146 -0
- package/shared/analyzer/languages/csharp.js +39 -0
- package/shared/analyzer/languages/generic.js +131 -0
- package/shared/analyzer/languages/go.js +70 -0
- package/shared/analyzer/languages/index.js +43 -0
- package/shared/analyzer/languages/java.js +39 -0
- package/shared/analyzer/languages/javascript.js +240 -0
- package/shared/analyzer/languages/python.js +120 -0
- package/shared/analyzer/languages/rust.js +42 -0
- package/shared/analyzer/languages/typescript.js +138 -0
- package/shared/analyzer/licenses.js +151 -0
- package/shared/analyzer/metrics.js +120 -0
- package/shared/analyzer/pathUtil.js +69 -0
- package/shared/analyzer/patterns.js +272 -0
- package/shared/analyzer/scan.js +473 -0
- package/shared/analyzer/security.js +187 -0
- package/shared/analyzer/services.js +157 -0
- package/shared/analyzer/stack.js +173 -0
- package/shared/analyzer/tour.js +69 -0
- package/shared/analyzer/util.js +123 -0
- package/shared/analyzer/workflows.js +143 -0
- package/shared/diagram/aiFacts.js +145 -0
- package/shared/diagram/aiMermaid.js +59 -0
- package/shared/diagram/atlas.js +93 -0
- package/shared/diagram/mermaid.js +459 -0
- package/shared/search/query.js +518 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// The About view: what this repo is, what it's built from, and how the scan
|
|
2
|
+
// that produced this page actually went.
|
|
3
|
+
//
|
|
4
|
+
// The rendering needs the DOM, but the parts worth checking — the coverage
|
|
5
|
+
// prose, the GitHub URL parsing, date formatting — are pure and exported for
|
|
6
|
+
// that reason. Nothing here runs at import time.
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from './html.js';
|
|
9
|
+
import { state } from './state.js';
|
|
10
|
+
import { streamExplain } from './api.js';
|
|
11
|
+
import { mdLite } from './markdown.js';
|
|
12
|
+
import * as llm from './llm.js';
|
|
13
|
+
|
|
14
|
+
let host = null;
|
|
15
|
+
let hooks = { onToast: () => {}, onOpenSettings: () => {}, repoOverview: () => '' };
|
|
16
|
+
|
|
17
|
+
export function initAbout(options) {
|
|
18
|
+
host = options.host;
|
|
19
|
+
hooks = { ...hooks, ...options };
|
|
20
|
+
host.addEventListener('click', (event) => {
|
|
21
|
+
const b = event.target.closest('[data-stack-sum]');
|
|
22
|
+
if (b) summarizeStackItem(b.dataset.stackSum);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderAbout() {
|
|
27
|
+
if (state.about.rendered) return;
|
|
28
|
+
state.about.rendered = true;
|
|
29
|
+
const { scan, facts } = state;
|
|
30
|
+
const gh = scan.gitUrl ? githubRepoPath(scan.gitUrl) : null;
|
|
31
|
+
|
|
32
|
+
const sourceLine = scan.gitUrl
|
|
33
|
+
? `Cloned from ${scan.gitUrl}`
|
|
34
|
+
: state.browserSource
|
|
35
|
+
? 'Picked in the browser — files never left this tab.'
|
|
36
|
+
: `Read in place from ${scan.root}`;
|
|
37
|
+
|
|
38
|
+
host.innerHTML = `
|
|
39
|
+
<div class="doc-page">
|
|
40
|
+
<h1 class="doc-title">${escapeHtml(scan.name)}</h1>
|
|
41
|
+
<p class="doc-sub">${escapeHtml(scan.tempId ? scan.tempId + ' — a temp clone' : scan.root)}</p>
|
|
42
|
+
<p class="doc-sub">${escapeHtml(sourceLine)}</p>
|
|
43
|
+
|
|
44
|
+
<div class="about-numbers">
|
|
45
|
+
${aboutNum(scan.stats.filesParsed, 'code files')}
|
|
46
|
+
${aboutNum(scan.stats.edgeCount, 'connections')}
|
|
47
|
+
${aboutNum(facts.entries.length, 'entry points')}
|
|
48
|
+
${aboutNum(facts.hubs.length, 'hubs')}
|
|
49
|
+
${aboutNum(facts.cycles.length, 'cycles')}
|
|
50
|
+
${aboutNum(Object.keys(scan.stats.languages).length, 'languages')}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<h2 class="doc-section-h">Stack</h2>
|
|
54
|
+
<div id="stackZone"></div>
|
|
55
|
+
|
|
56
|
+
${renderHistorySection(state.history)}
|
|
57
|
+
|
|
58
|
+
<h2 class="doc-section-h">From the remote</h2>
|
|
59
|
+
<div id="aboutRemote">${
|
|
60
|
+
gh
|
|
61
|
+
? '<span class="doc-readme-none">Asking GitHub…</span>'
|
|
62
|
+
: `<span class="doc-readme-none">${scan.gitUrl ? 'Not a GitHub URL — no remote facts to fetch.' : 'A local repo — no remote to ask about.'}</span>`
|
|
63
|
+
}</div>
|
|
64
|
+
|
|
65
|
+
<h2 class="doc-section-h">Lineage</h2>
|
|
66
|
+
<ul class="about-lineage">
|
|
67
|
+
<li>Scanned ${new Date(scan.scannedAt).toLocaleString()} in ${scan.stats.tookMs} ms.</li>
|
|
68
|
+
${lineageCoverage(scan)}
|
|
69
|
+
${scan.tempId ? `<li>The clone lives in your temp folder as <code>${escapeHtml(scan.tempId)}</code> until you hit New repo.</li>` : ''}
|
|
70
|
+
</ul>
|
|
71
|
+
</div>`;
|
|
72
|
+
|
|
73
|
+
if (gh) loadRemoteInfo(gh);
|
|
74
|
+
if (state.stack) renderStackZone(host.querySelector('#stackZone'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderHistorySection(h) {
|
|
78
|
+
if (!h) return '';
|
|
79
|
+
if (!h.available) {
|
|
80
|
+
return `<h2 class="doc-section-h">History</h2><p class="doc-readme-none">${escapeHtml(h.reason || 'No history available.')}</p>`;
|
|
81
|
+
}
|
|
82
|
+
return `
|
|
83
|
+
<h2 class="doc-section-h">History & Activity</h2>
|
|
84
|
+
<div class="about-numbers">
|
|
85
|
+
${aboutNum(h.commitCount, h.truncated ? 'commits analyzed' : 'commits')}
|
|
86
|
+
${aboutNum(h.authors.length, 'contributors')}
|
|
87
|
+
${aboutNum(h.soloFiles, 'solo-author files')}
|
|
88
|
+
${aboutNum(h.coChanged.length, 'co-change pairs')}
|
|
89
|
+
</div>
|
|
90
|
+
<ul class="about-lineage">
|
|
91
|
+
${h.firstCommitAt ? `<li>First commit: <b>${aboutDate(h.firstCommitAt)}</b> · Latest: <b>${aboutDate(h.lastCommitAt)}</b></li>` : ''}
|
|
92
|
+
${h.authors.length ? `<li>Top contributors: ${h.authors.slice(0, 5).map((a) => `<b>${escapeHtml(a.name || a.email)}</b> (${a.commits})`).join(', ')}.</li>` : ''}
|
|
93
|
+
</ul>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- Stack: detected languages, package managers, and frameworks ----------
|
|
98
|
+
|
|
99
|
+
function renderStackZone(zone) {
|
|
100
|
+
const st = state.stack;
|
|
101
|
+
if (!st) return (zone.innerHTML = '');
|
|
102
|
+
|
|
103
|
+
const langs = Object.entries(st.languages)
|
|
104
|
+
.map(([name, count]) => `<span class="stack-lang"><i>${count}</i> ${escapeHtml(name)}</span>`)
|
|
105
|
+
.join('');
|
|
106
|
+
|
|
107
|
+
const rows = st.items
|
|
108
|
+
.map(
|
|
109
|
+
(it) => `
|
|
110
|
+
<div class="stack-item">
|
|
111
|
+
<span class="stack-name">${escapeHtml(it.name)}</span>
|
|
112
|
+
<span class="stack-cat">${escapeHtml(it.category)}</span>
|
|
113
|
+
${it.version ? `<span class="stack-ver">v${escapeHtml(it.version)}</span>` : ''}
|
|
114
|
+
${it.dev ? '<span class="stack-dev">dev</span>' : ''}
|
|
115
|
+
<span class="stack-lang">${escapeHtml(it.lang)}</span>
|
|
116
|
+
<a class="stack-doc" href="${escapeHtml(it.docs)}" target="_blank" rel="noopener">docs ↗</a>
|
|
117
|
+
<button class="stack-sum" data-stack-sum="${escapeHtml(it.name)}" title="Summarize from official docs">summarize</button>
|
|
118
|
+
<div class="stack-body" data-stack-body></div>
|
|
119
|
+
</div>`
|
|
120
|
+
)
|
|
121
|
+
.join('');
|
|
122
|
+
|
|
123
|
+
zone.innerHTML = `
|
|
124
|
+
<div class="stack-block">
|
|
125
|
+
<div class="stack-langs">${langs || '<span class="doc-readme-none">No languages parsed.</span>'}</div>
|
|
126
|
+
${st.pm.length ? `<p class="stack-pm">package managers: ${escapeHtml(st.pm.join(', '))}</p>` : ''}
|
|
127
|
+
${st.items.length
|
|
128
|
+
? `<div class="stack-grid">${rows}</div>
|
|
129
|
+
<p class="stack-hint">Each “summarize” fetches that library’s official docs and folds them into an AI field note.</p>`
|
|
130
|
+
: '<p class="doc-readme-none">No declared dependencies — nothing to analyze.</p>'}
|
|
131
|
+
</div>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fetch a library's official docs (server-side, SSRF-safe) and summarize it
|
|
135
|
+
// with the AI, streamed into the row.
|
|
136
|
+
async function summarizeStackItem(name) {
|
|
137
|
+
const item = state.stack.items.find((i) => i.name === name);
|
|
138
|
+
if (!item) return hooks.onToast('Unknown dependency.');
|
|
139
|
+
if (!llm.isConfigured()) {
|
|
140
|
+
hooks.onOpenSettings();
|
|
141
|
+
return hooks.onToast('Add an endpoint and model first.');
|
|
142
|
+
}
|
|
143
|
+
const row = host.querySelector(`[data-stack-sum="${CSS.escape(name)}"]`)?.closest('.stack-item');
|
|
144
|
+
const body = row?.querySelector('[data-stack-body]');
|
|
145
|
+
if (!body) return;
|
|
146
|
+
|
|
147
|
+
row.querySelector('.stack-sum').textContent = 'looking…';
|
|
148
|
+
row.querySelector('.stack-sum').disabled = true;
|
|
149
|
+
|
|
150
|
+
let docsText = '';
|
|
151
|
+
let docsTitle = '';
|
|
152
|
+
try {
|
|
153
|
+
const r = await fetch('/api/doc', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({ url: item.docs }),
|
|
157
|
+
});
|
|
158
|
+
const j = await r.json();
|
|
159
|
+
if (j.text) {
|
|
160
|
+
docsText = j.text;
|
|
161
|
+
docsTitle = j.title || item.docs;
|
|
162
|
+
}
|
|
163
|
+
} catch { /* summarized from knowledge */ }
|
|
164
|
+
|
|
165
|
+
body.innerHTML = '<br><div class="stack-body-text"><span class="caret"></span></div>';
|
|
166
|
+
const slot = body.querySelector('.stack-body-text');
|
|
167
|
+
const settings = llm.getSettings();
|
|
168
|
+
const messages = llm.stackSummaryMessages({
|
|
169
|
+
repoName: state.scan.name,
|
|
170
|
+
item,
|
|
171
|
+
docsText,
|
|
172
|
+
repoNote: hooks.repoOverview(),
|
|
173
|
+
});
|
|
174
|
+
let text = '';
|
|
175
|
+
try {
|
|
176
|
+
for await (const delta of streamExplain({ ...settings, messages, maxTokens: 700 })) {
|
|
177
|
+
text += delta;
|
|
178
|
+
slot.innerHTML = mdLite(text) + '<span class="caret"></span>';
|
|
179
|
+
}
|
|
180
|
+
slot.innerHTML = mdLite(text)
|
|
181
|
+
+ `<p class="explain-src">${docsText ? `From ${escapeHtml(docsTitle)}` : 'No docs fetched — general knowledge'} · ${settings.model}</p>`;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
slot.innerHTML = `<p style="color:var(--warn)">${escapeHtml(err.message)}</p>`;
|
|
184
|
+
} finally {
|
|
185
|
+
const btn = row.querySelector('.stack-sum');
|
|
186
|
+
if (btn) {
|
|
187
|
+
btn.textContent = body.textContent.trim() ? 'summarize again' : 'summarize';
|
|
188
|
+
btn.disabled = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- the pure parts -------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export function aboutNum(value, label) {
|
|
196
|
+
return `<div class="about-num"><b>${value}</b><span>${label}</span></div>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// What the scan left out, itemized. The line here used to read "N files skipped
|
|
200
|
+
// by the ignore rules", which was one number covering eight unrelated reasons —
|
|
201
|
+
// most of them not the ignore rules at all.
|
|
202
|
+
export function lineageCoverage(scan) {
|
|
203
|
+
const s = scan.stats;
|
|
204
|
+
const k = s.skips || {};
|
|
205
|
+
const out = [];
|
|
206
|
+
|
|
207
|
+
if (s.truncated) {
|
|
208
|
+
out.push(
|
|
209
|
+
`<li><b>Partial scan.</b> Stopped at the ${s.truncated.atFiles}-file cap with ` +
|
|
210
|
+
`${s.truncated.dirsQueued} ${s.truncated.dirsQueued === 1 ? 'directory' : 'directories'} ` +
|
|
211
|
+
`still unvisited — every count below is a floor, not a total.</li>`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const reasons = [
|
|
216
|
+
[k.ignored, 'matched .gitignore'],
|
|
217
|
+
[k.vendorDir, 'in vendor or build folders'],
|
|
218
|
+
[k.notSource, 'not source (images, lockfiles, bundles)'],
|
|
219
|
+
[k.notCode, 'no analyzer for the extension'],
|
|
220
|
+
[k.tooLarge, 'over the size cap'],
|
|
221
|
+
[k.readFailed, 'could not be read'],
|
|
222
|
+
[k.analyzeFailed, 'could not be parsed'],
|
|
223
|
+
[k.listFailed, 'directories that would not list'],
|
|
224
|
+
].filter(([n]) => n > 0).map(([n, why]) => `${n} ${why}`);
|
|
225
|
+
if (reasons.length) {
|
|
226
|
+
out.push(`<li>${s.filesParsed} of ${s.filesTotal} files analyzed. Left out: ${reasons.join('; ')}.</li>`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const imp = s.imports;
|
|
230
|
+
if (imp?.total) {
|
|
231
|
+
const worst = imp.worst?.length
|
|
232
|
+
? ` Most common misses: ${imp.worst.slice(0, 3).map((w) => `<code>${escapeHtml(w.spec)}</code>`).join(', ')}.`
|
|
233
|
+
: '';
|
|
234
|
+
out.push(
|
|
235
|
+
`<li><b>${imp.confidence}% of ${imp.total} imports placed</b> — ${imp.internal} internal, ` +
|
|
236
|
+
`${imp.external} external packages, ${imp.unresolved} unresolved.${imp.unresolved ? worst : ''}</li>`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return out.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Both URL shapes git accepts: https://github.com/owner/repo(.git) and
|
|
243
|
+
// git@github.com:owner/repo(.git). Anything else is not GitHub and gets no
|
|
244
|
+
// remote lookup.
|
|
245
|
+
export function githubRepoPath(url) {
|
|
246
|
+
const m = String(url).match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/);
|
|
247
|
+
return m ? m[1] : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function aboutDate(iso) {
|
|
251
|
+
if (!iso) return 'unknown';
|
|
252
|
+
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function loadRemoteInfo(repoPath) {
|
|
256
|
+
const box = host.querySelector('#aboutRemote');
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(`https://api.github.com/repos/${repoPath}`);
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
throw new Error(res.status === 403
|
|
261
|
+
? 'GitHub rate-limited the ask — unauthenticated allowance is 60 an hour.'
|
|
262
|
+
: `GitHub answered ${res.status}.`);
|
|
263
|
+
}
|
|
264
|
+
const r = await res.json();
|
|
265
|
+
box.innerHTML = `
|
|
266
|
+
<div class="about-numbers">
|
|
267
|
+
${aboutNum(r.stargazers_count ?? 0, 'stars')}
|
|
268
|
+
${aboutNum(r.forks_count ?? 0, 'forks')}
|
|
269
|
+
${aboutNum(r.watchers_count ?? 0, 'watching')}
|
|
270
|
+
${aboutNum(r.open_issues_count ?? 0, 'open issues')}
|
|
271
|
+
</div>
|
|
272
|
+
${r.description ? `<p class="about-desc">${escapeHtml(r.description)}</p>` : ''}
|
|
273
|
+
<ul class="about-lineage">
|
|
274
|
+
${r.license?.spdx_id ? `<li>License: ${escapeHtml(r.license.spdx_id)}</li>` : ''}
|
|
275
|
+
<li>Created ${aboutDate(r.created_at)} · last push ${aboutDate(r.pushed_at)}.</li>
|
|
276
|
+
${r.default_branch ? `<li>Default branch: <code>${escapeHtml(r.default_branch)}</code></li>` : ''}
|
|
277
|
+
${r.topics?.length ? `<li>Topics: ${r.topics.map((t) => `<span class="stat-chip">${escapeHtml(t)}</span>`).join(' ')}</li>` : ''}
|
|
278
|
+
${r.homepage ? `<li><a href="${escapeHtml(r.homepage)}" target="_blank" rel="noopener noreferrer">${escapeHtml(r.homepage)}</a></li>` : ''}
|
|
279
|
+
<li class="doc-readme-none">GitHub keeps view counts private to the repo's owner — “watching” is the closest public number.</li>
|
|
280
|
+
</ul>`;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
box.innerHTML = `<span class="doc-readme-none">${escapeHtml(err.message)}</span>`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// The "AI draft" button: ask a model to redraw whatever the canvas is showing.
|
|
2
|
+
//
|
|
3
|
+
// Every view has a static diagram built from the import graph, and that is what
|
|
4
|
+
// loads. This is the optional second opinion — the model gets a fact sheet (see
|
|
5
|
+
// aiFacts.js) and answers with Mermaid, which is cached per view so flipping
|
|
6
|
+
// between the two is instant afterwards.
|
|
7
|
+
//
|
|
8
|
+
// Three states for one button: draw, drawn (flip back to static), and drawing.
|
|
9
|
+
// Nothing here is required for the app to work; with no endpoint configured the
|
|
10
|
+
// button opens the settings drawer and says so.
|
|
11
|
+
|
|
12
|
+
import { extractAIDiagram, matchNodeText } from '/shared/diagram/aiMermaid.js';
|
|
13
|
+
import { streamExplain } from './api.js';
|
|
14
|
+
import { state, aiViewKey } from './state.js';
|
|
15
|
+
import { gatherFileContext } from './repoFiles.js';
|
|
16
|
+
import {
|
|
17
|
+
diagramTarget, externalsFor, overviewFacts, layersFacts, servicesFacts,
|
|
18
|
+
fileFacts, tourStopFacts, folderFacts,
|
|
19
|
+
} from '/shared/diagram/aiFacts.js';
|
|
20
|
+
import * as llm from './llm.js';
|
|
21
|
+
|
|
22
|
+
const MAX_TOKENS = 4000;
|
|
23
|
+
const PROGRESS_MS = 250;
|
|
24
|
+
|
|
25
|
+
let btn = null;
|
|
26
|
+
let statusEl = null;
|
|
27
|
+
let nodeHost = null;
|
|
28
|
+
let hooks = {
|
|
29
|
+
onRender: () => {},
|
|
30
|
+
onOpenFile: () => {},
|
|
31
|
+
onOpenSettings: () => {},
|
|
32
|
+
onToast: () => {},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function initAiDraft(options) {
|
|
36
|
+
btn = options.host;
|
|
37
|
+
statusEl = options.statusEl;
|
|
38
|
+
nodeHost = options.nodeHost;
|
|
39
|
+
hooks = { ...hooks, ...options };
|
|
40
|
+
|
|
41
|
+
btn.addEventListener('click', () => {
|
|
42
|
+
const key = aiViewKey();
|
|
43
|
+
if (state.aiActiveKey === key) {
|
|
44
|
+
state.aiActiveKey = null; // back to static
|
|
45
|
+
hooks.onRender();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (state.aiDiagrams[key]) {
|
|
49
|
+
state.aiActiveKey = key; // cached draft, instant
|
|
50
|
+
hooks.onRender();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
drawWithAI(key);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Which of the three things the button currently offers. Called on every canvas
|
|
58
|
+
// render, because the answer depends on the view.
|
|
59
|
+
export function updateAiBtn(key = aiViewKey()) {
|
|
60
|
+
btn.hidden = state.view === 'docs' || state.view === 'about'
|
|
61
|
+
|| (state.view === 'atlas' && !state.atlasOpen);
|
|
62
|
+
if (state.aiActiveKey === key) {
|
|
63
|
+
btn.textContent = 'Static';
|
|
64
|
+
btn.title = 'Back to the static diagram';
|
|
65
|
+
} else {
|
|
66
|
+
btn.textContent = state.aiDiagrams[key] ? 'AI draft ✓' : 'AI draft';
|
|
67
|
+
btn.title = 'Ask the AI to draw this view';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// AI nodes carry no stable ids — clicks are matched by label text instead.
|
|
72
|
+
export function wireAIClicks() {
|
|
73
|
+
for (const g of nodeHost.querySelectorAll('g.node')) {
|
|
74
|
+
const path = matchNodeText(g.textContent, state.scan.files);
|
|
75
|
+
if (!path) continue;
|
|
76
|
+
g.style.cursor = 'pointer';
|
|
77
|
+
g.addEventListener('click', (event) => {
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
hooks.onOpenFile(path);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function drawWithAI(key) {
|
|
85
|
+
if (!llm.isConfigured()) {
|
|
86
|
+
hooks.onOpenSettings();
|
|
87
|
+
hooks.onToast('Add an endpoint and model first.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const settings = llm.getSettings();
|
|
91
|
+
btn.disabled = true;
|
|
92
|
+
btn.textContent = 'drawing…';
|
|
93
|
+
statusEl.hidden = false;
|
|
94
|
+
|
|
95
|
+
// Live progress: elapsed time plus how much the model has written so far. A
|
|
96
|
+
// reasoning model can be quiet for half a minute, and a canvas that says
|
|
97
|
+
// nothing is indistinguishable from one that has hung.
|
|
98
|
+
const startedAt = Date.now();
|
|
99
|
+
let received = 0;
|
|
100
|
+
const tick = () => {
|
|
101
|
+
const secs = Math.round((Date.now() - startedAt) / 1000);
|
|
102
|
+
const kb = received >= 1000 ? (received / 1000).toFixed(1) + 'k' : String(received);
|
|
103
|
+
statusEl.textContent = `The AI is sketching this view… ${kb} chars · ${secs}s`;
|
|
104
|
+
};
|
|
105
|
+
tick();
|
|
106
|
+
const progressTimer = setInterval(tick, PROGRESS_MS);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const { kind, facts } = await factsForView();
|
|
110
|
+
const messages = llm.diagramMessages({ repoName: state.scan.name, kind, facts });
|
|
111
|
+
let text = '';
|
|
112
|
+
// Big budget, and no visible thinking where the dialect allows it:
|
|
113
|
+
// reasoning models otherwise spend the whole allowance before a single
|
|
114
|
+
// diagram line appears. Azure rejects that parameter outright.
|
|
115
|
+
const providerOptions = llm.isAzureHost(settings.baseUrl) ? {} : { reasoning: { exclude: true } };
|
|
116
|
+
for await (const delta of streamExplain({ ...settings, messages, maxTokens: MAX_TOKENS, providerOptions })) {
|
|
117
|
+
text += delta;
|
|
118
|
+
received += delta.length;
|
|
119
|
+
}
|
|
120
|
+
const parsed = extractAIDiagram(text);
|
|
121
|
+
if (!parsed) throw new Error('The model answered without a diagram. Try again.');
|
|
122
|
+
state.aiDiagrams[key] = { body: parsed.body, caption: parsed.caption };
|
|
123
|
+
state.aiActiveKey = key;
|
|
124
|
+
statusEl.hidden = true;
|
|
125
|
+
hooks.onRender();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
statusEl.hidden = true;
|
|
128
|
+
hooks.onToast(err.message);
|
|
129
|
+
} finally {
|
|
130
|
+
clearInterval(progressTimer);
|
|
131
|
+
btn.disabled = false;
|
|
132
|
+
updateAiBtn(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Picks the sheet for the current view and fetches the source the file-shaped
|
|
137
|
+
// ones need. The choosing is pure and tested in aiFacts.js; this is the read.
|
|
138
|
+
async function factsForView() {
|
|
139
|
+
const { scan, facts, manifest, patterns } = state;
|
|
140
|
+
const target = diagramTarget(state);
|
|
141
|
+
|
|
142
|
+
switch (target.view) {
|
|
143
|
+
case 'map': return overviewFacts(scan, facts);
|
|
144
|
+
case 'patterns': return layersFacts(facts, patterns);
|
|
145
|
+
case 'services': return servicesFacts(scan, manifest);
|
|
146
|
+
case 'tour': {
|
|
147
|
+
const stop = state.tour.stops[state.tour.idx];
|
|
148
|
+
if (!stop) return tourStopFacts({ stop: null });
|
|
149
|
+
return tourStopFacts({
|
|
150
|
+
ctx: await gatherFileContext(target.path),
|
|
151
|
+
externals: externalsFor(scan, target.path),
|
|
152
|
+
stop,
|
|
153
|
+
index: state.tour.idx,
|
|
154
|
+
total: state.tour.stops.length,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
case 'file': return fileFacts({
|
|
158
|
+
ctx: await gatherFileContext(target.path),
|
|
159
|
+
externals: externalsFor(scan, target.path),
|
|
160
|
+
});
|
|
161
|
+
default: return folderFacts(scan, facts, target.path);
|
|
162
|
+
}
|
|
163
|
+
}
|