devlensio 0.2.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 (136) hide show
  1. package/LICENSE +674 -0
  2. package/dist/clustering/index.d.ts +27 -0
  3. package/dist/clustering/index.js +149 -0
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +78 -0
  6. package/dist/config/providers/file.d.ts +19 -0
  7. package/dist/config/providers/file.js +215 -0
  8. package/dist/config/providers/request.d.ts +2 -0
  9. package/dist/config/providers/request.js +72 -0
  10. package/dist/config/types.d.ts +46 -0
  11. package/dist/config/types.js +81 -0
  12. package/dist/config/writer.d.ts +29 -0
  13. package/dist/config/writer.js +103 -0
  14. package/dist/filesystem/appRouter.d.ts +2 -0
  15. package/dist/filesystem/appRouter.js +126 -0
  16. package/dist/filesystem/backendRoutes.d.ts +2 -0
  17. package/dist/filesystem/backendRoutes.js +161 -0
  18. package/dist/filesystem/index.d.ts +2 -0
  19. package/dist/filesystem/index.js +28 -0
  20. package/dist/filesystem/index.test.d.ts +1 -0
  21. package/dist/filesystem/index.test.js +178 -0
  22. package/dist/filesystem/pagesRouter.d.ts +2 -0
  23. package/dist/filesystem/pagesRouter.js +109 -0
  24. package/dist/fingerprint/detectors.d.ts +8 -0
  25. package/dist/fingerprint/detectors.js +174 -0
  26. package/dist/fingerprint/index.d.ts +2 -0
  27. package/dist/fingerprint/index.js +41 -0
  28. package/dist/fingerprint/index.test.d.ts +1 -0
  29. package/dist/fingerprint/index.test.js +148 -0
  30. package/dist/graph/buildLookup.d.ts +10 -0
  31. package/dist/graph/buildLookup.js +32 -0
  32. package/dist/graph/edges/callEdges.d.ts +7 -0
  33. package/dist/graph/edges/callEdges.js +145 -0
  34. package/dist/graph/edges/eventEdges.d.ts +7 -0
  35. package/dist/graph/edges/eventEdges.js +203 -0
  36. package/dist/graph/edges/guardEdges.d.ts +3 -0
  37. package/dist/graph/edges/guardEdges.js +232 -0
  38. package/dist/graph/edges/hookEdges.d.ts +3 -0
  39. package/dist/graph/edges/hookEdges.js +54 -0
  40. package/dist/graph/edges/importEdges.d.ts +8 -0
  41. package/dist/graph/edges/importEdges.js +224 -0
  42. package/dist/graph/edges/propEdges.d.ts +3 -0
  43. package/dist/graph/edges/propEdges.js +142 -0
  44. package/dist/graph/edges/routeEdge.d.ts +3 -0
  45. package/dist/graph/edges/routeEdge.js +124 -0
  46. package/dist/graph/edges/stateEdges.d.ts +3 -0
  47. package/dist/graph/edges/stateEdges.js +206 -0
  48. package/dist/graph/edges/testEdges.d.ts +3 -0
  49. package/dist/graph/edges/testEdges.js +143 -0
  50. package/dist/graph/edges/utils.d.ts +2 -0
  51. package/dist/graph/edges/utils.js +25 -0
  52. package/dist/graph/index.d.ts +6 -0
  53. package/dist/graph/index.js +65 -0
  54. package/dist/graph/index.test.d.ts +1 -0
  55. package/dist/graph/index.test.js +542 -0
  56. package/dist/graph/thirdPartyLibs.d.ts +8 -0
  57. package/dist/graph/thirdPartyLibs.js +162 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +15 -0
  60. package/dist/jobs/index.d.ts +5 -0
  61. package/dist/jobs/index.js +11 -0
  62. package/dist/jobs/queue/interface.d.ts +13 -0
  63. package/dist/jobs/queue/interface.js +1 -0
  64. package/dist/jobs/queue/memory.d.ts +24 -0
  65. package/dist/jobs/queue/memory.js +291 -0
  66. package/dist/jobs/runner.d.ts +3 -0
  67. package/dist/jobs/runner.js +136 -0
  68. package/dist/jobs/types.d.ts +112 -0
  69. package/dist/jobs/types.js +33 -0
  70. package/dist/parser/directives.d.ts +4 -0
  71. package/dist/parser/directives.js +31 -0
  72. package/dist/parser/extractors/components.d.ts +5 -0
  73. package/dist/parser/extractors/components.js +240 -0
  74. package/dist/parser/extractors/functions.d.ts +4 -0
  75. package/dist/parser/extractors/functions.js +240 -0
  76. package/dist/parser/extractors/hooks.d.ts +4 -0
  77. package/dist/parser/extractors/hooks.js +128 -0
  78. package/dist/parser/extractors/stores.d.ts +3 -0
  79. package/dist/parser/extractors/stores.js +181 -0
  80. package/dist/parser/index.d.ts +14 -0
  81. package/dist/parser/index.js +168 -0
  82. package/dist/parser/index.test.d.ts +1 -0
  83. package/dist/parser/index.test.js +319 -0
  84. package/dist/parser/typeUtils.d.ts +9 -0
  85. package/dist/parser/typeUtils.js +46 -0
  86. package/dist/pipeline/index.d.ts +50 -0
  87. package/dist/pipeline/index.js +249 -0
  88. package/dist/scoring/connectionCounter.d.ts +28 -0
  89. package/dist/scoring/connectionCounter.js +134 -0
  90. package/dist/scoring/fileScorer.d.ts +2 -0
  91. package/dist/scoring/fileScorer.js +44 -0
  92. package/dist/scoring/index.d.ts +22 -0
  93. package/dist/scoring/index.js +130 -0
  94. package/dist/scoring/index.test.d.ts +1 -0
  95. package/dist/scoring/index.test.js +453 -0
  96. package/dist/scoring/nodeScorer.d.ts +3 -0
  97. package/dist/scoring/nodeScorer.js +108 -0
  98. package/dist/scoring/noiseFilter.d.ts +18 -0
  99. package/dist/scoring/noiseFilter.js +92 -0
  100. package/dist/storage/fileStorage.d.ts +117 -0
  101. package/dist/storage/fileStorage.js +616 -0
  102. package/dist/storage/index.d.ts +4 -0
  103. package/dist/storage/index.js +2 -0
  104. package/dist/storage/interface.d.ts +27 -0
  105. package/dist/storage/interface.js +1 -0
  106. package/dist/summarizer/checkpoint.d.ts +15 -0
  107. package/dist/summarizer/checkpoint.js +110 -0
  108. package/dist/summarizer/index.d.ts +2 -0
  109. package/dist/summarizer/index.js +281 -0
  110. package/dist/summarizer/mapreduce.d.ts +4 -0
  111. package/dist/summarizer/mapreduce.js +87 -0
  112. package/dist/summarizer/prompts.d.ts +22 -0
  113. package/dist/summarizer/prompts.js +205 -0
  114. package/dist/summarizer/providers/anthropic.d.ts +9 -0
  115. package/dist/summarizer/providers/anthropic.js +78 -0
  116. package/dist/summarizer/providers/gemini.d.ts +9 -0
  117. package/dist/summarizer/providers/gemini.js +79 -0
  118. package/dist/summarizer/providers/index.d.ts +3 -0
  119. package/dist/summarizer/providers/index.js +43 -0
  120. package/dist/summarizer/providers/ollama.d.ts +9 -0
  121. package/dist/summarizer/providers/ollama.js +23 -0
  122. package/dist/summarizer/providers/openRouter.d.ts +9 -0
  123. package/dist/summarizer/providers/openRouter.js +19 -0
  124. package/dist/summarizer/providers/openai.d.ts +9 -0
  125. package/dist/summarizer/providers/openai.js +72 -0
  126. package/dist/summarizer/providers/types.d.ts +32 -0
  127. package/dist/summarizer/providers/types.js +1 -0
  128. package/dist/summarizer/retry.d.ts +7 -0
  129. package/dist/summarizer/retry.js +51 -0
  130. package/dist/summarizer/topological.d.ts +3 -0
  131. package/dist/summarizer/topological.js +105 -0
  132. package/dist/summarizer/types.d.ts +57 -0
  133. package/dist/summarizer/types.js +17 -0
  134. package/dist/types.d.ts +78 -0
  135. package/dist/types.js +1 -0
  136. package/package.json +48 -0
