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,146 @@
1
+ // Git history analysis: churn, authorship, the hotspot cross-tab, and
2
+ // temporal co-change coupling. Pure — the commits arrive as plain data from an
3
+ // injected provider (the server's `gitLog`), exactly as `scanRepo` takes files
4
+ // through `FileSource`. Nothing here knows git exists.
5
+ //
6
+ // The unit of history is a commit: { hash, author: { name, email }, date, files }.
7
+ // Everything is keyed on file path, which is already the join key the rest of
8
+ // the engine speaks — complexity comes over from scan.files, structural edges
9
+ // from scan.edges, and the two views of "these change together" sit side by side.
10
+
11
+ import { baseName } from './pathUtil.js';
12
+
13
+ // Commits touching more files than this are skipped for pair counting, not for
14
+ // churn — a 900-file reformat or a mass rename would otherwise contribute
15
+ // C(900,2) ≈ 400k pairs of noise to co-change while saying nothing about
16
+ // coupling. Churn still counts them; every file in them was really touched.
17
+ const MAX_COMMIT_FILES_FOR_PAIRS = 50;
18
+
19
+ // Once this many distinct pairs are tracked, new pairs stop being recorded and
20
+ // only existing ones keep counting. A hard bound on memory for repos with long
21
+ // histories of small commits; the top of the list is decided well before this.
22
+ const MAX_TRACKED_PAIRS = 5000;
23
+
24
+ export function analyzeHistory(scan, commits, opts = {}) {
25
+ const totalCommits = opts.totalCommits ?? commits.length;
26
+ const truncated = totalCommits > commits.length;
27
+
28
+ // ---- per-file rollup -----------------------------------------------------
29
+ // One pass over every commit's file list. Authors are identified by email —
30
+ // names collide and change; the email is what git itself keys identity on.
31
+ const churn = new Map(); // path -> commit count
32
+ const authorsOf = new Map(); // path -> Map(email -> display name)
33
+ const firstSeen = new Map(); // path -> earliest commit date (ISO strings sort)
34
+ const lastTouched = new Map();
35
+ const authorCommits = new Map(); // email -> { name, commits }
36
+ let pathsGone = 0;
37
+
38
+ for (const c of commits) {
39
+ const date = c.date || '';
40
+ const known = authorCommits.get(c.author?.email);
41
+ if (known) known.commits++;
42
+ else if (c.author?.email) authorCommits.set(c.author.email, { name: c.author.name || c.author.email, commits: 1 });
43
+
44
+ for (const p of c.files || []) {
45
+ churn.set(p, (churn.get(p) || 0) + 1);
46
+ if (!firstSeen.has(p) || date < firstSeen.get(p)) firstSeen.set(p, date);
47
+ if (!lastTouched.has(p) || date > lastTouched.get(p)) lastTouched.set(p, date);
48
+ let byEmail = authorsOf.get(p);
49
+ if (!byEmail) { byEmail = new Map(); authorsOf.set(p, byEmail); }
50
+ if (!byEmail.has(c.author?.email)) byEmail.set(c.author?.email, c.author?.name || c.author?.email || '?');
51
+ }
52
+ }
53
+
54
+ // ---- join with the scan ---------------------------------------------------
55
+ // Complexity lives on scan.files; history paths that are not in the scan are
56
+ // files deleted or renamed inside the window. They had churn once — counted,
57
+ // then set aside rather than silently mixed into rankings of files that no
58
+ // longer exist.
59
+ const cxOf = new Map(scan.files.map((f) => [f.path, f.complexity || 1]));
60
+ for (const p of churn.keys()) if (!cxOf.has(p)) pathsGone++;
61
+
62
+ const live = [...churn.entries()].filter(([p]) => cxOf.has(p));
63
+ const maxChurn = Math.max(1, ...live.map(([, n]) => n));
64
+ const maxCx = Math.max(1, ...live.map(([p]) => cxOf.get(p)));
65
+
66
+ const perFile = live.map(([p, n]) => {
67
+ const authors = authorsOf.get(p).size;
68
+ return {
69
+ path: p,
70
+ name: baseName(p),
71
+ churn: n,
72
+ authors,
73
+ solo: authors === 1,
74
+ firstSeen: firstSeen.get(p),
75
+ lastTouched: lastTouched.get(p),
76
+ complexity: cxOf.get(p),
77
+ // The cross-tab: both axes normalized against the repo's own worst, so
78
+ // the score reads "how far up both columns at once" and a file maxing
79
+ // one axis but not the other cannot outrank one high on both.
80
+ hotspot: Math.round(100 * (n / maxChurn) * (cxOf.get(p) / maxCx)),
81
+ };
82
+ });
83
+ perFile.sort((a, b) => b.hotspot - a.hotspot || b.churn - a.churn || a.path.localeCompare(b.path));
84
+
85
+ const byPath = {};
86
+ for (const row of perFile) byPath[row.path] = row;
87
+ const soloFiles = perFile.filter((r) => r.solo && r.churn >= 3).length;
88
+
89
+ const dates = commits.map((c) => c.date).filter(Boolean);
90
+
91
+ return {
92
+ available: true,
93
+ commitCount: commits.length,
94
+ totalCommits,
95
+ truncated,
96
+ // The raw commit list, not just the rollups. The insights view draws a
97
+ // 52-week heatmap and a punch card from per-commit dates; without the
98
+ // original array, that view rendered an empty calendar even when history
99
+ // was fully available. The cost is the same parsed commits we already hold
100
+ // — `parseGitLog` capped at 2,000 — so duplication here is honest.
101
+ commits,
102
+ authors: [...authorCommits.values()].sort((a, b) => b.commits - a.commits),
103
+ firstCommitAt: dates.length ? dates.reduce((a, b) => (a < b ? a : b)) : null,
104
+ lastCommitAt: dates.length ? dates.reduce((a, b) => (a > b ? a : b)) : null,
105
+ perFile,
106
+ byPath,
107
+ soloFiles,
108
+ coChanged: coChangePairs(commits, new Set(scan.edges.map((e) => e.from + '->' + e.to))),
109
+ pathsGone,
110
+ };
111
+ }
112
+
113
+ // Files that keep turning up in the same commit without importing each other.
114
+ // The structural coupling matrix already shows what imports what; this is the
115
+ // contrast — changes that travel together for reasons the import graph does
116
+ // not know about (shared schema, feature pairs, copy-paste siblings).
117
+ function coChangePairs(commits, structuralEdges) {
118
+ const pairs = new Map(); // 'a|b' (sorted) -> count
119
+
120
+ for (const c of commits) {
121
+ const files = c.files || [];
122
+ if (files.length < 2 || files.length > MAX_COMMIT_FILES_FOR_PAIRS) continue;
123
+ for (let i = 0; i < files.length; i++) {
124
+ for (let j = i + 1; j < files.length; j++) {
125
+ const a = files[i] < files[j] ? files[i] : files[j];
126
+ const b = files[i] < files[j] ? files[j] : files[i];
127
+ if (structuralEdges.has(a + '->' + b) || structuralEdges.has(b + '->' + a)) continue;
128
+ const key = a + '|' + b;
129
+ const seen = pairs.get(key);
130
+ if (seen) seen.count++;
131
+ else if (pairs.size < MAX_TRACKED_PAIRS) pairs.set(key, { a, b, count: 1 });
132
+ }
133
+ }
134
+ }
135
+
136
+ return [...pairs.values()]
137
+ .filter((p) => p.count >= 2) // once is coincidence; the plan is about habits
138
+ .sort((x, y) => y.count - x.count || x.a.localeCompare(y.a))
139
+ .slice(0, 20);
140
+ }
141
+
142
+ // The shape callers should hold when there is nothing to analyze — same fields,
143
+ // honest reason. Views render this instead of inventing zeros.
144
+ export function unavailableHistory(reason) {
145
+ return { available: false, reason };
146
+ }
@@ -0,0 +1,39 @@
1
+ // C# analyzer
2
+ import { blankComments, lineCounter, uniqueBy } from '../util.js';
3
+
4
+ export const extensions = ['.cs'];
5
+
6
+ export function analyze(source, path) {
7
+ const clean = blankComments(source);
8
+ const lineAt = lineCounter(clean);
9
+
10
+ const imports = [];
11
+ for (const m of clean.matchAll(/\busing\s+([a-zA-Z0-9_.]+)\s*;/g)) {
12
+ imports.push({ spec: m[1], line: lineAt(m.index) });
13
+ }
14
+
15
+ const classes = [];
16
+ for (const m of clean.matchAll(/(\[[^\]]+\]\s*)*\b(?:public\s+|private\s+|protected\s+|internal\s+)*(?:abstract\s+|sealed\s+)?(?:class|interface|struct|record|enum)\s+([a-zA-Z0-9_]+)/g)) {
17
+ const attributes = [];
18
+ if (m[1]) {
19
+ for (const am of m[1].matchAll(/\[([A-Za-z0-9_]+)/g)) {
20
+ attributes.push(am[1]);
21
+ }
22
+ }
23
+ classes.push({ name: m[2], attributes, line: lineAt(m.index) });
24
+ }
25
+
26
+ const hasMain = /static\s+(?:async\s+Task|void|int|async\s+Task<int>)\s+Main\s*\(/.test(clean);
27
+
28
+ return {
29
+ imports,
30
+ exports: classes.map(c => ({ name: c.name, kind: 'class' })),
31
+ functions: [],
32
+ classes,
33
+ hasMain
34
+ };
35
+ }
36
+
37
+ export function resolveImport(spec, fromPath, has, context = {}) {
38
+ return { unresolved: spec };
39
+ }
@@ -0,0 +1,131 @@
1
+ // Generic analyzer for C/C++, Ruby, and PHP — the languages that don't have
2
+ // a dedicated module. Java, Rust, and C# each have their own analyzer in this
3
+ // directory and are dispatched first by `languages/index.js`; the blocks for
4
+ // them used to live here, before they had anywhere better to go, and were
5
+ // removed when those files landed so the dispatch table is the only place
6
+ // that decides which family a file belongs to.
7
+ //
8
+ // The work this module does is deliberately cheap: a regex pass for the
9
+ // include/import/require/use lines plus the obvious function and class
10
+ // shapes. It is enough to place these files on the map; treating it as more
11
+ // than that would be lying.
12
+
13
+ import { blankComments, lineCounter, uniqueBy } from '../util.js';
14
+ import { dirOf, joinPath, baseName } from '../pathUtil.js';
15
+
16
+ export const extensions = ['.c', '.h', '.cc', '.cpp', '.hpp', '.rb', '.php'];
17
+
18
+ const BY_EXT = {
19
+ c: 'c', h: 'c', cc: 'c', cpp: 'c', hpp: 'c',
20
+ rb: 'ruby', php: 'php',
21
+ };
22
+
23
+ const CONTROL = new Set(['if', 'for', 'while', 'switch', 'catch', 'return', 'sizeof', 'do', 'else']);
24
+
25
+ export function analyze(source, path) {
26
+ const family = BY_EXT[path.slice(path.lastIndexOf('.') + 1)] || 'c';
27
+ const clean = blankComments(source, { lineChar: family === 'ruby' ? '#' : '//' });
28
+ const lineAt = lineCounter(clean);
29
+
30
+ const imports = [];
31
+ const functions = [];
32
+ const classes = [];
33
+
34
+ if (family === 'c') {
35
+ for (const m of clean.matchAll(/^[ \t]*#[ \t]*include[ \t]*"([^"]+)"/gm)) {
36
+ imports.push({ spec: m[1], local: true, line: lineAt(m.index) });
37
+ }
38
+ for (const m of clean.matchAll(/^[ \t]*#[ \t]*include[ \t]*<([^>]+)>/gm)) {
39
+ imports.push({ spec: m[1], local: false, line: lineAt(m.index) });
40
+ }
41
+ for (const m of clean.matchAll(/^[A-Za-z_][\w\s\*]*?\s+([\w]+)\s*\([^;{}]*\)\s*\{/gm)) {
42
+ if (!CONTROL.has(m[1])) functions.push({ name: m[1], kind: 'function', line: lineAt(m.index) });
43
+ }
44
+ }
45
+
46
+ if (family === 'ruby') {
47
+ for (const m of clean.matchAll(/^[ \t]*require_relative\s+['"]([^'"]+)['"]/gm)) {
48
+ imports.push({ spec: m[1], kind: 'require_relative', line: lineAt(m.index) });
49
+ }
50
+ for (const m of clean.matchAll(/^[ \t]*require\s+['"]([^'"]+)['"]/gm)) {
51
+ imports.push({ spec: m[1], kind: 'require', line: lineAt(m.index) });
52
+ }
53
+ for (const m of clean.matchAll(/^[ \t]*def\s+([\w!?]+)/gm)) {
54
+ functions.push({ name: m[1], kind: 'function', line: lineAt(m.index) });
55
+ }
56
+ for (const m of clean.matchAll(/^[ \t]*(?:class|module)\s+([\w:]+)/gm)) {
57
+ classes.push({ name: m[1], line: lineAt(m.index) });
58
+ }
59
+ }
60
+
61
+ if (family === 'php') {
62
+ for (const m of clean.matchAll(/\b(?:require|include)(?:_once)?\s*\(?\s*['"]([^'"]+)['"]/g)) {
63
+ imports.push({ spec: m[1], kind: 'require', line: lineAt(m.index) });
64
+ }
65
+ for (const m of clean.matchAll(/^[ \t]*use\s+([\w\\]+)\s*;/gm)) {
66
+ imports.push({ spec: m[1], kind: 'use', line: lineAt(m.index) });
67
+ }
68
+ for (const m of clean.matchAll(/\bfunction\s+([\w]+)\s*\(/g)) {
69
+ functions.push({ name: m[1], kind: 'function', line: lineAt(m.index) });
70
+ }
71
+ for (const m of clean.matchAll(/\bclass\s+([\w]+)/g)) {
72
+ classes.push({ name: m[1], line: lineAt(m.index) });
73
+ }
74
+ }
75
+
76
+ return {
77
+ imports: uniqueBy(imports, (i) => i.spec + ':' + (i.kind || '')),
78
+ exports: [],
79
+ functions,
80
+ classes,
81
+ calls: [],
82
+ hasMain: family === 'c' && /\bint\s+main\s*\(/.test(clean),
83
+ };
84
+ }
85
+
86
+ export function packageNameOf(spec) {
87
+ if (spec.endsWith('.h') || spec.endsWith('.hpp')) return spec;
88
+ return spec.split(/[.:/\\]/)[0] || spec;
89
+ }
90
+
91
+ export function resolveImport(spec, fromPath, has, context = {}, meta = {}) {
92
+ const fromDir = dirOf(fromPath);
93
+ const { findByName = () => null } = context;
94
+
95
+ // C angle-bracket includes like <stdio.h> are explicitly external
96
+ if (meta.local === false && meta.kind !== 'mod' && meta.kind !== 'use' && meta.kind !== 'import') {
97
+ return { external: packageNameOf(spec) };
98
+ }
99
+
100
+ const cleaned = spec.replace(/::/g, '/').replace(/\\/g, '/').replace(/\./g, '/');
101
+ const lastDot = spec.lastIndexOf('.');
102
+ const lastSlash = Math.max(spec.lastIndexOf('/'), spec.lastIndexOf('\\'));
103
+ const lastColon = spec.lastIndexOf('::');
104
+ const lastSep = Math.max(lastDot, lastSlash, lastColon >= 0 ? lastColon + 1 : -1);
105
+ const lastSegment = lastSep >= 0 ? spec.slice(lastSep + 1) : spec;
106
+
107
+ // Only the extensions this module owns. Java/Rust have their own resolvers.
108
+ const candidates = [
109
+ joinPath(fromDir, spec),
110
+ joinPath(fromDir, spec) + '.rb',
111
+ joinPath(fromDir, cleaned) + '.rb',
112
+ joinPath(fromDir, cleaned) + '.php',
113
+ joinPath(fromDir, spec) + '.php',
114
+ ];
115
+ for (const cand of candidates) {
116
+ if (cand && has(cand)) return { path: cand };
117
+ }
118
+
119
+ // Last resort for quoted includes: match on the file name anywhere.
120
+ if (lastSegment && lastSegment !== '*') {
121
+ const hitPhp = findByName(lastSegment + '.php');
122
+ if (hitPhp) return { path: hitPhp };
123
+ }
124
+ const hit = findByName(baseName(spec));
125
+ if (hit) return { path: hit };
126
+
127
+ if (meta.kind === 'use' || meta.kind === 'import' || meta.kind === 'require' || meta.local === false) {
128
+ return { external: packageNameOf(spec) };
129
+ }
130
+ return { unresolved: spec };
131
+ }
@@ -0,0 +1,70 @@
1
+ // Go. Package-oriented: imports resolve to directories, and the scanner maps
2
+ // a directory to the Go files inside it.
3
+
4
+ import { blankComments, lineCounter, uniqueBy } from '../util.js';
5
+
6
+ export const extensions = ['.go'];
7
+
8
+ const IMPORT_SINGLE_RE = /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm;
9
+ const IMPORT_BLOCK_RE = /\bimport\s*\(([\s\S]*?)\)/g;
10
+ const QUOTED_RE = /"([^"]+)"/g;
11
+ const FUNC_RE = /^func\s+(?:\(([^)]*)\)\s*)?([\w]+)\s*\(/gm;
12
+ const PACKAGE_RE = /^package\s+([\w]+)/m;
13
+
14
+ export function analyze(source, path) {
15
+ const clean = blankComments(source);
16
+ const lineAt = lineCounter(clean);
17
+
18
+ const imports = [];
19
+ for (const m of clean.matchAll(IMPORT_SINGLE_RE)) {
20
+ imports.push({ spec: m[1], line: lineAt(m.index) });
21
+ }
22
+ for (const m of clean.matchAll(IMPORT_BLOCK_RE)) {
23
+ for (const q of m[1].matchAll(QUOTED_RE)) {
24
+ imports.push({ spec: q[1], line: lineAt(m.index) });
25
+ }
26
+ }
27
+
28
+ const functions = [];
29
+ for (const m of clean.matchAll(FUNC_RE)) {
30
+ functions.push({
31
+ name: m[2],
32
+ kind: m[1] ? 'method' : 'function',
33
+ receiver: m[1] ? m[1].replace(/[*]/g, '').trim().split(/\s+/).pop() : undefined,
34
+ line: lineAt(m.index),
35
+ });
36
+ }
37
+
38
+ const pkg = (clean.match(PACKAGE_RE) || [])[1] || '';
39
+ const hasMain = pkg === 'main' && /^func\s+main\s*\(\s*\)/m.test(clean);
40
+
41
+ const exports = functions
42
+ .filter((f) => /^[A-Z]/.test(f.name))
43
+ .map((f) => ({ name: f.name, kind: f.kind }));
44
+
45
+ return {
46
+ imports: uniqueBy(imports, (i) => i.spec),
47
+ exports: uniqueBy(exports, (e) => e.name),
48
+ functions,
49
+ classes: [],
50
+ calls: [],
51
+ hasMain,
52
+ packageName: pkg,
53
+ };
54
+ }
55
+
56
+ export function packageNameOf(spec) {
57
+ return spec.split('/')[0].includes('.') ? spec.split('/').slice(0, 3).join('/') : spec.split('/')[0];
58
+ }
59
+
60
+ export function resolveImport(spec, fromPath, has, context = {}) {
61
+ const { modulePath = '', hasDir = () => false } = context;
62
+ if (modulePath && (spec === modulePath || spec.startsWith(modulePath + '/'))) {
63
+ const dir = spec.slice(modulePath.length).replace(/^\//, '');
64
+ if (hasDir(dir)) return { packageDir: dir };
65
+ return { unresolved: spec };
66
+ }
67
+ // Without a go.mod we guess: no dot in the first segment usually means the
68
+ // standard library; anything else is treated as an external module.
69
+ return { external: packageNameOf(spec) };
70
+ }
@@ -0,0 +1,43 @@
1
+ // Language registry. Order matters: the first match on extension wins.
2
+
3
+ import * as typescript from './typescript.js';
4
+ import * as javascript from './javascript.js';
5
+ import * as python from './python.js';
6
+ import * as go from './go.js';
7
+ import * as rust from './rust.js';
8
+ import * as java from './java.js';
9
+ import * as csharp from './csharp.js';
10
+ import * as generic from './generic.js';
11
+
12
+ export const languages = [
13
+ { id: 'typescript', label: 'TypeScript', short: 'TS', ...typescript },
14
+ { id: 'javascript', label: 'JavaScript', short: 'JS', ...javascript },
15
+ { id: 'python', label: 'Python', short: 'Python', ...python },
16
+ { id: 'go', label: 'Go', short: 'Go', ...go },
17
+ { id: 'rust', label: 'Rust', short: 'Rust', ...rust },
18
+ { id: 'java', label: 'Java', short: 'Java', ...java },
19
+ { id: 'csharp', label: 'C#', short: 'C#', ...csharp },
20
+ { id: 'generic', label: 'C / Ruby / PHP', short: 'C/Ruby/…', ...generic },
21
+ ];
22
+
23
+ const byExtension = new Map();
24
+ for (const lang of languages) {
25
+ for (const ext of lang.extensions) byExtension.set(ext, lang);
26
+ }
27
+
28
+ export function languageFor(path) {
29
+ const i = path.lastIndexOf('.');
30
+ if (i === -1) return null;
31
+ return byExtension.get(path.slice(i).toLowerCase()) || null;
32
+ }
33
+
34
+ // Display names for a language id. Both spellings were previously hardcoded in
35
+ // two places that had drifted apart — a `generic` file was labelled "C/Java/…"
36
+ // in one panel and "C/Java/Rust/Ruby/PHP" in another. The registry already knew
37
+ // the answer; now it is the only one that does. An unknown id comes back as
38
+ // itself, so a new language shows up as a lowercase word rather than nothing.
39
+ export function languageLabel(id, { short = false } = {}) {
40
+ const lang = languages.find((l) => l.id === id);
41
+ if (!lang) return String(id);
42
+ return short ? lang.short : lang.label;
43
+ }
@@ -0,0 +1,39 @@
1
+ // Java analyzer
2
+ import { blankComments, lineCounter, uniqueBy } from '../util.js';
3
+
4
+ export const extensions = ['.java'];
5
+
6
+ export function analyze(source, path) {
7
+ const clean = blankComments(source);
8
+ const lineAt = lineCounter(clean);
9
+
10
+ const imports = [];
11
+ for (const m of clean.matchAll(/\bimport\s+(static\s+)?([a-zA-Z0-9_.]+)/g)) {
12
+ imports.push({ spec: m[2], static: !!m[1], line: lineAt(m.index) });
13
+ }
14
+
15
+ const classes = [];
16
+ for (const m of clean.matchAll(/(@[A-Za-z0-9_]+\s*)*\b(?:public\s+|private\s+|protected\s+)?(?:abstract\s+)?(?:class|interface|enum|record|@interface)\s+([a-zA-Z0-9_]+)/g)) {
17
+ const annotations = [];
18
+ if (m[1]) {
19
+ for (const am of m[1].matchAll(/@([A-Za-z0-9_]+)/g)) {
20
+ annotations.push(am[1]);
21
+ }
22
+ }
23
+ classes.push({ name: m[2], annotations, line: lineAt(m.index) });
24
+ }
25
+
26
+ const hasMain = /public\s+static\s+void\s+main\s*\(/.test(clean);
27
+
28
+ return {
29
+ imports,
30
+ exports: classes.map(c => ({ name: c.name, kind: 'class' })),
31
+ functions: [],
32
+ classes,
33
+ hasMain
34
+ };
35
+ }
36
+
37
+ export function resolveImport(spec, fromPath, has, context = {}) {
38
+ return { unresolved: spec };
39
+ }