@ridit/lens 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +105368 -274002
- package/package.json +13 -19
- package/src/colors.ts +15 -15
- package/src/commands/chat.tsx +32 -23
- package/src/commands/provider.tsx +11 -238
- package/src/commands/repo.tsx +66 -120
- package/src/commands/timeline.tsx +11 -22
- package/src/components/ChatView.tsx +238 -0
- package/src/components/Message.tsx +46 -0
- package/src/components/ToolCall.tsx +67 -0
- package/src/components/chat/ChatView.tsx +550 -0
- package/src/components/chat/Message.tsx +152 -0
- package/src/components/chat/StatusBar.tsx +214 -0
- package/src/components/chat/TextArea.tsx +173 -176
- package/src/components/provider/ApiKeyStep.tsx +207 -199
- package/src/components/provider/ModelStep.tsx +90 -88
- package/src/components/provider/ProviderSetup.tsx +331 -0
- package/src/components/provider/ProviderTypeStep.tsx +53 -61
- package/src/components/repo/StepRow.tsx +68 -69
- package/src/components/timeline/TimelineView.tsx +840 -0
- package/src/components/toolcall-utils.ts +103 -0
- package/src/components/watch/RunView.tsx +497 -0
- package/src/hooks/useChatInput.ts +49 -0
- package/src/hooks/useCommandHandler.ts +117 -0
- package/src/index.tsx +386 -139
- package/src/utils/git.ts +149 -155
- package/src/utils/repo.ts +62 -69
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/watch.ts +165 -307
- package/tests/message.test.ts +38 -0
- package/tests/toolcall-utils.test.ts +111 -0
- package/tsconfig.json +8 -24
- package/CLAUDE.md +0 -50
- package/LENS.md +0 -48
- package/LICENSE +0 -21
- package/README.md +0 -93
- package/addons/README.md +0 -55
- package/addons/clean-cache.js +0 -48
- package/addons/generate-readme.js +0 -67
- package/addons/git-stats.js +0 -29
- package/addons/run-tests.js +0 -127
- package/src/commands/commit.tsx +0 -668
- package/src/commands/review.tsx +0 -294
- package/src/commands/run.tsx +0 -56
- package/src/commands/task.tsx +0 -36
- package/src/components/chat/ChatMessage.tsx +0 -195
- package/src/components/chat/ChatOverlays.tsx +0 -399
- package/src/components/chat/ChatRunner.tsx +0 -517
- package/src/components/chat/hooks/useChat.ts +0 -631
- package/src/components/chat/hooks/useChatInput.ts +0 -79
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
- package/src/components/provider/ProviderPicker.tsx +0 -76
- package/src/components/provider/RemoveProviderStep.tsx +0 -82
- package/src/components/repo/DiffViewer.tsx +0 -175
- package/src/components/repo/FileReviewer.tsx +0 -70
- package/src/components/repo/FileViewer.tsx +0 -60
- package/src/components/repo/IssueFixer.tsx +0 -666
- package/src/components/repo/LensFileMenu.tsx +0 -115
- package/src/components/repo/NoProviderPrompt.tsx +0 -28
- package/src/components/repo/PreviewRunner.tsx +0 -217
- package/src/components/repo/RepoAnalysis.tsx +0 -534
- package/src/components/task/TaskRunner.tsx +0 -396
- package/src/components/timeline/CommitDetail.tsx +0 -272
- package/src/components/timeline/CommitList.tsx +0 -162
- package/src/components/timeline/TimelineChat.tsx +0 -166
- package/src/components/timeline/TimelineRunner.tsx +0 -1285
- package/src/components/watch/RunRunner.tsx +0 -929
- package/src/prompts/fewshot.ts +0 -252
- package/src/prompts/index.ts +0 -2
- package/src/prompts/system.ts +0 -285
- package/src/tools/chart.ts +0 -202
- package/src/tools/convert-image.ts +0 -312
- package/src/tools/files.ts +0 -253
- package/src/tools/git.ts +0 -603
- package/src/tools/index.ts +0 -17
- package/src/tools/pdf.ts +0 -164
- package/src/tools/shell.ts +0 -96
- package/src/tools/view-image.ts +0 -335
- package/src/tools/web.ts +0 -212
- package/src/types/chat.ts +0 -86
- package/src/types/config.ts +0 -20
- package/src/types/repo.ts +0 -54
- package/src/utils/addons/loadAddons.ts +0 -34
- package/src/utils/ai.ts +0 -321
- package/src/utils/chat.ts +0 -326
- package/src/utils/chatHistory.ts +0 -121
- package/src/utils/config.ts +0 -61
- package/src/utils/files.ts +0 -105
- package/src/utils/intentClassifier.ts +0 -58
- package/src/utils/lensfile.ts +0 -142
- package/src/utils/llm.ts +0 -81
- package/src/utils/memory.ts +0 -209
- package/src/utils/preview.ts +0 -119
- package/src/utils/stats.ts +0 -174
- package/src/utils/tools/builtins.ts +0 -377
- package/src/utils/tools/registry.ts +0 -105
package/src/utils/memory.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import os from "os";
|
|
4
|
-
|
|
5
|
-
export type MemoryEntryKind =
|
|
6
|
-
| "file-written"
|
|
7
|
-
| "file-read"
|
|
8
|
-
| "url-fetched"
|
|
9
|
-
| "shell-run"
|
|
10
|
-
| "code-applied"
|
|
11
|
-
| "code-skipped";
|
|
12
|
-
|
|
13
|
-
export type MemoryEntry = {
|
|
14
|
-
kind: MemoryEntryKind;
|
|
15
|
-
detail: string;
|
|
16
|
-
summary: string;
|
|
17
|
-
timestamp: string;
|
|
18
|
-
repoPath?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type Memory = {
|
|
22
|
-
id: string;
|
|
23
|
-
content: string;
|
|
24
|
-
timestamp: string;
|
|
25
|
-
repoPath?: string;
|
|
26
|
-
scope: "repo" | "global";
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type MemoryFile = {
|
|
30
|
-
memories: Memory[];
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const LENS_DIR = path.join(os.homedir(), ".lens");
|
|
34
|
-
const MEMORY_PATH = path.join(LENS_DIR, "memory.json");
|
|
35
|
-
|
|
36
|
-
function loadMemoryFile(): MemoryFile {
|
|
37
|
-
if (!existsSync(MEMORY_PATH)) return { memories: [] };
|
|
38
|
-
try {
|
|
39
|
-
const data = JSON.parse(
|
|
40
|
-
readFileSync(MEMORY_PATH, "utf-8"),
|
|
41
|
-
) as Partial<MemoryFile>;
|
|
42
|
-
return { memories: data.memories ?? [] };
|
|
43
|
-
} catch {
|
|
44
|
-
return { memories: [] };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function saveMemoryFile(m: MemoryFile): void {
|
|
49
|
-
if (!existsSync(LENS_DIR)) mkdirSync(LENS_DIR, { recursive: true });
|
|
50
|
-
writeFileSync(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Session-only action entries (in-memory, never written to disk) ────────────
|
|
54
|
-
|
|
55
|
-
const sessionEntries: MemoryEntry[] = [];
|
|
56
|
-
|
|
57
|
-
export function appendMemory(
|
|
58
|
-
entry: Omit<MemoryEntry, "timestamp">,
|
|
59
|
-
repoPath?: string,
|
|
60
|
-
): void {
|
|
61
|
-
sessionEntries.push({
|
|
62
|
-
...entry,
|
|
63
|
-
repoPath,
|
|
64
|
-
timestamp: new Date().toISOString(),
|
|
65
|
-
});
|
|
66
|
-
if (sessionEntries.length > 200)
|
|
67
|
-
sessionEntries.splice(0, sessionEntries.length - 200);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function buildMemorySummary(repoPath: string): string {
|
|
71
|
-
const m = loadMemoryFile();
|
|
72
|
-
|
|
73
|
-
const globalMemories = m.memories.filter((mem) => mem.scope === "global");
|
|
74
|
-
const repoMemories = m.memories.filter(
|
|
75
|
-
(mem) => mem.scope === "repo" && mem.repoPath === repoPath,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const relevant = sessionEntries
|
|
79
|
-
.filter((e) => !e.repoPath || e.repoPath === repoPath)
|
|
80
|
-
.slice(-50);
|
|
81
|
-
|
|
82
|
-
const parts: string[] = [];
|
|
83
|
-
|
|
84
|
-
if (globalMemories.length > 0) {
|
|
85
|
-
parts.push(
|
|
86
|
-
`## GLOBAL MEMORIES (apply to all repos)\n\n${globalMemories
|
|
87
|
-
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
88
|
-
.join("\n")}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (repoMemories.length > 0) {
|
|
93
|
-
parts.push(
|
|
94
|
-
`## MEMORIES ABOUT THIS REPO\n\n${repoMemories
|
|
95
|
-
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
96
|
-
.join("\n")}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (relevant.length > 0) {
|
|
101
|
-
const lines = relevant.map((e) => {
|
|
102
|
-
const ts = new Date(e.timestamp).toLocaleString();
|
|
103
|
-
return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
|
|
104
|
-
});
|
|
105
|
-
parts.push(
|
|
106
|
-
`## WHAT YOU HAVE ALREADY DONE THIS SESSION\n\nThe following actions have already been completed. Do NOT repeat them unless the user explicitly asks:\n\n${lines.join("\n")}`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return parts.join("\n\n");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function getRepoMemory(repoPath: string): MemoryEntry[] {
|
|
114
|
-
return sessionEntries.filter((e) => !e.repoPath || e.repoPath === repoPath);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function clearRepoMemory(repoPath: string): void {
|
|
118
|
-
// clear session entries for this repo
|
|
119
|
-
const toRemove = sessionEntries
|
|
120
|
-
.map((e, i) => (e.repoPath === repoPath ? i : -1))
|
|
121
|
-
.filter((i) => i >= 0)
|
|
122
|
-
.reverse();
|
|
123
|
-
for (const i of toRemove) sessionEntries.splice(i, 1);
|
|
124
|
-
|
|
125
|
-
// clear persisted memories for this repo (keep global)
|
|
126
|
-
const m = loadMemoryFile();
|
|
127
|
-
m.memories = m.memories.filter(
|
|
128
|
-
(mem) => mem.scope === "global" || mem.repoPath !== repoPath,
|
|
129
|
-
);
|
|
130
|
-
saveMemoryFile(m);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function generateId(): string {
|
|
134
|
-
return Math.random().toString(36).slice(2, 8);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function addMemory(content: string, repoPath: string): Memory {
|
|
138
|
-
const m = loadMemoryFile();
|
|
139
|
-
|
|
140
|
-
const isGlobal = content.startsWith("[global]");
|
|
141
|
-
const cleanContent = isGlobal
|
|
142
|
-
? content.replace("[global]", "").trim()
|
|
143
|
-
: content;
|
|
144
|
-
|
|
145
|
-
const memory: Memory = {
|
|
146
|
-
id: generateId(),
|
|
147
|
-
content: cleanContent,
|
|
148
|
-
repoPath: isGlobal ? undefined : repoPath,
|
|
149
|
-
scope: isGlobal ? "global" : "repo",
|
|
150
|
-
timestamp: new Date().toISOString(),
|
|
151
|
-
};
|
|
152
|
-
m.memories.push(memory);
|
|
153
|
-
saveMemoryFile(m);
|
|
154
|
-
return memory;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function deleteMemory(id: string, repoPath: string): boolean {
|
|
158
|
-
const m = loadMemoryFile();
|
|
159
|
-
const before = m.memories.length;
|
|
160
|
-
m.memories = m.memories.filter((mem) => mem.id !== id);
|
|
161
|
-
if (m.memories.length === before) return false;
|
|
162
|
-
saveMemoryFile(m);
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function listMemories(repoPath: string): Memory[] {
|
|
167
|
-
return loadMemoryFile().memories.filter(
|
|
168
|
-
(mem) => mem.scope === "global" || mem.repoPath === repoPath,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
type SessionToolLog = {
|
|
173
|
-
toolName: string;
|
|
174
|
-
input: string;
|
|
175
|
-
resultPreview: string;
|
|
176
|
-
timestamp: string;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const sessionToolLog: SessionToolLog[] = [];
|
|
180
|
-
|
|
181
|
-
export function logToolCall(
|
|
182
|
-
toolName: string,
|
|
183
|
-
input: string,
|
|
184
|
-
result: string,
|
|
185
|
-
repoPath?: string,
|
|
186
|
-
): void {
|
|
187
|
-
sessionToolLog.push({
|
|
188
|
-
toolName,
|
|
189
|
-
input,
|
|
190
|
-
resultPreview: result.slice(0, 120),
|
|
191
|
-
timestamp: new Date().toISOString(),
|
|
192
|
-
});
|
|
193
|
-
if (sessionToolLog.length > 100)
|
|
194
|
-
sessionToolLog.splice(0, sessionToolLog.length - 100);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function getSessionToolSummary(repoPath: string): string {
|
|
198
|
-
if (sessionToolLog.length === 0) return "";
|
|
199
|
-
const recent = sessionToolLog.slice(-30);
|
|
200
|
-
const lines = recent.map((e) => {
|
|
201
|
-
const input = e.input.length > 60 ? e.input.slice(0, 60) + "…" : e.input;
|
|
202
|
-
return `- ${e.toolName}: ${input}`;
|
|
203
|
-
});
|
|
204
|
-
return `## TOOLS ALREADY USED THIS SESSION\n\nDo NOT call these again unless the user explicitly asks:\n\n${lines.join("\n")}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function clearSessionLog(): void {
|
|
208
|
-
sessionToolLog.splice(0, sessionToolLog.length);
|
|
209
|
-
}
|
package/src/utils/preview.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
4
|
-
import type { PackageManager, PreviewInfo } from "../types/repo";
|
|
5
|
-
|
|
6
|
-
export function detectPreview(repoPath: string): PreviewInfo | null {
|
|
7
|
-
|
|
8
|
-
const pkgPath = path.join(repoPath, "package.json");
|
|
9
|
-
if (existsSync(pkgPath)) {
|
|
10
|
-
try {
|
|
11
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
12
|
-
scripts?: Record<string, string>;
|
|
13
|
-
};
|
|
14
|
-
const scripts = pkg.scripts ?? {};
|
|
15
|
-
|
|
16
|
-
const pm: PackageManager = existsSync(
|
|
17
|
-
path.join(repoPath, "pnpm-lock.yaml"),
|
|
18
|
-
)
|
|
19
|
-
? "pnpm"
|
|
20
|
-
: existsSync(path.join(repoPath, "yarn.lock"))
|
|
21
|
-
? "yarn"
|
|
22
|
-
: "npm";
|
|
23
|
-
|
|
24
|
-
const devScript =
|
|
25
|
-
scripts["dev"] ?? scripts["start"] ?? scripts["serve"] ?? null;
|
|
26
|
-
if (!devScript) return null;
|
|
27
|
-
|
|
28
|
-
const devCmd =
|
|
29
|
-
pm === "npm" ? "npm run dev" : pm === "yarn" ? "yarn dev" : "pnpm dev";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const portMatch = devScript.match(/--port[= ](\d+)/);
|
|
33
|
-
const port = portMatch ? parseInt(portMatch[1]!, 10) : 5173;
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
packageManager: pm,
|
|
37
|
-
installCmd:
|
|
38
|
-
pm === "npm"
|
|
39
|
-
? "npm install"
|
|
40
|
-
: pm === "yarn"
|
|
41
|
-
? "yarn install"
|
|
42
|
-
: "pnpm install",
|
|
43
|
-
devCmd,
|
|
44
|
-
port,
|
|
45
|
-
};
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
existsSync(path.join(repoPath, "requirements.txt")) ||
|
|
54
|
-
existsSync(path.join(repoPath, "pyproject.toml"))
|
|
55
|
-
) {
|
|
56
|
-
return {
|
|
57
|
-
packageManager: "pip",
|
|
58
|
-
installCmd: "pip install -r requirements.txt",
|
|
59
|
-
devCmd: existsSync(path.join(repoPath, "manage.py"))
|
|
60
|
-
? "python manage.py runserver"
|
|
61
|
-
: "python main.py",
|
|
62
|
-
port: 8000,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type PreviewProcess = {
|
|
70
|
-
kill: () => void;
|
|
71
|
-
onLog: (cb: (line: string) => void) => void;
|
|
72
|
-
onError: (cb: (line: string) => void) => void;
|
|
73
|
-
onExit: (cb: (code: number | null) => void) => void;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export function runInstall(
|
|
77
|
-
repoPath: string,
|
|
78
|
-
installCmd: string,
|
|
79
|
-
): PreviewProcess {
|
|
80
|
-
return spawnProcess(repoPath, installCmd);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function runDev(repoPath: string, devCmd: string): PreviewProcess {
|
|
84
|
-
return spawnProcess(repoPath, devCmd);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function spawnProcess(cwd: string, cmd: string): PreviewProcess {
|
|
88
|
-
const [bin, ...args] = cmd.split(" ") as [string, ...string[]];
|
|
89
|
-
const child: ChildProcess = spawn(bin, args, {
|
|
90
|
-
cwd,
|
|
91
|
-
shell: true,
|
|
92
|
-
env: { ...process.env },
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const logCallbacks: ((line: string) => void)[] = [];
|
|
96
|
-
const errorCallbacks: ((line: string) => void)[] = [];
|
|
97
|
-
const exitCallbacks: ((code: number | null) => void)[] = [];
|
|
98
|
-
|
|
99
|
-
child.stdout?.on("data", (data: Buffer) => {
|
|
100
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
101
|
-
lines.forEach((line) => logCallbacks.forEach((cb) => cb(line)));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
child.stderr?.on("data", (data: Buffer) => {
|
|
105
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
106
|
-
lines.forEach((line) => errorCallbacks.forEach((cb) => cb(line)));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
child.on("exit", (code) => {
|
|
110
|
-
exitCallbacks.forEach((cb) => cb(code));
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
kill: () => child.kill(),
|
|
115
|
-
onLog: (cb) => logCallbacks.push(cb),
|
|
116
|
-
onError: (cb) => errorCallbacks.push(cb),
|
|
117
|
-
onExit: (cb) => exitCallbacks.push(cb),
|
|
118
|
-
};
|
|
119
|
-
}
|
package/src/utils/stats.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
export type CodeStats = {
|
|
5
|
-
totalFiles: number;
|
|
6
|
-
totalLines: number;
|
|
7
|
-
languages: Record<string, number>;
|
|
8
|
-
functions: number;
|
|
9
|
-
classes: number;
|
|
10
|
-
blankLines: number;
|
|
11
|
-
commentLines: number;
|
|
12
|
-
codeLines: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const LANG_MAP: Record<string, string> = {
|
|
16
|
-
".ts": "TS",
|
|
17
|
-
".tsx": "TSX",
|
|
18
|
-
".js": "JS",
|
|
19
|
-
".jsx": "JSX",
|
|
20
|
-
".css": "CSS",
|
|
21
|
-
".scss": "SCSS",
|
|
22
|
-
".json": "JSON",
|
|
23
|
-
".md": "MD",
|
|
24
|
-
".py": "PY",
|
|
25
|
-
".go": "GO",
|
|
26
|
-
".rs": "RS",
|
|
27
|
-
".java": "Java",
|
|
28
|
-
".cpp": "C++",
|
|
29
|
-
".c": "C",
|
|
30
|
-
".html": "HTML",
|
|
31
|
-
".yaml": "YAML",
|
|
32
|
-
".yml": "YAML",
|
|
33
|
-
".sh": "SH",
|
|
34
|
-
".env": "ENV",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const SKIP_DIRS = new Set([
|
|
38
|
-
"node_modules",
|
|
39
|
-
".git",
|
|
40
|
-
"dist",
|
|
41
|
-
"build",
|
|
42
|
-
".next",
|
|
43
|
-
"out",
|
|
44
|
-
"coverage",
|
|
45
|
-
"__pycache__",
|
|
46
|
-
".cache",
|
|
47
|
-
"vendor",
|
|
48
|
-
".venv",
|
|
49
|
-
"venv",
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
function isBinary(filePath: string): boolean {
|
|
53
|
-
const binaryExts = new Set([
|
|
54
|
-
".png",
|
|
55
|
-
".jpg",
|
|
56
|
-
".jpeg",
|
|
57
|
-
".gif",
|
|
58
|
-
".webp",
|
|
59
|
-
".ico",
|
|
60
|
-
".svg",
|
|
61
|
-
".woff",
|
|
62
|
-
".woff2",
|
|
63
|
-
".ttf",
|
|
64
|
-
".eot",
|
|
65
|
-
".mp4",
|
|
66
|
-
".mp3",
|
|
67
|
-
".wav",
|
|
68
|
-
".zip",
|
|
69
|
-
".tar",
|
|
70
|
-
".gz",
|
|
71
|
-
".pdf",
|
|
72
|
-
".lock",
|
|
73
|
-
]);
|
|
74
|
-
return binaryExts.has(path.extname(filePath).toLowerCase());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function countFunctionsAndClasses(
|
|
78
|
-
content: string,
|
|
79
|
-
ext: string,
|
|
80
|
-
): { functions: number; classes: number } {
|
|
81
|
-
let functions = 0;
|
|
82
|
-
let classes = 0;
|
|
83
|
-
|
|
84
|
-
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
85
|
-
const fnMatches = content.match(
|
|
86
|
-
/(?:^|\s)(?:function\s+\w+|const\s+\w+\s*=\s*(?:async\s*)?\(|(?:async\s+)?\w+\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{|\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)/gm,
|
|
87
|
-
);
|
|
88
|
-
functions = fnMatches?.length ?? 0;
|
|
89
|
-
|
|
90
|
-
const classMatches = content.match(/(?:^|\s)class\s+\w+/gm);
|
|
91
|
-
classes = classMatches?.length ?? 0;
|
|
92
|
-
} else if (ext === ".py") {
|
|
93
|
-
const fnMatches = content.match(/^\s*def\s+\w+/gm);
|
|
94
|
-
functions = fnMatches?.length ?? 0;
|
|
95
|
-
const classMatches = content.match(/^\s*class\s+\w+/gm);
|
|
96
|
-
classes = classMatches?.length ?? 0;
|
|
97
|
-
} else if ([".go", ".rs"].includes(ext)) {
|
|
98
|
-
const fnMatches = content.match(/^\s*(?:func|fn)\s+\w+/gm);
|
|
99
|
-
functions = fnMatches?.length ?? 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { functions, classes };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function computeStats(repoPath: string, files: string[]): CodeStats {
|
|
106
|
-
const stats: CodeStats = {
|
|
107
|
-
totalFiles: 0,
|
|
108
|
-
totalLines: 0,
|
|
109
|
-
languages: {},
|
|
110
|
-
functions: 0,
|
|
111
|
-
classes: 0,
|
|
112
|
-
blankLines: 0,
|
|
113
|
-
commentLines: 0,
|
|
114
|
-
codeLines: 0,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
for (const filePath of files) {
|
|
118
|
-
const parts = filePath.split(/[/\\]/);
|
|
119
|
-
if (parts.some((p) => SKIP_DIRS.has(p))) continue;
|
|
120
|
-
if (isBinary(filePath)) continue;
|
|
121
|
-
|
|
122
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
123
|
-
const lang = LANG_MAP[ext];
|
|
124
|
-
if (!lang) continue;
|
|
125
|
-
|
|
126
|
-
stats.totalFiles++;
|
|
127
|
-
stats.languages[lang] = (stats.languages[lang] ?? 0) + 1;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const content = readFileSync(path.join(repoPath, filePath), "utf-8");
|
|
131
|
-
const lines = content.split("\n");
|
|
132
|
-
stats.totalLines += lines.length;
|
|
133
|
-
|
|
134
|
-
for (const line of lines) {
|
|
135
|
-
const trimmed = line.trim();
|
|
136
|
-
if (trimmed === "") {
|
|
137
|
-
stats.blankLines++;
|
|
138
|
-
} else if (
|
|
139
|
-
trimmed.startsWith("//") ||
|
|
140
|
-
trimmed.startsWith("#") ||
|
|
141
|
-
trimmed.startsWith("*") ||
|
|
142
|
-
trimmed.startsWith("/*") ||
|
|
143
|
-
trimmed.startsWith("<!--")
|
|
144
|
-
) {
|
|
145
|
-
stats.commentLines++;
|
|
146
|
-
} else {
|
|
147
|
-
stats.codeLines++;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const { functions, classes } = countFunctionsAndClasses(content, ext);
|
|
152
|
-
stats.functions += functions;
|
|
153
|
-
stats.classes += classes;
|
|
154
|
-
} catch {}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return stats;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function formatNumber(n: number): string {
|
|
161
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
162
|
-
return n.toString();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function topLanguages(
|
|
166
|
-
languages: Record<string, number>,
|
|
167
|
-
limit = 5,
|
|
168
|
-
): string {
|
|
169
|
-
return Object.entries(languages)
|
|
170
|
-
.sort((a, b) => b[1] - a[1])
|
|
171
|
-
.slice(0, limit)
|
|
172
|
-
.map(([lang]) => lang)
|
|
173
|
-
.join(", ");
|
|
174
|
-
}
|