@@ -0,0 +1,81 @@
1
+ // Note: apiKey is intentionally absent from all defaults.
2
+ // If a user reaches the defaults with no key configured anywhere,
3
+ // the system fails clearly at the LLM call — never silently sends an empty key.
4
+ export const OLLAMA_DEFAULTS = {
5
+ deploymentMode: "local",
6
+ summarization: {
7
+ provider: "ollama",
8
+ model: "qwen2.5-coder:3b", // code-aware, 3B params, runs on ~2GB RAM
9
+ baseUrl: "http://localhost:11434",
10
+ batchSize: 50,
11
+ },
12
+ embedding: {
13
+ provider: "ollama",
14
+ model: "nomic-embed-text", // best local embedding model, 768 dims
15
+ baseUrl: "http://localhost:11434",
16
+ },
17
+ // neo4j absent — file-only mode is the safe default
18
+ };
19
+ export const ANTHROPIC_DEFAULTS = {
20
+ deploymentMode: "local",
21
+ summarization: {
22
+ provider: "anthropic",
23
+ model: "claude-haiku-4-5", // fastest Claude, cheapest, good code understanding
24
+ batchSize: 50,
25
+ // apiKey intentionally absent — user must set in config.json
26
+ },
27
+ embedding: {
28
+ provider: "openai",
29
+ model: "text-embedding-3-small", // most common, cheapest OpenAI embedding
30
+ // apiKey intentionally absent
31
+ },
32
+ // neo4j absent
33
+ };
34
+ // ─── Request Header Names ─────────────────────────────────────────────────────
35
+ //
36
+ // Exact header names the cloud backend sends to this Bun backend.
37
+ // Defined as constants so they are never mistyped across files.
38
+ //
39
+ // The server layer MUST call sanitizeHeaders() before logging any request.
40
+ // Headers marked "NEVER LOG" must never appear in any log output.
41
+ export const CONFIG_HEADERS = {
42
+ // LLM provider for summarization
43
+ PROVIDER: "x-llm-provider", // e.g. "anthropic"
44
+ MODEL: "x-llm-model", // e.g. "claude-haiku-4-5"
45
+ API_KEY: "x-llm-key",
46
+ BASE_URL: "x-llm-base-url", // for Ollama: "http://localhost:11434"
47
+ BATCH_SIZE: "x-batch-size", // e.g. "30"
48
+ EMBED_PROVIDER: "x-embed-provider",
49
+ EMBED_MODEL: "x-embed-model",
50
+ EMBED_KEY: "x-embed-key",
51
+ EMBED_BASE_URL: "x-embed-base-url", // for Ollama embedding
52
+ NEO4J_URL: "x-neo4j-url",
53
+ NEO4J_USER: "x-neo4j-user",
54
+ NEO4J_PASSWORD: "x-neo4j-password",
55
+ NEO4J_STORECODE: "false",
56
+ };
57
+ // ─── Sensitive Headers Set ────────────────────────────────────────────────────
58
+ //
59
+ // Used by sanitizeHeaders() below.
60
+ // Add any new secret header here the moment it is added to CONFIG_HEADERS.
61
+ const SENSITIVE_HEADERS = new Set([
62
+ CONFIG_HEADERS.API_KEY,
63
+ CONFIG_HEADERS.EMBED_KEY,
64
+ CONFIG_HEADERS.NEO4J_PASSWORD,
65
+ ]);
66
+ // ─── sanitizeHeaders ──────────────────────────────────────────────────────────
67
+ //
68
+ // Call this before logging ANY request headers anywhere in the codebase.
69
+ // Replaces secret values with "[REDACTED]" so API keys never appear in logs.
70
+ //
71
+ // Usage:
72
+ // console.log("Incoming headers:", sanitizeHeaders(Object.fromEntries(req.headers)));
73
+ export function sanitizeHeaders(headers) {
74
+ const sanitized = {};
75
+ for (const [key, value] of Object.entries(headers)) {
76
+ sanitized[key] = SENSITIVE_HEADERS.has(key.toLowerCase())
77
+ ? "[REDACTED]"
78
+ : value;
79
+ }
80
+ return sanitized;
81
+ }
@@ -0,0 +1,29 @@
1
+ import type { DevLensConfig } from "./types.js";
2
+ type DeepPartial<T> = {
3
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
4
+ };
5
+ type PartialConfig = DeepPartial<DevLensConfig>;
6
+ export interface SafeConfig {
7
+ deploymentMode: DevLensConfig["deploymentMode"];
8
+ summarization: {
9
+ provider: string;
10
+ model: string;
11
+ baseUrl?: string;
12
+ batchSize: number;
13
+ apiKeyHint?: string;
14
+ };
15
+ embedding: {
16
+ provider: string;
17
+ model: string;
18
+ baseUrl?: string;
19
+ apiKeyHint?: string;
20
+ };
21
+ neo4j?: {
22
+ url: string;
23
+ username: string;
24
+ storeRawCode: boolean;
25
+ };
26
+ }
27
+ export declare function writeConfig(partial: PartialConfig): void;
28
+ export declare function maskConfig(config: DevLensConfig): SafeConfig;
29
+ export {};
@@ -0,0 +1,103 @@
1
+ //! The concept of local config file should only exist in open Source, and in case of deployment the apis for writing this cofig file should never be exposed.
2
+ import fs from "fs";
3
+ import { CONFIG_FILE, CONFIG_DIR } from "./providers/file.js";
4
+ function readRawFile() {
5
+ if (!fs.existsSync(CONFIG_FILE))
6
+ return {};
7
+ try {
8
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
9
+ }
10
+ catch {
11
+ console.warn(`DevLens: config file is malformed, resetting to empty.`);
12
+ return {};
13
+ }
14
+ }
15
+ // atomicWrite
16
+ // Writes to a temp file first, then renames to the real path.
17
+ // If the server crashes mid-write, the old config survives intact.
18
+ function atomicWrite(filePath, content) {
19
+ const tmp = `${filePath}.tmp`;
20
+ fs.writeFileSync(tmp, content, "utf-8");
21
+ fs.renameSync(tmp, filePath);
22
+ }
23
+ // writeConfig ─
24
+ //
25
+ // Public — called by PATCH /api/config handler.
26
+ //
27
+ // Takes only what the user changed from the UI settings form.
28
+ // Merges it on top of whatever is currently in config.json.
29
+ // Writes the result back atomically.
30
+ //
31
+ // Does NOT merge with defaults or env vars — that is file.ts's job at read time.
32
+ // The file only ever contains what the user explicitly set.
33
+ export function writeConfig(partial) {
34
+ // Ensure ~/.devlens/ exists — creates it on first save
35
+ if (!fs.existsSync(CONFIG_DIR)) {
36
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
37
+ }
38
+ const existing = readRawFile();
39
+ const updated = {
40
+ ...existing,
41
+ // Only merge blocks that the user actually touched
42
+ ...(partial.deploymentMode && {
43
+ deploymentMode: partial.deploymentMode,
44
+ }),
45
+ ...(partial.summarization && {
46
+ summarization: {
47
+ ...existing.summarization,
48
+ ...partial.summarization,
49
+ },
50
+ }),
51
+ ...(partial.embedding && {
52
+ embedding: {
53
+ ...existing.embedding,
54
+ ...partial.embedding,
55
+ },
56
+ }),
57
+ // neo4j: if user sent it, merge. If user sent null explicitly, delete it.
58
+ // undefined means "don't touch it"
59
+ ...(partial.neo4j !== undefined && {
60
+ neo4j: partial.neo4j === null
61
+ ? undefined // user explicitly removed Neo4j config
62
+ : {
63
+ ...existing.neo4j,
64
+ ...partial.neo4j,
65
+ },
66
+ }),
67
+ };
68
+ atomicWrite(CONFIG_FILE, JSON.stringify(updated, null, 2));
69
+ }
70
+ function maskKey(key) {
71
+ if (!key)
72
+ return undefined;
73
+ // Show last 3 characters only — enough to identify which key is set
74
+ return `...${key.slice(-3)}`;
75
+ }
76
+ // maskConfig
77
+ // Public — called by GET /api/config handler.
78
+ export function maskConfig(config) {
79
+ return {
80
+ deploymentMode: config.deploymentMode,
81
+ summarization: {
82
+ provider: config.summarization.provider,
83
+ model: config.summarization.model,
84
+ baseUrl: config.summarization.baseUrl,
85
+ batchSize: config.summarization.batchSize,
86
+ apiKeyHint: maskKey(config.summarization.apiKey),
87
+ },
88
+ embedding: {
89
+ provider: config.embedding.provider,
90
+ model: config.embedding.model,
91
+ baseUrl: config.embedding.baseUrl,
92
+ apiKeyHint: maskKey(config.embedding.apiKey),
93
+ },
94
+ // neo4j: return url, storeCode, and username only — password never sent to browser
95
+ ...(config.neo4j && {
96
+ neo4j: {
97
+ url: config.neo4j.url,
98
+ username: config.neo4j.username,
99
+ storeRawCode: config.neo4j.storeRawCode,
100
+ },
101
+ }),
102
+ };
103
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteNode } from "../types.js";
2
+ export declare function analyzeAppRouter(repoPath: string): RouteNode[];
@@ -0,0 +1,126 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ // Converts a filesystem path to a URL path
4
+ // e.g. /users/[userId]/posts → /users/:userId/posts
5
+ function toUrlPath(relativePath) {
6
+ return ("/" +
7
+ relativePath
8
+ .split(path.sep)
9
+ .map((segment) => {
10
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
11
+ // Catch-all: [...slug] → :slug*
12
+ const param = segment.slice(4, -1);
13
+ return `:${param}*`;
14
+ }
15
+ if (segment.startsWith("[") && segment.endsWith("]")) {
16
+ // Dynamic: [userId] → :userId
17
+ const param = segment.slice(1, -1);
18
+ return `:${param}`;
19
+ }
20
+ if (segment.startsWith("(") && segment.endsWith(")")) {
21
+ // Route group: (auth) → ignored in URL
22
+ return null;
23
+ }
24
+ return segment;
25
+ })
26
+ .filter(Boolean)
27
+ .join("/"));
28
+ }
29
+ // Extracts param names from a url path
30
+ // e.g. /users/:userId/posts/:postId → ["userId", "postId"]
31
+ function extractParams(urlPath) {
32
+ const matches = urlPath.match(/:([a-zA-Z]+)\*?/g) || [];
33
+ return matches.map((m) => m.replace(":", "").replace("*", ""));
34
+ }
35
+ // Determines the RouteNodeType from a filename
36
+ function getRouteNodeType(fileName) {
37
+ if (fileName === "page.tsx" || fileName === "page.ts" || fileName === "page.jsx" || fileName === "page.js")
38
+ return "PAGE";
39
+ if (fileName === "layout.tsx" || fileName === "layout.ts" || fileName === "layout.jsx" || fileName === "layout.js")
40
+ return "LAYOUT";
41
+ if (fileName === "loading.tsx" || fileName === "loading.ts")
42
+ return "LOADING";
43
+ if (fileName === "error.tsx" || fileName === "error.ts")
44
+ return "ERROR";
45
+ if (fileName === "not-found.tsx" || fileName === "not-found.ts")
46
+ return "NOT_FOUND";
47
+ if (fileName === "route.ts" || fileName === "route.js")
48
+ return "API_ROUTE";
49
+ return null;
50
+ }
51
+ // Finds the closest layout file that wraps a given route
52
+ function findLayoutPath(filePath, appDir) {
53
+ let dir = path.dirname(filePath);
54
+ while (dir !== appDir && dir !== path.dirname(appDir)) {
55
+ const layout = ["layout.tsx", "layout.ts", "layout.jsx", "layout.js"]
56
+ .map((f) => path.join(dir, f))
57
+ .find((f) => fs.existsSync(f));
58
+ if (layout)
59
+ return layout;
60
+ dir = path.dirname(dir);
61
+ }
62
+ return undefined;
63
+ }
64
+ // Recursively walks the app directory and collects all route nodes
65
+ function walkAppDir(currentDir, appDir, nodes) {
66
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const fullPath = path.join(currentDir, entry.name);
69
+ if (entry.isDirectory()) {
70
+ walkAppDir(fullPath, appDir, nodes);
71
+ continue;
72
+ }
73
+ const nodeType = getRouteNodeType(entry.name);
74
+ if (!nodeType)
75
+ continue;
76
+ // Get path relative to app directory
77
+ const relativePath = path.relative(appDir, path.dirname(fullPath));
78
+ const urlPath = relativePath === "." ? "/" : toUrlPath(relativePath);
79
+ const params = extractParams(urlPath);
80
+ const isDynamic = params.length > 0;
81
+ const isCatchAll = urlPath.includes("*");
82
+ const isGroupRoute = path
83
+ .dirname(fullPath)
84
+ .split(path.sep)
85
+ .some((s) => s.startsWith("(") && s.endsWith(")"));
86
+ const layoutPath = nodeType === "PAGE"
87
+ ? findLayoutPath(fullPath, appDir)
88
+ : undefined;
89
+ nodes.push({
90
+ type: nodeType,
91
+ urlPath,
92
+ filePath: fullPath,
93
+ isDynamic,
94
+ isCatchAll,
95
+ isGroupRoute,
96
+ layoutPath,
97
+ params: params.length > 0 ? params : undefined,
98
+ });
99
+ }
100
+ }
101
+ export function analyzeAppRouter(repoPath) {
102
+ let appDir = path.join(repoPath, "src/app");
103
+ if (!fs.existsSync(appDir)) {
104
+ appDir = path.join(repoPath, "app");
105
+ }
106
+ if (!fs.existsSync(appDir)) {
107
+ throw new Error(`No app directory found at: ${appDir}`);
108
+ }
109
+ // Check for middleware at root level
110
+ const nodes = [];
111
+ const middlewarePath = ["middleware.ts", "middleware.js"]
112
+ .map((f) => path.join(repoPath, f))
113
+ .find((f) => fs.existsSync(f));
114
+ if (middlewarePath) {
115
+ nodes.push({
116
+ type: "MIDDLEWARE",
117
+ urlPath: "*",
118
+ filePath: middlewarePath,
119
+ isDynamic: false,
120
+ isCatchAll: true,
121
+ isGroupRoute: false,
122
+ });
123
+ }
124
+ walkAppDir(appDir, appDir, nodes);
125
+ return nodes;
126
+ }
@@ -0,0 +1,2 @@
1
+ import type { BackendRouteNode } from "../types.js";
2
+ export declare function analyzeBackendRoutes(repoPath: string): BackendRouteNode[];
@@ -0,0 +1,161 @@
1
+ import { Project, SyntaxKind } from "ts-morph";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ const HTTP_METHODS = [
5
+ "get", "post", "put", "delete",
6
+ "patch", "options", "head",
7
+ ];
8
+ const APP_INSTANCE_NAMES = [
9
+ "app", "router", "fastify",
10
+ "server", "api", "koa",
11
+ ];
12
+ const IGNORE_DIRS = [
13
+ "node_modules", "dist", "build",
14
+ ".next", "coverage", ".git",
15
+ ];
16
+ function extractParams(urlPath) {
17
+ const matches = urlPath.match(/:([a-zA-Z0-9_]+)/g) || [];
18
+ return matches.map((m) => m.replace(":", ""));
19
+ }
20
+ function normalizeMethod(method) {
21
+ return method.toUpperCase();
22
+ }
23
+ // Detects backend framework from file import statements
24
+ function detectFileFramework(fileContent) {
25
+ if (fileContent.includes("from 'express'") ||
26
+ fileContent.includes('from "express"') ||
27
+ fileContent.includes("require('express')") ||
28
+ fileContent.includes('require("express")'))
29
+ return "express";
30
+ if (fileContent.includes("from 'fastify'") ||
31
+ fileContent.includes('from "fastify"') ||
32
+ fileContent.includes("require('fastify')") ||
33
+ fileContent.includes('require("fastify")'))
34
+ return "fastify";
35
+ if (fileContent.includes("from 'koa'") ||
36
+ fileContent.includes('from "koa"') ||
37
+ fileContent.includes("require('koa')") ||
38
+ fileContent.includes('require("koa")'))
39
+ return "koa";
40
+ return null;
41
+ }
42
+ function findBackendFiles(dir, files = []) {
43
+ let entries;
44
+ try {
45
+ entries = fs.readdirSync(dir, { withFileTypes: true });
46
+ }
47
+ catch {
48
+ return files;
49
+ }
50
+ for (const entry of entries) {
51
+ const fullPath = path.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ if (IGNORE_DIRS.includes(entry.name))
54
+ continue;
55
+ findBackendFiles(fullPath, files);
56
+ }
57
+ else if (entry.isFile()) {
58
+ if (/\.(ts|js)$/.test(entry.name)) {
59
+ files.push(fullPath);
60
+ }
61
+ }
62
+ }
63
+ return files;
64
+ }
65
+ export function analyzeBackendRoutes(repoPath) {
66
+ const nodes = [];
67
+ const project = new Project({
68
+ compilerOptions: {
69
+ allowJs: true,
70
+ checkJs: false,
71
+ strict: false,
72
+ },
73
+ skipAddingFilesFromTsConfig: true,
74
+ });
75
+ const files = findBackendFiles(repoPath);
76
+ // Only add files that actually import a backend framework
77
+ for (const filePath of files) {
78
+ const content = fs.readFileSync(filePath, "utf-8");
79
+ if (detectFileFramework(content)) {
80
+ project.addSourceFileAtPath(filePath);
81
+ }
82
+ }
83
+ for (const file of project.getSourceFiles()) {
84
+ const filePath = file.getFilePath();
85
+ const content = fs.readFileSync(filePath, "utf-8");
86
+ const framework = detectFileFramework(content);
87
+ if (!framework)
88
+ continue;
89
+ const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression);
90
+ for (const call of callExpressions) {
91
+ try {
92
+ const expression = call.getExpression();
93
+ const expressionText = expression.getText();
94
+ const parts = expressionText.split(".");
95
+ if (parts.length < 2)
96
+ continue;
97
+ const methodName = parts[parts.length - 1].toLowerCase();
98
+ const objectName = parts[parts.length - 2].toLowerCase();
99
+ if (!HTTP_METHODS.includes(methodName))
100
+ continue;
101
+ const isKnownInstance = APP_INSTANCE_NAMES.includes(objectName);
102
+ const looksLikeRouter = objectName.includes("router") ||
103
+ objectName.includes("app") ||
104
+ objectName.includes("server") ||
105
+ objectName.includes("api");
106
+ if (!isKnownInstance && !looksLikeRouter)
107
+ continue;
108
+ const args = call.getArguments();
109
+ if (args.length === 0)
110
+ continue;
111
+ const firstArgText = args[0].getText();
112
+ // Must be a string literal
113
+ if (!firstArgText.startsWith("'") &&
114
+ !firstArgText.startsWith('"') &&
115
+ !firstArgText.startsWith("`"))
116
+ continue;
117
+ // Remove quotes correctly — handles single, double, and backtick
118
+ const urlPath = firstArgText.replace(/^['"`]|['"`]$/g, "");
119
+ if (!urlPath.startsWith("/"))
120
+ continue;
121
+ // Extract handler name if last argument is a simple identifier
122
+ let handlerName;
123
+ let inlineHandler;
124
+ if (args.length >= 2) {
125
+ const lastArg = args[args.length - 1];
126
+ const lastArgText = lastArg.getText();
127
+ if (!lastArgText.includes("=>") &&
128
+ !lastArgText.includes("function") &&
129
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(lastArgText)) {
130
+ handlerName = lastArgText;
131
+ }
132
+ else if (lastArgText.includes("=>") || lastArgText.includes("function")) {
133
+ //extract the inline handler
134
+ inlineHandler = {
135
+ rawCode: lastArgText,
136
+ startLine: lastArg.getStartLineNumber(),
137
+ endLine: lastArg.getEndLineNumber(),
138
+ };
139
+ }
140
+ ;
141
+ }
142
+ const params = extractParams(urlPath);
143
+ nodes.push({
144
+ type: "BACKEND_ROUTE",
145
+ urlPath,
146
+ filePath,
147
+ httpMethod: normalizeMethod(methodName),
148
+ handlerName,
149
+ inlineHandler,
150
+ framework,
151
+ isDynamic: params.length > 0,
152
+ params: params.length > 0 ? params : undefined,
153
+ });
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ }
159
+ }
160
+ return nodes;
161
+ }
@@ -0,0 +1,2 @@
1
+ import type { BackendRouteNode, ProjectFingerprint, RouteNode } from "../types.js";
2
+ export declare function analyzeFilesystem(repoPath: string, fingerprint: ProjectFingerprint): (RouteNode[] | BackendRouteNode[]);
@@ -0,0 +1,28 @@
1
+ import path from "path";
2
+ import { analyzeAppRouter } from "./appRouter.js";
3
+ import { analyzePagesRouter } from "./pagesRouter.js";
4
+ import { analyzeBackendRoutes } from "./backendRoutes.js";
5
+ export function analyzeFilesystem(repoPath, fingerprint) {
6
+ // Handle backend frameworks first
7
+ if (["express", "fastify", "koa"].includes(fingerprint.framework)) {
8
+ return analyzeBackendRoutes(repoPath);
9
+ }
10
+ // Handle Next.js frontend
11
+ if (fingerprint.framework !== "nextjs") {
12
+ return [];
13
+ }
14
+ switch (fingerprint.router) {
15
+ case "app":
16
+ return analyzeAppRouter(repoPath);
17
+ case "pages":
18
+ return analyzePagesRouter(repoPath);
19
+ case "app+pages":
20
+ // Analyze both and merge results
21
+ const appRoutes = analyzeAppRouter(repoPath);
22
+ const pagesRoutes = analyzePagesRouter(repoPath);
23
+ return [...appRoutes, ...pagesRoutes];
24
+ default:
25
+ console.warn(`Next.js project detected but no app or pages folder found at: ${path.resolve(repoPath)}`);
26
+ return [];
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export {};