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,157 @@
1
+ // Service and manifest detection: docker-compose services, package.json
2
+ // (workspaces, scripts, entry points), Procfile. Regex/JSON based — a YAML
3
+ // parser would be overkill for what is a navigational aid.
4
+
5
+ export async function detectManifest(source) {
6
+ const manifest = { entryPoints: [], services: [], packageName: '', scripts: [], workspaces: [], deps: { npm: {}, dev: {}, pip: [], go: [], cargo: [] } };
7
+
8
+ // package.json
9
+ try {
10
+ const pkg = JSON.parse(await source.read('package.json'));
11
+ manifest.packageName = pkg.name || '';
12
+ manifest.scripts = Object.keys(pkg.scripts || {});
13
+ manifest.deps.npm = pkg.dependencies || {};
14
+ manifest.deps.dev = pkg.devDependencies || {};
15
+ if (pkg.main) manifest.entryPoints.push(pkg.main.replace(/^\.\//, ''));
16
+ if (pkg.bin) {
17
+ for (const p of Object.values(typeof pkg.bin === 'string' ? { bin: pkg.bin } : pkg.bin)) {
18
+ manifest.entryPoints.push(String(p).replace(/^\.\//, ''));
19
+ }
20
+ }
21
+ const ws = pkg.workspaces;
22
+ const list = Array.isArray(ws) ? ws : ws?.packages || [];
23
+ manifest.workspaces = list.map((w) => w.replace(/\/$/, ''));
24
+ if (manifest.scripts.some((s) => /start|serve|dev/.test(s))) {
25
+ manifest.hasServerScript = true;
26
+ }
27
+ } catch {
28
+ /* no package.json or unreadable — fine */
29
+ }
30
+
31
+ // requirements.txt
32
+ try {
33
+ manifest.deps.pip = parseRequirements(await source.read('requirements.txt'));
34
+ } catch { /* none */ }
35
+
36
+ // go.mod
37
+ try {
38
+ manifest.deps.go = parseGoRequires(await source.read('go.mod'));
39
+ } catch { /* none */ }
40
+
41
+ // Cargo.toml
42
+ try {
43
+ manifest.deps.cargo = parseCargoDeps(await source.read('Cargo.toml'));
44
+ } catch { /* none */ }
45
+
46
+ // docker-compose.yml / docker-compose.yaml / compose.yml
47
+ for (const name of ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']) {
48
+ let text;
49
+ try {
50
+ text = await source.read(name);
51
+ } catch {
52
+ continue;
53
+ }
54
+ manifest.composeFile = name;
55
+ manifest.services = parseComposeServices(text);
56
+ break;
57
+ }
58
+
59
+ // Procfile
60
+ try {
61
+ const text = await source.read('Procfile');
62
+ for (const line of text.split('\n')) {
63
+ const m = line.match(/^([\w-]+):\s*(.+)$/);
64
+ if (m) manifest.services.push({ name: m[1], command: m[2].trim(), source: 'Procfile' });
65
+ }
66
+ } catch {
67
+ /* none */
68
+ }
69
+
70
+ return manifest;
71
+ }
72
+
73
+ export function parseRequirements(text) {
74
+ return String(text)
75
+ .split('\n')
76
+ .map((l) => l.split('#')[0].trim())
77
+ .filter(Boolean)
78
+ .map((l) => l.split(/[<>=!~;\[ ]/)[0].replace(/["']/g, '').trim())
79
+ .filter(Boolean);
80
+ }
81
+
82
+ export function parseGoRequires(text) {
83
+ const s = String(text);
84
+ const names = new Set();
85
+ const block = s.match(/require\s*\(([\s\S]*?)\)/);
86
+ const scoped = block ? block[1] : '';
87
+ const single = (s.match(/require\s+([\w./-]+)(?:\s+v[\w.\-+]+)?(?:\n|$)/) || [, ''])[1];
88
+ if (single) names.add(single);
89
+ for (const [, name] of (scoped.matchAll(/^\s*([\w./-]+)\s+v?[\w.\-+]*/gm) || [])) if (name) names.add(name);
90
+ const lines = scoped.length ? scoped.split('\n') : s.split('\n').filter((l) => l.trim().startsWith('require'));
91
+ for (const line of lines) {
92
+ const m = line.match(/\b([\w./-]+)\s+v[\w.\-+]+/);
93
+ if (m) names.add(m[1]);
94
+ }
95
+ // drop the standard library pseudo module
96
+ names.delete('.');
97
+ return [...names].sort();
98
+ }
99
+
100
+ export function parseCargoDeps(text) {
101
+ const s = String(text);
102
+ const sec = s.match(/\[dependencies\]([\s\S]*?)(?=\n\[|$)/);
103
+ const body = sec ? sec[1] : '';
104
+ const names = [];
105
+ for (const line of body.split('\n')) {
106
+ const m = line.match(/^([A-Za-z0-9_-]+)\s*(?:=\s*[^{]|\{)/);
107
+ if (m) names.push(m[1]);
108
+ }
109
+ return [...new Set(names)];
110
+ }
111
+
112
+ function parseComposeServices(text) {
113
+ const lines = text.split('\n');
114
+ const services = [];
115
+ let inServices = false;
116
+ let servicesIndent = 0;
117
+ let current = null;
118
+ let currentIndent = 0;
119
+
120
+ for (const line of lines) {
121
+ if (/^\s*#/.test(line) || line.trim() === '') continue;
122
+ const indent = line.length - line.trimStart().length;
123
+ const trimmed = line.trim();
124
+
125
+ if (!inServices) {
126
+ if (/^services:\s*$/.test(trimmed)) {
127
+ inServices = true;
128
+ servicesIndent = indent;
129
+ }
130
+ continue;
131
+ }
132
+
133
+ if (indent <= servicesIndent && !/^services:/.test(trimmed)) break; // left the block
134
+
135
+ const svcMatch = trimmed.match(/^([\w.-]+):\s*$/);
136
+ if (svcMatch && indent === servicesIndent + 2) {
137
+ current = { name: svcMatch[1], source: 'compose', ports: [], dependsOn: [] };
138
+ currentIndent = indent;
139
+ services.push(current);
140
+ continue;
141
+ }
142
+
143
+ if (!current || indent <= currentIndent) continue;
144
+
145
+ let m;
146
+ if ((m = trimmed.match(/^build:\s*(.+)$/))) {
147
+ current.build = m[1].trim();
148
+ } else if ((m = trimmed.match(/^image:\s*(.+)$/))) {
149
+ current.image = m[1].trim();
150
+ } else if ((m = trimmed.match(/^-\s*["']?(\d+):(\d+)/))) {
151
+ current.ports.push(m[1] + ':' + m[2]);
152
+ } else if ((m = trimmed.match(/^context:\s*(.+)$/))) {
153
+ current.build = m[1].trim();
154
+ }
155
+ }
156
+ return services;
157
+ }
@@ -0,0 +1,173 @@
1
+ // Stack analytics: from the manifest's declared dependencies plus the scan
2
+ // language counts, name the frameworks, libraries and languages a repo leans
3
+ // on — each tagged with a category and the official docs URL. The docs URLs
4
+ // feed the server-side /api/doc fetch (allowlisted) for informed summaries.
5
+
6
+ // name -> [category, docs url]
7
+ const KNOWN = {
8
+ react: ['Frontend framework', 'https://react.dev'],
9
+ 'react-dom': ['Frontend framework', 'https://react.dev'],
10
+ '@angular/core': ['Frontend framework', 'https://angular.dev'],
11
+ vue: ['Frontend framework', 'https://vuejs.org'],
12
+ svelte: ['Frontend framework', 'https://svelte.dev'],
13
+ next: ['Web framework', 'https://nextjs.org/docs'],
14
+ nuxt: ['Web framework', 'https://nuxt.com/docs'],
15
+ remix: ['Web framework', 'https://remix.run/docs'],
16
+ gatsby: ['Web framework', 'https://www.gatsbyjs.com/docs'],
17
+ express: ['Web framework', 'https://expressjs.com'],
18
+ fastify: ['Web framework', 'https://fastify.dev'],
19
+ koa: ['Web framework', 'https://koajs.com'],
20
+ '@nestjs/core': ['Web framework', 'https://docs.nestjs.com'],
21
+ hapi: ['Web framework', 'https://hapi.dev'],
22
+ flask: ['Web framework', 'https://flask.palletsprojects.com'],
23
+ django: ['Web framework', 'https://docs.djangoproject.com'],
24
+ fastapi: ['Web framework', 'https://fastapi.tiangolo.com'],
25
+ 'spring-boot': ['Web framework', 'https://docs.spring.io/spring-boot'],
26
+ laravel: ['Web framework', 'https://laravel.com/docs'],
27
+ rails: ['Web framework', 'https://guides.rubyonrails.org'],
28
+ gin: ['Web framework', 'https://gin-gonic.com'],
29
+ echo: ['Web framework', 'https://echo.labstack.com'],
30
+ fiber: ['Web framework', 'https://gofiber.io'],
31
+ 'actix-web': ['Web framework', 'https://actix.rs'],
32
+ axum: ['Web framework', 'https://docs.rs/axum'],
33
+ rocket: ['Web framework', 'https://rocket.rs'],
34
+ prisma: ['Database / ORM', 'https://www.prisma.io/docs'],
35
+ sequelize: ['Database / ORM', 'https://sequelize.org'],
36
+ typeorm: ['Database / ORM', 'https://typeorm.io'],
37
+ sqlalchemy: ['Database / ORM', 'https://docs.sqlalchemy.org'],
38
+ 'drizzle-orm': ['Database / ORM', 'https://orm.drizzle.team'],
39
+ mongoose: ['Database / ORM', 'https://mongoosejs.com'],
40
+ knex: ['Database / ORM', 'https://knexjs.org'],
41
+ pg: ['Database / ORM', 'https://node-postgres.com'],
42
+ pymongo: ['Database / ORM', 'https://pymongo.readthedocs.io'],
43
+ gorm: ['Database / ORM', 'https://gorm.io'],
44
+ sqlx: ['Database / ORM', 'https://docs.rs/sqlx'],
45
+ diesel: ['Database / ORM', 'https://diesel.rs'],
46
+ redis: ['Database / ORM', 'https://redis.io/docs/'],
47
+ jest: ['Testing', 'https://jestjs.io'],
48
+ vitest: ['Testing', 'https://vitest.dev'],
49
+ mocha: ['Testing', 'https://mochajs.org'],
50
+ '@testing-library/react': ['Testing', 'https://testing-library.com'],
51
+ pytest: ['Testing', 'https://docs.pytest.org'],
52
+ playwright: ['Testing', 'https://playwright.dev'],
53
+ cypress: ['Testing', 'https://docs.cypress.io'],
54
+ rspec: ['Testing', 'https://rspec.info'],
55
+ typescript: ['Tooling / build', 'https://www.typescriptlang.org/docs'],
56
+ vite: ['Tooling / build', 'https://vitejs.dev/guide/'],
57
+ webpack: ['Tooling / build', 'https://webpack.js.org'],
58
+ esbuild: ['Tooling / build', 'https://esbuild.github.io'],
59
+ babel: ['Tooling / build', 'https://babeljs.io/docs'],
60
+ rollup: ['Tooling / build', 'https://rollupjs.org'],
61
+ tailwindcss: ['UI / styling', 'https://tailwindcss.com/docs'],
62
+ bootstrap: ['UI / styling', 'https://getbootstrap.com/docs'],
63
+ sass: ['UI / styling', 'https://sass-lang.com/documentation'],
64
+ 'styled-components': ['UI / styling', 'https://styled-components.com'],
65
+ axios: ['Networking', 'https://axios-http.com'],
66
+ requests: ['Networking', 'https://requests.readthedocs.io'],
67
+ urllib3: ['Networking', 'https://urllib3.readthedocs.io'],
68
+ httpx: ['Networking', 'https://www.python-httpx.org'],
69
+ passport: ['Auth', 'https://www.passportjs.org'],
70
+ 'next-auth': ['Auth', 'https://next-auth.js.org'],
71
+ jsonwebtoken: ['Auth', 'https://github.com/auth0/node-jsonwebtoken'],
72
+ pydantic: ['Data validation', 'https://docs.pydantic.dev'],
73
+ numpy: ['Data / science', 'https://numpy.org/doc/'],
74
+ pandas: ['Data / science', 'https://pandas.pydata.org/docs/'],
75
+ 'scikit-learn': ['Data / science', 'https://scikit-learn.org/stable/'],
76
+ torch: ['Data / science', 'https://pytorch.org/docs/'],
77
+ tensorflow: ['Data / science', 'https://www.tensorflow.org/api_docs'],
78
+ 'opencv-python': ['Data / science', 'https://docs.opencv.org'],
79
+ keras: ['Data / science', 'https://keras.io'],
80
+ 'python-dotenv': ['Tooling / config', 'https://github.com/theskumar/python-dotenv'],
81
+ };
82
+
83
+ const ORDER = [
84
+ 'Web framework', 'Frontend framework', 'Database / ORM', 'Auth',
85
+ 'Networking', 'Data / science', 'Data validation', 'UI / styling',
86
+ 'Testing', 'Tooling / build', 'Tooling / config', 'Library',
87
+ ];
88
+
89
+ // Where a dependency that is not in KNOWN sends you: its registry page. Named
90
+ // here rather than inlined below because two things need them — the URL
91
+ // builders, and the host allowlist under them.
92
+ const REGISTRY = {
93
+ npm: (name) => `https://www.npmjs.com/package/${name}`,
94
+ pip: (name) => `https://pypi.org/project/${name}/`,
95
+ go: (name) => `https://pkg.go.dev/${name}`,
96
+ cargo: (name) => `https://docs.rs/${name}`,
97
+ };
98
+
99
+ // Every host `analyzeStack` can hand out a docs URL for. The server allowlists
100
+ // exactly this set for `/api/doc`, and that is the SSRF guard: a host no
101
+ // dependency name can produce is a host the server will not fetch.
102
+ //
103
+ // Derived, not restated. The server used to carry its own hand-written copy of
104
+ // this list, and the two had drifted: `diesel.rs` was reachable from a Cargo
105
+ // dependency but missing from the allowlist, so those docs always came back 403,
106
+ // while `docusaurus.io`, `auth0.com` and a bare `readthedocs.io` were allowed
107
+ // and nothing here could ever ask for them — the last one opening every project
108
+ // on the site rather than the three we name.
109
+ //
110
+ // `www.` comes off because the caller strips it from the incoming host too;
111
+ // leaving it on would mean `www.npmjs.com` never matched a request for
112
+ // `npmjs.com`.
113
+ export const DOC_HOSTS = Object.freeze([...new Set(
114
+ [
115
+ ...Object.values(KNOWN).map(([, url]) => url),
116
+ ...Object.values(REGISTRY).map((toUrl) => toUrl('any')),
117
+ ].map((url) => new URL(url).hostname.replace(/^www\./, ''))
118
+ )].sort());
119
+
120
+ function cleanVersion(v) {
121
+ return String(v || '').replace(/^[\^~>=< ]+/, '').split(' ')[0].trim();
122
+ }
123
+
124
+ export function analyzeStack(manifest = {}, languages = {}) {
125
+ const deps = manifest.deps || { npm: {}, dev: {}, pip: [], go: [], cargo: [] };
126
+ const items = [];
127
+ const seen = new Set();
128
+
129
+ const add = (name, version, category, lang, docs, dev = false) => {
130
+ if (!name || seen.has(name)) return;
131
+ seen.add(name);
132
+ items.push({ name, version: cleanVersion(version), category, lang, docs, dev });
133
+ };
134
+
135
+ const npmDocs = (name) => {
136
+ const k = KNOWN[name];
137
+ return k ? k[1] : REGISTRY.npm(name);
138
+ };
139
+ const catOf = (name) => {
140
+ const k = KNOWN[name];
141
+ return k ? k[0] : 'Library';
142
+ };
143
+
144
+ for (const [name, ver] of Object.entries(deps.npm || {})) {
145
+ if (name !== 'react-dom') add(name, ver, catOf(name), 'JavaScript', npmDocs(name), false);
146
+ }
147
+ for (const [name, ver] of Object.entries(deps.dev || {})) {
148
+ if (name !== 'react-dom') add(name, ver, catOf(name), 'JavaScript', npmDocs(name), true);
149
+ }
150
+ for (const name of deps.pip || []) {
151
+ const k = KNOWN[name];
152
+ add(name, '', k ? k[0] : 'Library', 'Python', k ? k[1] : REGISTRY.pip(name), false);
153
+ }
154
+ for (const name of deps.go || []) {
155
+ const k = KNOWN[name.split('/').pop()];
156
+ add(name, '', k ? k[0] : 'Library', 'Go', REGISTRY.go(name), false);
157
+ }
158
+ for (const name of deps.cargo || []) {
159
+ const k = KNOWN[name] || KNOWN[name.replace(/_/g, '-')];
160
+ add(name, '', k ? k[0] : 'Library', 'Rust', k ? k[1] : REGISTRY.cargo(name), false);
161
+ }
162
+
163
+ items.sort((a, b) => (ORDER.indexOf(a.category) - ORDER.indexOf(b.category)) || a.name.localeCompare(b.name));
164
+
165
+ const pm = [];
166
+ if (deps.npm && Object.keys(deps.npm).length) pm.push('npm');
167
+ if (deps.pip && deps.pip.length) pm.push('pip');
168
+ if (deps.go && deps.go.length) pm.push('go modules');
169
+ if (deps.cargo && deps.cargo.length) pm.push('cargo');
170
+
171
+ return { languages, pm, items: items.slice(0, 48) };
172
+ }
173
+
@@ -0,0 +1,69 @@
1
+ // The guided walk: pick the handful of files a new teammate should see
2
+ // first, in the order they should see them, each with a one-line reason why.
3
+ //
4
+ // This lives in `shared/` because two very different callers want the same
5
+ // ordering: the browser draws it as a tour, and the MCP server hands it to an
6
+ // agent as "read these eight files first". A second implementation would drift,
7
+ // and a drifted tour would tell a person and an agent different things about the
8
+ // same repository. `describeStop` is presentation for the browser's own chrome
9
+ // and has no business here.
10
+
11
+ import { baseName } from './pathUtil.js';
12
+
13
+ export function buildTourStops(scan, facts) {
14
+ const stops = [];
15
+ const used = new Set();
16
+
17
+ const push = (path, why) => {
18
+ if (!path || used.has(path)) return;
19
+ if (!scan.files.some((f) => f.path === path)) return;
20
+ used.add(path);
21
+ stops.push({ path, why });
22
+ };
23
+
24
+ // 1. The front door: the entry point with the most reach.
25
+ const entriesByReach = facts.entries
26
+ .map((p) => ({ path: p, out: facts.fanOut[p] || 0 }))
27
+ .sort((a, b) => b.out - a.out);
28
+ if (entriesByReach.length) {
29
+ push(entriesByReach[0].path, 'Start here. This is a way in — code that runs first and calls everything else.');
30
+ }
31
+
32
+ // 2–4. The load-bearing files.
33
+ for (const hub of facts.hubs.slice(0, 3)) {
34
+ push(hub.path, `${hub.fanIn} files depend on this. Understand it before you change anything around it.`);
35
+ }
36
+
37
+ // 5. A second entry if there is one (servers often have a CLI beside them).
38
+ if (entriesByReach.length > 1) {
39
+ push(entriesByReach[1].path, 'Another door into the codebase — same building, different handle.');
40
+ }
41
+
42
+ // 6. A well-used leaf: end of a chain, usually where the real work happens.
43
+ const leaf = scan.files
44
+ .filter((f) => (facts.fanOut[f.path] || 0) === 0 && (facts.fanIn[f.path] || 0) >= 2)
45
+ .sort((a, b) => (facts.fanIn[b.path] || 0) - (facts.fanIn[a.path] || 0))[0];
46
+ if (leaf) {
47
+ push(leaf.path, 'The end of a chain. Nothing below it but the work itself.');
48
+ }
49
+
50
+ // 7. A cycle member, if the repo has them — worth the warning early.
51
+ if (facts.cycles.length) {
52
+ const member = facts.cycles[0][0];
53
+ push(member, `Careful with this one: it sits in a loop of ${facts.cycles[0].length} files importing each other.`);
54
+ }
55
+
56
+ // 8. An orphan, if any survive the earlier stops.
57
+ const orphan = facts.orphans.find((p) => !used.has(p));
58
+ if (orphan && stops.length < 8) {
59
+ push(orphan, 'Nothing imports this. Dead code, or a door we did not recognize — you decide.');
60
+ }
61
+
62
+ return stops.slice(0, 8);
63
+ }
64
+
65
+ // The name a stop shows in the browser's tour bar. Exported from here anyway so
66
+ // neither caller has to import `baseName` just to label a stop.
67
+ export function tourStopTitle(stop) {
68
+ return baseName(stop.path);
69
+ }
@@ -0,0 +1,123 @@
1
+ // Small text utilities shared by the language analyzers.
2
+
3
+ // Blanks out comments while preserving string contents, offsets and line
4
+ // numbers (every matched character becomes a space except newlines), so
5
+ // positions reported afterwards still line up with the original source.
6
+ export function blankComments(source, { block = true, line = true, lineChar = '//' } = {}) {
7
+ let out = source;
8
+ if (block) {
9
+ out = out.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
10
+ }
11
+ if (line && lineChar === '//') {
12
+ out = out.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
13
+ }
14
+ if (line && lineChar === '#') {
15
+ out = out.replace(/^[ \t]*#[^\n]*/gm, (m) => ' '.repeat(m.length));
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export function lineOf(source, index) {
21
+ let line = 1;
22
+ for (let i = 0; i < index && i < source.length; i++) {
23
+ if (source.charCodeAt(i) === 10) line++;
24
+ }
25
+ return line;
26
+ }
27
+
28
+ // The same answer as `lineOf`, for callers that need many of them from one
29
+ // source. `lineOf` walks from byte 0 every time, which is nothing once and
30
+ // quadratic when a 200KB file reports 300 imports, functions and classes — each
31
+ // scanning an average of half the file. This walks the source once, records
32
+ // where every line starts, and binary-searches that table per lookup.
33
+ //
34
+ // The table is built on first use rather than up front, so a file that reports
35
+ // no positions at all pays nothing for asking.
36
+ export function lineCounter(source) {
37
+ const s = String(source);
38
+ let starts = null;
39
+
40
+ return function lineAt(index) {
41
+ if (!starts) {
42
+ starts = [0];
43
+ for (let i = 0; i < s.length; i++) {
44
+ if (s.charCodeAt(i) === 10) starts.push(i + 1);
45
+ }
46
+ }
47
+ // The line containing `index` is the last one that starts at or before it.
48
+ let lo = 0;
49
+ let hi = starts.length - 1;
50
+ while (lo < hi) {
51
+ const mid = (lo + hi + 1) >> 1;
52
+ if (starts[mid] <= index) lo = mid;
53
+ else hi = mid - 1;
54
+ }
55
+ return lo + 1;
56
+ };
57
+ }
58
+
59
+ // Returns the index just past the brace that closes the one at openIndex.
60
+ // Best-effort: a brace inside a string can throw the count off, which is an
61
+ // accepted trade of the no-parser approach.
62
+ export function braceBodyEnd(source, openIndex) {
63
+ let depth = 0;
64
+ for (let i = openIndex; i < source.length; i++) {
65
+ const c = source[i];
66
+ if (c === '{') depth++;
67
+ else if (c === '}') {
68
+ depth--;
69
+ if (depth === 0) return i + 1;
70
+ }
71
+ }
72
+ return source.length;
73
+ }
74
+
75
+ export function uniqueBy(items, key) {
76
+ const seen = new Set();
77
+ const out = [];
78
+ for (const item of items) {
79
+ const k = key(item);
80
+ if (seen.has(k)) continue;
81
+ seen.add(k);
82
+ out.push(item);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ // Which of `names` is called inside `body`? Returns the distinct ones, in the
88
+ // order they first appear.
89
+ //
90
+ // This replaced the obvious version — compile `\bNAME\s*\(` per candidate and
91
+ // test the body with it — which is O(functions²) in compiled regexes. A file
92
+ // with 2,300 functions built five million of them, and profiling put two thirds
93
+ // of the whole parse phase in that loop and the `escapeRe` feeding it. Reading
94
+ // the body once and looking each token up in a Set does the same work in one
95
+ // pass over the text.
96
+ //
97
+ // `$` counts as part of an identifier, as JavaScript has it, so `$foo(` is a
98
+ // call to `$foo` and not to `foo`. A call written `obj.method(` counts as a call
99
+ // to `method`: this is a best-effort text scan, not a resolver, and a method
100
+ // name that matches a local function is more likely that function than not.
101
+ const CALL_TOKEN = /(?<![\w$])([A-Za-z_$][\w$]*)\s*\(/g;
102
+
103
+ export function callsWithin(body, names) {
104
+ const found = [];
105
+ const seen = new Set();
106
+ for (const m of String(body).matchAll(CALL_TOKEN)) {
107
+ const name = m[1];
108
+ if (seen.has(name) || !names.has(name)) continue;
109
+ seen.add(name);
110
+ found.push(name);
111
+ }
112
+ return found;
113
+ }
114
+
115
+ // The order the callee was declared in, first declaration winning — so the call
116
+ // list comes out in the same order the per-name loop produced it.
117
+ export function declarationOrder(fns) {
118
+ const order = new Map();
119
+ fns.forEach((f, i) => {
120
+ if (!order.has(f.name)) order.set(f.name, i);
121
+ });
122
+ return order;
123
+ }
@@ -0,0 +1,143 @@
1
+ // GitHub Actions & CI/CD workflow parser.
2
+ // Pure isomorphic analyzer — parses YAML workflow files without external dependencies.
3
+
4
+ export async function analyzeWorkflows(source) {
5
+ const workflows = [];
6
+ const entries = await source.list('.github/workflows').catch(() => []);
7
+ const ymlFiles = entries.filter((e) => e.type === 'file' && /\.(ya?ml)$/i.test(e.name));
8
+
9
+ for (const entry of ymlFiles) {
10
+ const raw = await source.read(entry.path).catch(() => '');
11
+ if (!raw) continue;
12
+ const parsed = parseWorkflowYaml(raw, entry.path, entry.name);
13
+ if (parsed) workflows.push(parsed);
14
+ }
15
+
16
+ return workflows;
17
+ }
18
+
19
+ export function parseWorkflowYaml(text, filePath = '', fileName = '') {
20
+ const lines = text.split('\n');
21
+ let name = fileName.replace(/\.ya?ml$/i, '');
22
+ const triggers = [];
23
+ const jobs = [];
24
+
25
+ let currentSection = null;
26
+ let currentJob = null;
27
+ let currentStep = null;
28
+ let inOnSection = false;
29
+
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i];
32
+ const trimmed = line.trim();
33
+ if (!trimmed || trimmed.startsWith('#')) continue;
34
+
35
+ const indent = line.search(/\S/);
36
+
37
+ // Top-level name: ...
38
+ if (indent === 0 && /^name\s*:\s*(.+)$/i.test(trimmed)) {
39
+ const m = trimmed.match(/^name\s*:\s*['"]?([^'"]+)['"]?/i);
40
+ if (m) name = m[1].trim();
41
+ continue;
42
+ }
43
+
44
+ // Top-level on: ...
45
+ if (indent === 0 && /^on\s*:/i.test(trimmed)) {
46
+ currentSection = 'on';
47
+ inOnSection = true;
48
+ const rest = trimmed.replace(/^on\s*:\s*/i, '').trim();
49
+ if (rest) {
50
+ if (rest.startsWith('[') && rest.endsWith(']')) {
51
+ const list = rest.slice(1, -1).split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
52
+ triggers.push(...list.filter(Boolean));
53
+ } else {
54
+ triggers.push(rest.replace(/^['"]|['"]$/g, ''));
55
+ }
56
+ }
57
+ continue;
58
+ }
59
+
60
+ // Top-level jobs: ...
61
+ if (indent === 0 && /^jobs\s*:/i.test(trimmed)) {
62
+ currentSection = 'jobs';
63
+ inOnSection = false;
64
+ continue;
65
+ }
66
+
67
+ if (currentSection === 'on') {
68
+ if (indent === 0) {
69
+ currentSection = null;
70
+ inOnSection = false;
71
+ } else {
72
+ const trigMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
73
+ const listMatch = trimmed.match(/^-\s*([a-zA-Z0-9_-]+)/);
74
+ if (trigMatch && indent <= 4) triggers.push(trigMatch[1]);
75
+ else if (listMatch && indent <= 4) triggers.push(listMatch[1]);
76
+ }
77
+ }
78
+
79
+ if (currentSection === 'jobs' && indent > 0) {
80
+ // Job declaration at indent 2 (or top under jobs)
81
+ if (indent === 2 && trimmed.endsWith(':')) {
82
+ const jobId = trimmed.replace(/:$/, '').trim();
83
+ currentJob = {
84
+ id: jobId,
85
+ name: jobId,
86
+ runsOn: 'ubuntu-latest',
87
+ needs: [],
88
+ steps: [],
89
+ };
90
+ jobs.push(currentJob);
91
+ currentStep = null;
92
+ continue;
93
+ }
94
+
95
+ if (!currentJob) continue;
96
+
97
+ // Inside a job:
98
+ if (/^name\s*:\s*(.+)$/i.test(trimmed) && indent <= 6 && !currentStep) {
99
+ const m = trimmed.match(/^name\s*:\s*['"]?([^'"]+)['"]?/i);
100
+ if (m) currentJob.name = m[1].trim();
101
+ } else if (/^runs-on\s*:\s*(.+)$/i.test(trimmed)) {
102
+ const m = trimmed.match(/^runs-on\s*:\s*['"]?([^'"]+)['"]?/i);
103
+ if (m) currentJob.runsOn = m[1].trim();
104
+ } else if (/^needs\s*:\s*(.+)$/i.test(trimmed)) {
105
+ const rest = trimmed.replace(/^needs\s*:\s*/i, '').trim();
106
+ if (rest.startsWith('[') && rest.endsWith(']')) {
107
+ const list = rest.slice(1, -1).split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
108
+ currentJob.needs.push(...list.filter(Boolean));
109
+ } else if (rest) {
110
+ currentJob.needs.push(rest.replace(/^['"]|['"]$/g, ''));
111
+ }
112
+ } else if (/^-\s*needs\s*:\s*(.+)$/i.test(trimmed)) {
113
+ const m = trimmed.match(/^-\s*needs\s*:\s*(.+)$/i);
114
+ if (m) currentJob.needs.push(m[1].trim());
115
+ } else if (trimmed.startsWith('-') && (indent >= 4 || trimmed.startsWith('- uses:') || trimmed.startsWith('- name:'))) {
116
+ // Step start
117
+ const stepNameMatch = trimmed.match(/^-\s*name\s*:\s*['"]?([^'"]+)['"]?/i);
118
+ const stepUsesMatch = trimmed.match(/^-\s*uses\s*:\s*['"]?([^'"]+)['"]?/i);
119
+ const stepRunMatch = trimmed.match(/^-\s*run\s*:\s*(.+)$/i);
120
+
121
+ currentStep = {
122
+ name: stepNameMatch ? stepNameMatch[1] : (stepUsesMatch ? stepUsesMatch[1] : (stepRunMatch ? stepRunMatch[1] : 'Step')),
123
+ uses: stepUsesMatch ? stepUsesMatch[1] : null,
124
+ run: stepRunMatch ? stepRunMatch[1] : null,
125
+ };
126
+ currentJob.steps.push(currentStep);
127
+ } else if (currentStep) {
128
+ const stepUsesMatch = trimmed.match(/^uses\s*:\s*['"]?([^'"]+)['"]?/i);
129
+ const stepRunMatch = trimmed.match(/^run\s*:\s*(.+)$/i);
130
+ if (stepUsesMatch) currentStep.uses = stepUsesMatch[1];
131
+ if (stepRunMatch) currentStep.run = stepRunMatch[1];
132
+ }
133
+ }
134
+ }
135
+
136
+ return {
137
+ file: filePath,
138
+ fileName,
139
+ name: name || fileName,
140
+ triggers: [...new Set(triggers.filter(Boolean))],
141
+ jobs,
142
+ };
143
+ }