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,169 @@
|
|
|
1
|
+
// The Server drawer: the web face of the same config file the CLI wizard
|
|
2
|
+
// writes. Reads come back with the key masked; saves send only the changed
|
|
3
|
+
// keys; rotation reveals the new key once and stores it in this browser.
|
|
4
|
+
//
|
|
5
|
+
// The module owns its own DOM and its own round-trips. What it borrows from
|
|
6
|
+
// app.js is the drawer choreography — one scrim at a time, Escape to close —
|
|
7
|
+
// which is why it returns open/close/isOpen instead of wiring the topbar
|
|
8
|
+
// button itself.
|
|
9
|
+
|
|
10
|
+
import { getAccessKey, setAccessKey, fetchServerSettings, updateServerSettings, rotateServerAccessKey } from '/js/api.js';
|
|
11
|
+
|
|
12
|
+
// A function declaration, not a const arrow: the import-time DOM check in
|
|
13
|
+
// tests/frontend.test.js blanks top-level function bodies but reads const
|
|
14
|
+
// bodies, and this module must stay importable from Node.
|
|
15
|
+
function $(id) {
|
|
16
|
+
return document.getElementById(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MODE_NOTES = {
|
|
20
|
+
local: 'Local mode answers only this machine. No key, no network — the safe default.',
|
|
21
|
+
'self-hosted': 'Self-hosted answers whoever can reach the port, so every API call needs the access key below.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createServerDrawer(options = {}) {
|
|
25
|
+
// Pulled out of the parameter list on purpose: the front-end static check
|
|
26
|
+
// in tests/frontend.test.js only understands plain params, and `toast` must
|
|
27
|
+
// show up as a declaration, not a dangling call.
|
|
28
|
+
const toast = typeof options.toast === 'function' ? options.toast : () => {};
|
|
29
|
+
const dom = {};
|
|
30
|
+
[
|
|
31
|
+
'serverDrawer', 'serverScrim', 'serverClose', 'serverModeNote', 'srvMode', 'srvHost', 'srvPort',
|
|
32
|
+
'srvDomain', 'srvDomainField', 'srvAutoOpen', 'srvKeySection', 'srvKeyMasked', 'srvRotate',
|
|
33
|
+
'srvFreshKeyRow', 'srvFreshKey', 'srvCopyKey', 'srvKeyHint', 'srvName', 'srvEmail',
|
|
34
|
+
'srvTunnelCloudflare', 'srvTunnelCloudflareStatus', 'srvTunnelTailscale', 'srvTunnelTailscaleStatus',
|
|
35
|
+
'srvSave', 'srvStatus', 'srvConfigPath',
|
|
36
|
+
].forEach((id) => (dom[id] = $(id)));
|
|
37
|
+
|
|
38
|
+
let current = null; // the last settings body the server sent
|
|
39
|
+
|
|
40
|
+
function status(text, cls = '') {
|
|
41
|
+
dom.srvStatus.className = 'drawer-status' + (cls ? ' ' + cls : '');
|
|
42
|
+
dom.srvStatus.textContent = text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tunnelLine(el, t) {
|
|
46
|
+
if (!t) { el.textContent = ''; return; }
|
|
47
|
+
if (!t.enabled) {
|
|
48
|
+
el.textContent = t.installed ? 'Off.' : `Off — and ${t.install || 'the binary is not installed'}.`;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
el.textContent = t.installed
|
|
52
|
+
? `On. Start it with: ${t.command}`
|
|
53
|
+
: `On, but the binary is missing — ${t.install || 'install it first'}.`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function render(data) {
|
|
57
|
+
current = data;
|
|
58
|
+
const s = data.settings;
|
|
59
|
+
dom.srvMode.value = s.mode;
|
|
60
|
+
dom.serverModeNote.textContent = MODE_NOTES[s.mode] || '';
|
|
61
|
+
dom.srvHost.value = s.host;
|
|
62
|
+
dom.srvPort.value = s.port;
|
|
63
|
+
dom.srvDomain.value = s.domain || '';
|
|
64
|
+
dom.srvAutoOpen.checked = Boolean(s.autoOpen);
|
|
65
|
+
dom.srvName.value = s.account?.name || '';
|
|
66
|
+
dom.srvEmail.value = s.account?.email || '';
|
|
67
|
+
dom.srvTunnelCloudflare.checked = Boolean(s.tunnel?.cloudflare);
|
|
68
|
+
dom.srvTunnelTailscale.checked = Boolean(s.tunnel?.tailscale);
|
|
69
|
+
tunnelLine(dom.srvTunnelCloudflareStatus, data.tunnels?.cloudflare);
|
|
70
|
+
tunnelLine(dom.srvTunnelTailscaleStatus, data.tunnels?.tailscale);
|
|
71
|
+
dom.srvConfigPath.textContent = `Config file: ${data.configFile}`;
|
|
72
|
+
dom.srvKeyMasked.value = s.accessKeyMasked || (s.hasAccessKey ? '' : '(none set — the API refuses every call)');
|
|
73
|
+
dom.srvKeyHint.textContent = getAccessKey()
|
|
74
|
+
? 'This browser holds a key and sends it with every request.'
|
|
75
|
+
: 'This browser has no key saved. Paste one via rotate, or open the link from the startup banner.';
|
|
76
|
+
applyModeVisibility();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Mode drives which rows even make sense: local wipes the network/security
|
|
80
|
+
// fields in the schema, so asking for them here would be collecting lies.
|
|
81
|
+
function applyModeVisibility() {
|
|
82
|
+
const remote = dom.srvMode.value === 'self-hosted';
|
|
83
|
+
dom.srvKeySection.hidden = !remote;
|
|
84
|
+
dom.srvDomainField.hidden = !remote;
|
|
85
|
+
dom.serverModeNote.textContent = MODE_NOTES[dom.srvMode.value] || '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function open() {
|
|
89
|
+
dom.serverDrawer.hidden = false;
|
|
90
|
+
dom.serverScrim.hidden = false;
|
|
91
|
+
dom.srvFreshKeyRow.hidden = true;
|
|
92
|
+
status('Reading the server…');
|
|
93
|
+
try {
|
|
94
|
+
render(await fetchServerSettings());
|
|
95
|
+
status('');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
status(err.message, 'err');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function close() {
|
|
102
|
+
dom.serverDrawer.hidden = true;
|
|
103
|
+
dom.serverScrim.hidden = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async function save() {
|
|
108
|
+
const patch = {
|
|
109
|
+
mode: dom.srvMode.value,
|
|
110
|
+
host: dom.srvHost.value.trim() || '127.0.0.1',
|
|
111
|
+
port: Number(dom.srvPort.value),
|
|
112
|
+
autoOpen: dom.srvAutoOpen.checked,
|
|
113
|
+
account: { name: dom.srvName.value.trim(), email: dom.srvEmail.value.trim() },
|
|
114
|
+
tunnel: { cloudflare: dom.srvTunnelCloudflare.checked, tailscale: dom.srvTunnelTailscale.checked },
|
|
115
|
+
};
|
|
116
|
+
if (patch.mode === 'self-hosted') patch.domain = dom.srvDomain.value.trim();
|
|
117
|
+
status('Saving…');
|
|
118
|
+
try {
|
|
119
|
+
const data = await updateServerSettings(patch);
|
|
120
|
+
render({ ...current, settings: data.settings });
|
|
121
|
+
const restart = (data.restartRequired || []).join(' and ');
|
|
122
|
+
status(
|
|
123
|
+
restart
|
|
124
|
+
? `Saved. The ${restart} change takes effect after a restart — the running socket cannot move.`
|
|
125
|
+
: 'Saved — live already.',
|
|
126
|
+
'ok',
|
|
127
|
+
);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
status(err.message, 'err');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function rotate() {
|
|
134
|
+
status('Rotating…');
|
|
135
|
+
try {
|
|
136
|
+
const data = await rotateServerAccessKey();
|
|
137
|
+
// The one moment a key is ever shown. It goes straight into this
|
|
138
|
+
// browser's localStorage too, so this tab keeps working without a paste.
|
|
139
|
+
setAccessKey(data.accessKey);
|
|
140
|
+
render({ ...current, settings: data.settings });
|
|
141
|
+
dom.srvFreshKey.value = data.accessKey;
|
|
142
|
+
dom.srvFreshKeyRow.hidden = false; // render() does not know about the reveal
|
|
143
|
+
status('Rotated. The old key is dead; update every other device.', 'ok');
|
|
144
|
+
} catch (err) {
|
|
145
|
+
status(err.message, 'err');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
dom.srvMode.addEventListener('change', applyModeVisibility);
|
|
150
|
+
dom.srvSave.addEventListener('click', save);
|
|
151
|
+
dom.srvRotate.addEventListener('click', rotate);
|
|
152
|
+
dom.serverClose.addEventListener('click', close);
|
|
153
|
+
dom.serverScrim.addEventListener('click', close);
|
|
154
|
+
dom.srvCopyKey.addEventListener('click', async () => {
|
|
155
|
+
try {
|
|
156
|
+
await navigator.clipboard.writeText(dom.srvFreshKey.value);
|
|
157
|
+
toast('Key copied');
|
|
158
|
+
} catch {
|
|
159
|
+
dom.srvFreshKey.select(); // clipboard refused — select so a Ctrl+C works
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
open,
|
|
165
|
+
close,
|
|
166
|
+
isOpen: () => !dom.serverDrawer.hidden,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The application's state, in one place.
|
|
2
|
+
//
|
|
3
|
+
// This is a plain mutable object, not a reactive store, and that is deliberate.
|
|
4
|
+
// Rendering here is expensive and stateful — the canvas holds a pan/zoom
|
|
5
|
+
// transform, the code tab holds a Monaco editor — so "every mutation triggers a
|
|
6
|
+
// re-render" would be both slow and destructive. Callers still decide what to
|
|
7
|
+
// redraw. What this module does own is the state's *invariants*, which used to
|
|
8
|
+
// be maintained by convention in a 2,400-line file and are now enforced in one
|
|
9
|
+
// place that can be tested.
|
|
10
|
+
//
|
|
11
|
+
// No DOM, no imports from the view layer: importable in Node.
|
|
12
|
+
|
|
13
|
+
// The views that share the Explorer tab and its canvas.
|
|
14
|
+
export const EXPLORER_VIEWS = ['map', 'files', 'patterns', 'health', 'security', 'history', 'services', 'tour', 'atlas'];
|
|
15
|
+
|
|
16
|
+
// Views whose default inspector panel is their own, rather than the overview.
|
|
17
|
+
const PANEL_VIEWS = { patterns: 'patterns', health: 'health', security: 'security', history: 'history' };
|
|
18
|
+
|
|
19
|
+
// Per-repo state, rebuilt by `loadRepo`. Split out from the rest so "what has
|
|
20
|
+
// to be reset when a new repo arrives" is a list in one place rather than a
|
|
21
|
+
// thing you remember — forgetting an entry here is how a new repo inherits the
|
|
22
|
+
// previous one's open tabs.
|
|
23
|
+
function perRepo() {
|
|
24
|
+
return {
|
|
25
|
+
scan: null,
|
|
26
|
+
facts: null,
|
|
27
|
+
manifest: null,
|
|
28
|
+
stack: null,
|
|
29
|
+
health: null,
|
|
30
|
+
security: null,
|
|
31
|
+
history: null,
|
|
32
|
+
patterns: null,
|
|
33
|
+
treeData: null,
|
|
34
|
+
scanId: null,
|
|
35
|
+
cloneId: null,
|
|
36
|
+
browserSource: null,
|
|
37
|
+
view: 'map',
|
|
38
|
+
lastExplorer: 'map',
|
|
39
|
+
folder: '',
|
|
40
|
+
detail: null, // file open in deep-dive, if any
|
|
41
|
+
selected: null, // the focused FILE (the inspector's current subject)
|
|
42
|
+
focusedFolder: null, // the focused FOLDER subject, when it's a folder
|
|
43
|
+
tour: { stops: [], idx: 0 },
|
|
44
|
+
currentDiagram: { source: '', nodes: {} },
|
|
45
|
+
aiDiagrams: {}, // viewKey -> { body, caption } — unthemed, composed at render
|
|
46
|
+
aiActiveKey: null, // which viewKey is currently showing its AI draft
|
|
47
|
+
docs: { rendered: false, writing: false, tabs: new Map(), active: 'overview' },
|
|
48
|
+
about: { rendered: false },
|
|
49
|
+
mm: { expanded: new Set(['dir:']) }, // tree map: open folder/file cells
|
|
50
|
+
filters: { text: '', showTests: true, depth: 0 },
|
|
51
|
+
atlas: { built: false, items: [] },
|
|
52
|
+
atlasOpen: null, // an atlas card currently open on the canvas
|
|
53
|
+
code: { path: null, history: [], idx: -1, cache: {} }, // the Code tab
|
|
54
|
+
// Deep-analysis engines (semgrep, gitleaks, knip…). `status` is which are
|
|
55
|
+
// installed; `report` is the last run; `findings` merge into the security
|
|
56
|
+
// view. Only ever filled for a server-side scan — a browser-picked folder
|
|
57
|
+
// has no server-visible root for a tool to read.
|
|
58
|
+
//
|
|
59
|
+
// `install` is per-engine GUI install state ({ running, log, ok, error });
|
|
60
|
+
// `options` is the per-engine form values the Configure panel edits.
|
|
61
|
+
tools: {
|
|
62
|
+
status: null, report: null, running: false, loading: false,
|
|
63
|
+
findings: [], merged: 0, error: null,
|
|
64
|
+
install: {}, options: {},
|
|
65
|
+
},
|
|
66
|
+
// The Deep Analysis view's own controls: which findings are on screen, and
|
|
67
|
+
// whatever the AI has said about the report so far. Per-repo, so a new scan
|
|
68
|
+
// does not inherit the previous repo's filters or its explanation.
|
|
69
|
+
analysis: {
|
|
70
|
+
severity: 'all', tool: 'all', q: '',
|
|
71
|
+
ai: { text: '', error: '', running: false, question: '' },
|
|
72
|
+
},
|
|
73
|
+
history: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const state = perRepo();
|
|
78
|
+
|
|
79
|
+
// Replaces the per-repo state wholesale. Anything not listed in `perRepo()`
|
|
80
|
+
// survives, which is what you want for preferences like the theme.
|
|
81
|
+
export function loadRepo({ scan, facts, manifest, history, scanId, cloneId }, extra = {}) {
|
|
82
|
+
Object.assign(state, perRepo(), {
|
|
83
|
+
scan,
|
|
84
|
+
facts,
|
|
85
|
+
manifest: manifest || { services: [] },
|
|
86
|
+
history: history || null,
|
|
87
|
+
scanId: scanId || null,
|
|
88
|
+
cloneId: cloneId || null,
|
|
89
|
+
}, extra);
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function unloadRepo() {
|
|
94
|
+
Object.assign(state, perRepo());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Focus: a file and a folder are alternatives, never both. Every selection path
|
|
98
|
+
// in the app funnels through these, so the inspector can trust that reading
|
|
99
|
+
// `selected` first and `focusedFolder` second gives an unambiguous subject.
|
|
100
|
+
export function focusFile(path) {
|
|
101
|
+
state.selected = path;
|
|
102
|
+
state.focusedFolder = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function focusFolder(path) {
|
|
106
|
+
state.focusedFolder = path;
|
|
107
|
+
state.selected = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearFocus() {
|
|
111
|
+
state.selected = null;
|
|
112
|
+
state.focusedFolder = null;
|
|
113
|
+
state.detail = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Which subject the inspector should show, and the label for the "back" control
|
|
117
|
+
// that returns to the view's own panel. Pure function of state, so the panel
|
|
118
|
+
// and the canvas can never disagree about what is focused.
|
|
119
|
+
export function inspectorSubject(s = state) {
|
|
120
|
+
const backLabel = inspectorBackLabel(s);
|
|
121
|
+
if (s.selected && s.facts && s.facts.fanIn[s.selected] !== undefined) {
|
|
122
|
+
return { kind: 'file', path: s.selected, backLabel };
|
|
123
|
+
}
|
|
124
|
+
if (s.focusedFolder !== null && s.focusedFolder !== undefined) {
|
|
125
|
+
return { kind: 'folder', path: s.focusedFolder, backLabel };
|
|
126
|
+
}
|
|
127
|
+
if (PANEL_VIEWS[s.view]) return { kind: s.view, backLabel: null };
|
|
128
|
+
if (s.view === 'files') return { kind: 'folder', path: s.folder, backLabel };
|
|
129
|
+
return { kind: 'overview', backLabel: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function inspectorBackLabel(s = state) {
|
|
133
|
+
if (s.view === 'tour') return null; // the tour drives its own focus
|
|
134
|
+
if (s.view === 'files') return s.folder ? 'the folder' : 'the overview';
|
|
135
|
+
if (PANEL_VIEWS[s.view]) return PANEL_VIEWS[s.view];
|
|
136
|
+
return 'the overview';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isExplorerView(view) {
|
|
140
|
+
return EXPLORER_VIEWS.includes(view);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Which slot in `aiDiagrams` the current view's AI draft belongs to. Two views
|
|
144
|
+
// that show different pictures must never share a key, or one's draft appears
|
|
145
|
+
// over the other's diagram.
|
|
146
|
+
export function aiViewKey(s = state) {
|
|
147
|
+
// A tour draft belongs to the stop, not to whatever file was last selected.
|
|
148
|
+
if (s.view === 'tour') return `tour:${s.tour.idx}`;
|
|
149
|
+
if (s.view === 'atlas') {
|
|
150
|
+
return s.atlasOpen ? `atlas:${s.atlasOpen.kind}:${s.atlasOpen.path}` : 'atlas:';
|
|
151
|
+
}
|
|
152
|
+
return `${s.view}:${s.detail || s.folder || ''}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- the Code tab's history ------------------------------------------------
|
|
156
|
+
//
|
|
157
|
+
// A browser-style back/forward stack over the files opened in the Code tab.
|
|
158
|
+
// `idx` is a cursor into `history`, not always its last entry: after going back,
|
|
159
|
+
// opening a new file has to discard the entries in front of the cursor, exactly
|
|
160
|
+
// as a browser drops the forward stack. That rule is the whole reason this lives
|
|
161
|
+
// here with a test rather than inline in a render function.
|
|
162
|
+
|
|
163
|
+
// Which file the Code tab should show when it is asked to render. The chain is
|
|
164
|
+
// most-specific-first: what the tab already had, then whatever the rest of the
|
|
165
|
+
// app is looking at, then the repo's own front door, then anything at all.
|
|
166
|
+
export function defaultCodePath(s = state) {
|
|
167
|
+
const { scan, facts, code } = s;
|
|
168
|
+
// `facts.entries` holds plain path strings — `graph.js` builds it by mapping
|
|
169
|
+
// files to `f.path`. (`facts.hubs`, confusingly, holds objects.) An earlier
|
|
170
|
+
// version of this accepted either shape for `entries`; nothing has ever
|
|
171
|
+
// produced the object one, and pretending otherwise hid the asymmetry.
|
|
172
|
+
return code.path || s.detail || s.selected || facts?.entries?.[0]
|
|
173
|
+
|| scan?.files?.[0]?.path || scan?.allFiles?.[0] || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function pushCodeHistory(path, s = state) {
|
|
177
|
+
const h = s.code.history;
|
|
178
|
+
if (h[s.code.idx] === path) return; // re-opening the current file is not a move
|
|
179
|
+
h.length = s.code.idx + 1; // drop any forward entries
|
|
180
|
+
h.push(path);
|
|
181
|
+
s.code.idx = h.length - 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Moves the cursor by `delta` and reports where it landed, or null if that would
|
|
185
|
+
// step off either end. The caller opens the returned path; refusing to move and
|
|
186
|
+
// returning null is how the disabled arrow buttons stay honest.
|
|
187
|
+
export function codeHistoryTarget(delta, s = state) {
|
|
188
|
+
const next = s.code.idx + delta;
|
|
189
|
+
if (next < 0 || next >= s.code.history.length) return null;
|
|
190
|
+
s.code.idx = next;
|
|
191
|
+
return s.code.history[next];
|
|
192
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The guided walk, as the browser sees it.
|
|
2
|
+
//
|
|
3
|
+
// The ordering itself moved to `shared/analyzer/tour.js`: the MCP server hands the
|
|
4
|
+
// same list to an agent, and two copies of "which eight files should a newcomer
|
|
5
|
+
// read first" would eventually disagree. This module keeps only what is genuinely
|
|
6
|
+
// the view's — the label the tour bar draws.
|
|
7
|
+
|
|
8
|
+
import { buildTourStops, tourStopTitle } from '/shared/analyzer/tour.js';
|
|
9
|
+
|
|
10
|
+
export { buildTourStops };
|
|
11
|
+
|
|
12
|
+
export function describeStop(stop, index, total) {
|
|
13
|
+
return {
|
|
14
|
+
title: tourStopTitle(stop),
|
|
15
|
+
count: `${index + 1} / ${total}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function withTransition(callback) {
|
|
2
|
+
if (document.startViewTransition) {
|
|
3
|
+
document.startViewTransition(callback);
|
|
4
|
+
} else {
|
|
5
|
+
callback();
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setupViewTransitionNames(elements) {
|
|
10
|
+
// Utility for adding transition names if needed
|
|
11
|
+
Object.entries(elements).forEach(([id, name]) => {
|
|
12
|
+
const el = document.getElementById(id);
|
|
13
|
+
if (el) el.style.viewTransitionName = name;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Sidebar file tree. Built from the plain path list, rendered with
|
|
2
|
+
// collapsible folders, badges for entries and hubs, and a substring filter.
|
|
3
|
+
|
|
4
|
+
import { escapeHtml } from './html.js';
|
|
5
|
+
|
|
6
|
+
export function buildTree(allFiles) {
|
|
7
|
+
const root = { name: '', path: '', dirs: new Map(), files: [] };
|
|
8
|
+
for (const path of allFiles) {
|
|
9
|
+
const parts = path.split('/');
|
|
10
|
+
let node = root;
|
|
11
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
12
|
+
const name = parts[i];
|
|
13
|
+
if (!node.dirs.has(name)) {
|
|
14
|
+
const dirPath = parts.slice(0, i + 1).join('/');
|
|
15
|
+
node.dirs.set(name, { name, path: dirPath, dirs: new Map(), files: [] });
|
|
16
|
+
}
|
|
17
|
+
node = node.dirs.get(name);
|
|
18
|
+
}
|
|
19
|
+
node.files.push(path);
|
|
20
|
+
}
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The folder node at `path`, or null if no such folder exists. `''` is the root,
|
|
25
|
+
// which is a real folder here and not "no folder" — several callers pass the
|
|
26
|
+
// empty string on purpose.
|
|
27
|
+
export function findNode(root, path) {
|
|
28
|
+
if (!path) return root;
|
|
29
|
+
let node = root;
|
|
30
|
+
for (const part of path.split('/')) {
|
|
31
|
+
node = node.dirs?.get(part);
|
|
32
|
+
if (!node) return null;
|
|
33
|
+
}
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Files at or below a node. Used to rank folders by size when choosing which
|
|
38
|
+
// ones are worth documenting.
|
|
39
|
+
export function countFiles(node) {
|
|
40
|
+
if (!node) return 0;
|
|
41
|
+
let n = node.files.length;
|
|
42
|
+
for (const dir of node.dirs.values()) n += countFiles(dir);
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A copy of the tree with the filtered-out files removed, and with folders that
|
|
47
|
+
// end up holding nothing removed too — otherwise the tree map fills with empty
|
|
48
|
+
// scaffolding whose labels no longer match anything inside them.
|
|
49
|
+
//
|
|
50
|
+
// `include` is a predicate over file paths, and `matchesFolder` an optional one
|
|
51
|
+
// over folder paths: a folder whose own name matches keeps its whole branch,
|
|
52
|
+
// because searching for "lib" should show you lib's contents rather than an empty
|
|
53
|
+
// folder named lib. The root always survives, even when nothing matches, so the
|
|
54
|
+
// caller has something to render instead of null.
|
|
55
|
+
//
|
|
56
|
+
// The predicates are passed in rather than built here: this file has to stay
|
|
57
|
+
// importable in Node to be testable, which rules out reaching into `/shared/`.
|
|
58
|
+
export function pruneTree(root, include, matchesFolder = null) {
|
|
59
|
+
if (!include) return root;
|
|
60
|
+
const walk = (node) => {
|
|
61
|
+
if (node.path && matchesFolder?.(node.path)) return node;
|
|
62
|
+
const dirs = new Map();
|
|
63
|
+
for (const [name, child] of node.dirs) {
|
|
64
|
+
const pruned = walk(child);
|
|
65
|
+
if (pruned) dirs.set(name, pruned);
|
|
66
|
+
}
|
|
67
|
+
const files = node.files.filter(include);
|
|
68
|
+
if (!node.path) return { ...node, dirs, files }; // the root always survives
|
|
69
|
+
if (!dirs.size && !files.length) return null;
|
|
70
|
+
return { ...node, dirs, files };
|
|
71
|
+
};
|
|
72
|
+
return walk(root);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The set of folder ids to treat as open when the reader picks a depth from the
|
|
76
|
+
// stage's depth selector. Ids are `dir:` + path, which is the vocabulary the tree
|
|
77
|
+
// map's cells use; `dir:` alone is the root.
|
|
78
|
+
export function expandedToDepth(root, maxDepth) {
|
|
79
|
+
const out = new Set();
|
|
80
|
+
const walk = (node, depth) => {
|
|
81
|
+
if (depth > maxDepth) return;
|
|
82
|
+
out.add('dir:' + node.path);
|
|
83
|
+
for (const d of node.dirs.values()) walk(d, depth + 1);
|
|
84
|
+
};
|
|
85
|
+
walk(root, 0);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const openState = new Set();
|
|
90
|
+
|
|
91
|
+
export function resetOpenState() {
|
|
92
|
+
openState.clear();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderTree(container, tree, ctx) {
|
|
96
|
+
const { facts, filter = '', selected, onFile, onFolder } = ctx;
|
|
97
|
+
container.innerHTML = '';
|
|
98
|
+
const parsedSet = new Set(ctx.parsedPaths || []);
|
|
99
|
+
const hubSet = new Set((facts?.hubs || []).filter((h) => h.fanIn >= 4).map((h) => h.path));
|
|
100
|
+
const entrySet = new Set(facts?.entries || []);
|
|
101
|
+
const needle = filter.trim().toLowerCase();
|
|
102
|
+
|
|
103
|
+
const frag = document.createDocumentFragment();
|
|
104
|
+
|
|
105
|
+
const renderDir = (node, parent) => {
|
|
106
|
+
const dirNames = [...node.dirs.keys()].sort();
|
|
107
|
+
const filePaths = node.files.slice().sort();
|
|
108
|
+
|
|
109
|
+
for (const name of dirNames) {
|
|
110
|
+
const child = node.dirs.get(name);
|
|
111
|
+
if (needle && !subtreeHas(child, needle)) continue;
|
|
112
|
+
|
|
113
|
+
const wrap = document.createElement('div');
|
|
114
|
+
wrap.className = 'tree-dir' + (openState.has(child.path) || needle ? ' is-open' : '');
|
|
115
|
+
|
|
116
|
+
const head = document.createElement('div');
|
|
117
|
+
head.className = 'tree-dir-head';
|
|
118
|
+
const count = countFiles(child);
|
|
119
|
+
head.innerHTML = `<span class="tree-caret">▸</span><span>${escapeHtml(name)}/</span><span class="tree-count">${count}</span>`;
|
|
120
|
+
head.addEventListener('click', () => {
|
|
121
|
+
if (openState.has(child.path)) openState.delete(child.path);
|
|
122
|
+
else openState.add(child.path);
|
|
123
|
+
wrap.classList.toggle('is-open');
|
|
124
|
+
onFolder?.(child.path);
|
|
125
|
+
});
|
|
126
|
+
wrap.appendChild(head);
|
|
127
|
+
|
|
128
|
+
const kids = document.createElement('div');
|
|
129
|
+
kids.className = 'tree-children';
|
|
130
|
+
renderDir(child, kids);
|
|
131
|
+
wrap.appendChild(kids);
|
|
132
|
+
parent.appendChild(wrap);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const path of filePaths) {
|
|
136
|
+
const name = path.split('/').pop();
|
|
137
|
+
if (needle && !path.toLowerCase().includes(needle)) continue;
|
|
138
|
+
const el = document.createElement('div');
|
|
139
|
+
el.className = 'tree-file' + (path === selected ? ' is-selected' : '');
|
|
140
|
+
el.dataset.path = path;
|
|
141
|
+
|
|
142
|
+
const dot = document.createElement('span');
|
|
143
|
+
dot.className = 'tree-dot';
|
|
144
|
+
dot.style.backgroundColor = getExtColor(path);
|
|
145
|
+
el.appendChild(dot);
|
|
146
|
+
|
|
147
|
+
const label = document.createElement('span');
|
|
148
|
+
label.textContent = name;
|
|
149
|
+
if (!parsedSet.has(path)) label.style.color = 'var(--faint)';
|
|
150
|
+
el.appendChild(label);
|
|
151
|
+
|
|
152
|
+
if (entrySet.has(path)) {
|
|
153
|
+
el.appendChild(badge('in', 'is-entry'));
|
|
154
|
+
} else if (hubSet.has(path)) {
|
|
155
|
+
el.appendChild(badge('hub', 'is-hub'));
|
|
156
|
+
}
|
|
157
|
+
el.addEventListener('click', () => onFile?.(path));
|
|
158
|
+
parent.appendChild(el);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
renderDir(tree, frag);
|
|
163
|
+
container.appendChild(frag);
|
|
164
|
+
|
|
165
|
+
if (!container.children.length) {
|
|
166
|
+
container.innerHTML = '<p style="padding:10px;color:var(--faint)">Nothing matches that filter.</p>';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function subtreeHas(node, needle) {
|
|
171
|
+
if (node.path.toLowerCase().includes(needle)) return true;
|
|
172
|
+
for (const f of node.files) if (f.toLowerCase().includes(needle)) return true;
|
|
173
|
+
for (const child of node.dirs.values()) if (subtreeHas(child, needle)) return true;
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function badge(text, cls) {
|
|
178
|
+
const b = document.createElement('span');
|
|
179
|
+
b.className = 'tree-badge ' + cls;
|
|
180
|
+
b.textContent = text;
|
|
181
|
+
return b;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getExtColor(path) {
|
|
185
|
+
const ext = (path.match(/\.([A-Za-z0-9]+)$/) || [, ''])[1].toLowerCase();
|
|
186
|
+
switch (ext) {
|
|
187
|
+
case 'ts': case 'tsx': return '#3178c6';
|
|
188
|
+
case 'js': case 'jsx': case 'mjs': case 'cjs': return '#f7df1e';
|
|
189
|
+
case 'py': return '#3572A5';
|
|
190
|
+
case 'rs': return '#dea584';
|
|
191
|
+
case 'go': return '#00ADD8';
|
|
192
|
+
case 'java': return '#b07219';
|
|
193
|
+
case 'cs': return '#178600';
|
|
194
|
+
case 'html': return '#e34c26';
|
|
195
|
+
case 'css': return '#563d7c';
|
|
196
|
+
case 'json': return '#cbcb41';
|
|
197
|
+
case 'md': return '#083fa1';
|
|
198
|
+
default: return 'var(--faint)';
|
|
199
|
+
}
|
|
200
|
+
}
|