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.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. 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
+ }