@ridit/lens 0.3.2 → 0.3.4
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/LENS.md +41 -0
- package/README.md +2 -2
- package/dist/index.mjs +5438 -3829
- package/package.json +1 -2
- package/src/commands/commit.tsx +10 -2
- package/src/commands/watch.tsx +56 -0
- package/src/components/repo/LensFileMenu.tsx +2 -9
- package/src/components/repo/RepoAnalysis.tsx +241 -50
- package/src/components/timeline/TimelineRunner.tsx +0 -7
- package/src/components/watch/WatchRunner.tsx +929 -0
- package/src/index.tsx +144 -110
- package/src/types/repo.ts +15 -3
- package/src/utils/ai.ts +108 -20
- package/src/utils/lensfile.ts +83 -18
- package/src/utils/watch.ts +307 -0
package/src/index.tsx
CHANGED
|
@@ -1,110 +1,144 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import "./utils/tools/registry";
|
|
3
|
-
import { render } from "ink";
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { RepoCommand } from "./commands/repo";
|
|
6
|
-
import { InitCommand } from "./commands/provider";
|
|
7
|
-
import { ReviewCommand } from "./commands/review";
|
|
8
|
-
import { TaskCommand } from "./commands/task";
|
|
9
|
-
import { ChatCommand } from "./commands/chat";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.option(
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.option("--
|
|
85
|
-
.option("--
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "./utils/tools/registry";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { RepoCommand } from "./commands/repo";
|
|
6
|
+
import { InitCommand } from "./commands/provider";
|
|
7
|
+
import { ReviewCommand } from "./commands/review";
|
|
8
|
+
import { TaskCommand } from "./commands/task";
|
|
9
|
+
import { ChatCommand } from "./commands/chat";
|
|
10
|
+
import { WatchCommand } from "./commands/watch";
|
|
11
|
+
import { TimelineCommand } from "./commands/timeline";
|
|
12
|
+
import { CommitCommand } from "./commands/commit";
|
|
13
|
+
import { registerBuiltins } from "./utils/tools/builtins";
|
|
14
|
+
import { loadAddons } from "./utils/addons/loadAddons";
|
|
15
|
+
|
|
16
|
+
registerBuiltins();
|
|
17
|
+
await loadAddons();
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command("stalk <url>")
|
|
23
|
+
.alias("repo")
|
|
24
|
+
.description("Analyze a remote repository")
|
|
25
|
+
.action((url) => {
|
|
26
|
+
render(<RepoCommand url={url} />);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command("provider")
|
|
31
|
+
.description("Configure AI providers")
|
|
32
|
+
.action(() => {
|
|
33
|
+
render(<InitCommand />);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("judge [path]")
|
|
38
|
+
.alias("review")
|
|
39
|
+
.description("Review a local codebase")
|
|
40
|
+
.action((inputPath) => {
|
|
41
|
+
render(<ReviewCommand path={inputPath ?? "."} />);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command("cook <text>")
|
|
46
|
+
.alias("task")
|
|
47
|
+
.description("Apply a natural language change to the codebase")
|
|
48
|
+
.option("-p, --path <path>", "Path to the repo", ".")
|
|
49
|
+
.action((text: string, opts: { path: string }) => {
|
|
50
|
+
render(<TaskCommand prompt={text} path={opts.path} />);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command("vibe")
|
|
55
|
+
.alias("chat")
|
|
56
|
+
.description("Chat with your codebase — ask questions or make changes")
|
|
57
|
+
.option("-p, --path <path>", "Path to the repo", ".")
|
|
58
|
+
.action((opts: { path: string }) => {
|
|
59
|
+
render(<ChatCommand path={opts.path} />);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("history")
|
|
64
|
+
.alias("timeline")
|
|
65
|
+
.description(
|
|
66
|
+
"Explore your code history — see commits, changes, and evolution",
|
|
67
|
+
)
|
|
68
|
+
.option("-p, --path <path>", "Path to the repo", ".")
|
|
69
|
+
.action((opts: { path: string }) => {
|
|
70
|
+
render(<TimelineCommand path={opts.path} />);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command("crimes [files...]")
|
|
75
|
+
.alias("commit")
|
|
76
|
+
.description(
|
|
77
|
+
"Generate a smart conventional commit message from staged changes or specific files",
|
|
78
|
+
)
|
|
79
|
+
.option("-p, --path <path>", "Path to the repo", ".")
|
|
80
|
+
.option(
|
|
81
|
+
"--auto",
|
|
82
|
+
"Stage all changes (or the given files) and commit without confirmation",
|
|
83
|
+
)
|
|
84
|
+
.option("--confirm", "Show preview before committing even when using --auto")
|
|
85
|
+
.option("--preview", "Show the generated message without committing")
|
|
86
|
+
.option("--push", "Push to remote after committing")
|
|
87
|
+
.action(
|
|
88
|
+
(
|
|
89
|
+
files: string[],
|
|
90
|
+
opts: {
|
|
91
|
+
path: string;
|
|
92
|
+
auto: boolean;
|
|
93
|
+
confirm: boolean;
|
|
94
|
+
preview: boolean;
|
|
95
|
+
push: boolean;
|
|
96
|
+
},
|
|
97
|
+
) => {
|
|
98
|
+
render(
|
|
99
|
+
<CommitCommand
|
|
100
|
+
path={opts.path}
|
|
101
|
+
files={files ?? []}
|
|
102
|
+
auto={opts.auto ?? false}
|
|
103
|
+
confirm={opts.confirm ?? false}
|
|
104
|
+
preview={opts.preview ?? false}
|
|
105
|
+
push={opts.push ?? false}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.command("watch <cmd>")
|
|
113
|
+
.alias("spy")
|
|
114
|
+
.description("Watch a dev command and get AI suggestions for errors")
|
|
115
|
+
.option("-p, --path <path>", "Path to the repo", ".")
|
|
116
|
+
.option("--clean", "Only show AI suggestions, hide raw logs")
|
|
117
|
+
.option("--fix-all", "Auto-apply fixes as errors are detected")
|
|
118
|
+
.option("--auto-restart", "Automatically re-run the command after a crash")
|
|
119
|
+
.option("--prompt <text>", "Extra context for the AI about your project")
|
|
120
|
+
.action(
|
|
121
|
+
(
|
|
122
|
+
cmd: string,
|
|
123
|
+
opts: {
|
|
124
|
+
path: string;
|
|
125
|
+
clean: boolean;
|
|
126
|
+
fixAll: boolean;
|
|
127
|
+
autoRestart: boolean;
|
|
128
|
+
prompt?: string;
|
|
129
|
+
},
|
|
130
|
+
) => {
|
|
131
|
+
render(
|
|
132
|
+
<WatchCommand
|
|
133
|
+
cmd={cmd}
|
|
134
|
+
path={opts.path}
|
|
135
|
+
clean={opts.clean ?? false}
|
|
136
|
+
fixAll={opts.fixAll ?? false}
|
|
137
|
+
autoRestart={opts.autoRestart ?? false}
|
|
138
|
+
prompt={opts.prompt}
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
program.parse(process.argv);
|
package/src/types/repo.ts
CHANGED
|
@@ -27,12 +27,24 @@ export type AIProvider =
|
|
|
27
27
|
export type AnalysisResult = {
|
|
28
28
|
overview: string;
|
|
29
29
|
importantFolders: string[];
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
tooling: Record<string, string>;
|
|
32
|
+
|
|
33
|
+
keyFiles: string[];
|
|
34
|
+
|
|
35
|
+
patterns: string[];
|
|
36
|
+
|
|
37
|
+
architecture: string;
|
|
32
38
|
suggestions: string[];
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
export type PackageManager =
|
|
41
|
+
export type PackageManager =
|
|
42
|
+
| "npm"
|
|
43
|
+
| "yarn"
|
|
44
|
+
| "pnpm"
|
|
45
|
+
| "bun"
|
|
46
|
+
| "pip"
|
|
47
|
+
| "unknown";
|
|
36
48
|
|
|
37
49
|
export type PreviewInfo = {
|
|
38
50
|
packageManager: PackageManager;
|
package/src/utils/ai.ts
CHANGED
|
@@ -19,8 +19,8 @@ Your job is to select the files you need to read to fully understand what this p
|
|
|
19
19
|
Rules:
|
|
20
20
|
- ALWAYS include package.json, tsconfig.json, README.md if they exist
|
|
21
21
|
- ALWAYS include ALL files inside src/ — especially index files, main entry points, and any files that reveal the project's purpose (components, hooks, utilities, exports)
|
|
22
|
-
- Include config files: vite.config, eslint.config, tailwind.config, etc.
|
|
23
|
-
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
22
|
+
- Include config files: vite.config, eslint.config, tailwind.config, bun.lockb, .nvmrc, etc.
|
|
23
|
+
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
24
24
|
- Do NOT skip source files just because there are many — pick up to 30 files
|
|
25
25
|
- Prefer breadth: pick at least one file from every folder under src/
|
|
26
26
|
|
|
@@ -36,36 +36,96 @@ export function buildAnalysisPrompt(
|
|
|
36
36
|
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
37
37
|
.join("\n\n");
|
|
38
38
|
|
|
39
|
-
return `You are a senior software engineer
|
|
39
|
+
return `You are a senior software engineer building a persistent knowledge base about a codebase. Your output will be stored and incrementally updated over time — it must be durable, structural knowledge, not ephemeral warnings.
|
|
40
|
+
|
|
40
41
|
Repository URL: ${repoUrl}
|
|
41
42
|
|
|
42
43
|
Here are the file contents:
|
|
43
44
|
|
|
44
45
|
${fileList}
|
|
45
46
|
|
|
46
|
-
Analyze this repository
|
|
47
|
+
Analyze this repository and extract permanent, structural understanding. Focus on WHAT the codebase IS and HOW it works — not linting issues or missing configs.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
- Read
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
49
|
+
Rules:
|
|
50
|
+
- Read source code carefully. Reference real file names, real function names, real patterns.
|
|
51
|
+
- tooling: detect from package.json, lockfiles, config files. Keys: packageManager (npm/yarn/pnpm/bun), language, runtime, bundler, framework, testRunner, linter, formatter — only include what you actually found evidence of.
|
|
52
|
+
- keyFiles: list the most important files with a one-line description of what they do. Format: "src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai"
|
|
53
|
+
- patterns: list recurring idioms, design patterns, or conventions actually used in the code. E.g. "Discriminated union state machines for multi-stage UI flows", "React + Ink for terminal rendering"
|
|
54
|
+
- architecture: 2-3 sentences describing the high-level structure and how data flows through the system.
|
|
55
|
+
- importantFolders: describe EVERY folder with specifics — what files are in it and what they do.
|
|
56
|
+
- suggestions: specific, actionable improvements referencing real file names and real patterns you saw. No generic advice.
|
|
57
|
+
- overview: 3-5 sentences naming actual components, features, exports. Be specific.
|
|
56
58
|
|
|
57
|
-
Respond ONLY with a JSON object (no markdown, no explanation)
|
|
59
|
+
Respond ONLY with a JSON object (no markdown, no explanation):
|
|
58
60
|
{
|
|
59
|
-
"overview": "
|
|
61
|
+
"overview": "...",
|
|
62
|
+
"architecture": "...",
|
|
63
|
+
"tooling": {
|
|
64
|
+
"packageManager": "bun",
|
|
65
|
+
"language": "TypeScript",
|
|
66
|
+
"runtime": "Node.js",
|
|
67
|
+
"bundler": "tsup",
|
|
68
|
+
"framework": "Ink"
|
|
69
|
+
},
|
|
60
70
|
"importantFolders": [
|
|
61
|
-
"src/
|
|
71
|
+
"src/commands: contains chat.tsx, commit.tsx, review.tsx — each exports an Ink component that is the top-level renderer for that CLI command"
|
|
72
|
+
],
|
|
73
|
+
"keyFiles": [
|
|
74
|
+
"src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai providers via a unified Provider type"
|
|
75
|
+
],
|
|
76
|
+
"patterns": [
|
|
77
|
+
"Discriminated union state machines (type + stage fields) for multi-step UI flows in every command component"
|
|
62
78
|
],
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
"suggestions": [
|
|
80
|
+
"In src/utils/ai.ts, callModel has no retry logic — adding exponential backoff would improve reliability for ollama which can be slow to start"
|
|
81
|
+
]
|
|
66
82
|
}`;
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
export function buildToolingPatchPrompt(
|
|
86
|
+
repoUrl: string,
|
|
87
|
+
files: ImportantFile[],
|
|
88
|
+
): string {
|
|
89
|
+
const relevant = files.filter((f) =>
|
|
90
|
+
[
|
|
91
|
+
"package.json",
|
|
92
|
+
"bun.lockb",
|
|
93
|
+
"yarn.lock",
|
|
94
|
+
"pnpm-lock.yaml",
|
|
95
|
+
"package-lock.json",
|
|
96
|
+
"tsconfig.json",
|
|
97
|
+
".nvmrc",
|
|
98
|
+
".node-version",
|
|
99
|
+
].includes(path.basename(f.path)),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (relevant.length === 0) return "";
|
|
103
|
+
|
|
104
|
+
const fileList = relevant
|
|
105
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 2000)}\n\`\`\``)
|
|
106
|
+
.join("\n\n");
|
|
107
|
+
|
|
108
|
+
return `You are analyzing a repository's tooling configuration.
|
|
109
|
+
Repository: ${repoUrl}
|
|
110
|
+
|
|
111
|
+
${fileList}
|
|
112
|
+
|
|
113
|
+
Extract only tooling information. Respond ONLY with a JSON object:
|
|
114
|
+
{
|
|
115
|
+
"tooling": {
|
|
116
|
+
"packageManager": "bun | npm | yarn | pnpm",
|
|
117
|
+
"language": "TypeScript | JavaScript | ...",
|
|
118
|
+
"runtime": "Node.js | Bun | Deno | ...",
|
|
119
|
+
"bundler": "tsup | esbuild | vite | webpack | ...",
|
|
120
|
+
"framework": "React | Ink | Next.js | ...",
|
|
121
|
+
"testRunner": "vitest | jest | ...",
|
|
122
|
+
"linter": "eslint | biome | ...",
|
|
123
|
+
"formatter": "prettier | biome | ..."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
Only include keys where you found actual evidence. No markdown, no explanation.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
function parseStringArray(text: string): string[] {
|
|
70
130
|
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
71
131
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
@@ -87,12 +147,25 @@ function parseResult(text: string): AnalysisResult {
|
|
|
87
147
|
return {
|
|
88
148
|
overview: parsed.overview ?? "No overview provided",
|
|
89
149
|
importantFolders: parsed.importantFolders ?? [],
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
tooling: parsed.tooling ?? {},
|
|
151
|
+
keyFiles: parsed.keyFiles ?? [],
|
|
152
|
+
patterns: parsed.patterns ?? [],
|
|
153
|
+
architecture: parsed.architecture ?? "",
|
|
92
154
|
suggestions: parsed.suggestions ?? [],
|
|
93
155
|
};
|
|
94
156
|
}
|
|
95
157
|
|
|
158
|
+
function parseToolingPatch(text: string): Partial<AnalysisResult> | null {
|
|
159
|
+
try {
|
|
160
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
161
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
return JSON.parse(match[0]) as Partial<AnalysisResult>;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
96
169
|
export function checkOllamaInstalled(): Promise<boolean> {
|
|
97
170
|
return new Promise((resolve) => {
|
|
98
171
|
exec("ollama --version", (err) => resolve(!err));
|
|
@@ -220,6 +293,21 @@ export async function requestFileList(
|
|
|
220
293
|
return files;
|
|
221
294
|
}
|
|
222
295
|
|
|
296
|
+
export async function extractToolingPatch(
|
|
297
|
+
repoUrl: string,
|
|
298
|
+
files: ImportantFile[],
|
|
299
|
+
provider: Provider,
|
|
300
|
+
): Promise<Partial<AnalysisResult> | null> {
|
|
301
|
+
const prompt = buildToolingPatchPrompt(repoUrl, files);
|
|
302
|
+
if (!prompt) return null;
|
|
303
|
+
try {
|
|
304
|
+
const text = await callModel(provider, prompt);
|
|
305
|
+
return parseToolingPatch(text);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
223
311
|
export async function analyzeRepo(
|
|
224
312
|
repoUrl: string,
|
|
225
313
|
files: ImportantFile[],
|
package/src/utils/lensfile.ts
CHANGED
|
@@ -7,10 +7,13 @@ export const LENS_FILENAME = "LENS.md";
|
|
|
7
7
|
export type LensFile = {
|
|
8
8
|
overview: string;
|
|
9
9
|
importantFolders: string[];
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
tooling: Record<string, string>;
|
|
11
|
+
keyFiles: string[];
|
|
12
|
+
patterns: string[];
|
|
13
|
+
architecture: string;
|
|
12
14
|
suggestions: string[];
|
|
13
15
|
generatedAt: string;
|
|
16
|
+
lastUpdated: string;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export function lensFilePath(repoPath: string): string {
|
|
@@ -21,36 +24,96 @@ export function lensFileExists(repoPath: string): boolean {
|
|
|
21
24
|
return existsSync(lensFilePath(repoPath));
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
27
|
+
function renderLensFile(data: LensFile): string {
|
|
28
|
+
const toolingLines = Object.entries(data.tooling)
|
|
29
|
+
.map(([k, v]) => `- **${k}**: ${v}`)
|
|
30
|
+
.join("\n");
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
> Generated: ${data.generatedAt}
|
|
32
|
+
return `# Lens
|
|
33
|
+
> Generated: ${data.generatedAt}${data.lastUpdated !== data.generatedAt ? ` | Updated: ${data.lastUpdated}` : ""}
|
|
32
34
|
|
|
33
35
|
## Overview
|
|
34
36
|
${data.overview}
|
|
35
37
|
|
|
38
|
+
## Architecture
|
|
39
|
+
${data.architecture}
|
|
40
|
+
|
|
41
|
+
## Tooling & Conventions
|
|
42
|
+
${toolingLines || "- Not yet determined"}
|
|
43
|
+
|
|
36
44
|
## Important Folders
|
|
37
|
-
${data.importantFolders.map((f) => `- ${f}`).join("\n")}
|
|
45
|
+
${data.importantFolders.map((f) => `- ${f}`).join("\n") || "- None"}
|
|
38
46
|
|
|
39
|
-
##
|
|
40
|
-
${data.
|
|
47
|
+
## Key Files
|
|
48
|
+
${data.keyFiles.map((f) => `- ${f}`).join("\n") || "- None"}
|
|
41
49
|
|
|
42
|
-
##
|
|
43
|
-
${data.
|
|
50
|
+
## Patterns & Idioms
|
|
51
|
+
${data.patterns.map((p) => `- ${p}`).join("\n") || "- None"}
|
|
44
52
|
|
|
45
53
|
## Suggestions
|
|
46
|
-
${data.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
54
|
+
${data.suggestions.map((s) => `- ${s}`).join("\n") || "- None"}
|
|
47
55
|
|
|
48
56
|
<!--lens-json
|
|
49
57
|
${JSON.stringify(data)}
|
|
50
58
|
lens-json-->
|
|
51
59
|
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeLensFile(repoPath: string, result: AnalysisResult): void {
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const data: LensFile = {
|
|
65
|
+
overview: result.overview,
|
|
66
|
+
importantFolders: result.importantFolders,
|
|
67
|
+
tooling: result.tooling ?? {},
|
|
68
|
+
keyFiles: result.keyFiles ?? [],
|
|
69
|
+
patterns: result.patterns ?? [],
|
|
70
|
+
architecture: result.architecture ?? "",
|
|
71
|
+
suggestions: result.suggestions,
|
|
72
|
+
generatedAt: now,
|
|
73
|
+
lastUpdated: now,
|
|
74
|
+
};
|
|
75
|
+
writeFileSync(lensFilePath(repoPath), renderLensFile(data), "utf-8");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function patchLensFile(
|
|
79
|
+
repoPath: string,
|
|
80
|
+
patch: Partial<AnalysisResult>,
|
|
81
|
+
): void {
|
|
82
|
+
const existing = readLensFile(repoPath);
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
|
|
85
|
+
const base: LensFile = existing ?? {
|
|
86
|
+
overview: "",
|
|
87
|
+
importantFolders: [],
|
|
88
|
+
tooling: {},
|
|
89
|
+
keyFiles: [],
|
|
90
|
+
patterns: [],
|
|
91
|
+
architecture: "",
|
|
92
|
+
suggestions: [],
|
|
93
|
+
generatedAt: now,
|
|
94
|
+
lastUpdated: now,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const merged: LensFile = {
|
|
98
|
+
...base,
|
|
99
|
+
lastUpdated: now,
|
|
100
|
+
overview: patch.overview ?? base.overview,
|
|
101
|
+
architecture: patch.architecture ?? base.architecture,
|
|
102
|
+
tooling: { ...base.tooling, ...(patch.tooling ?? {}) },
|
|
103
|
+
importantFolders: dedup([
|
|
104
|
+
...base.importantFolders,
|
|
105
|
+
...(patch.importantFolders ?? []),
|
|
106
|
+
]),
|
|
107
|
+
keyFiles: dedup([...base.keyFiles, ...(patch.keyFiles ?? [])]),
|
|
108
|
+
patterns: dedup([...base.patterns, ...(patch.patterns ?? [])]),
|
|
109
|
+
suggestions: dedup([...base.suggestions, ...(patch.suggestions ?? [])]),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
writeFileSync(lensFilePath(repoPath), renderLensFile(merged), "utf-8");
|
|
113
|
+
}
|
|
52
114
|
|
|
53
|
-
|
|
115
|
+
function dedup(arr: string[]): string[] {
|
|
116
|
+
return [...new Map(arr.map((s) => [s.trim().toLowerCase(), s])).values()];
|
|
54
117
|
}
|
|
55
118
|
|
|
56
119
|
export function readLensFile(repoPath: string): LensFile | null {
|
|
@@ -70,8 +133,10 @@ export function lensFileToAnalysisResult(lf: LensFile): AnalysisResult {
|
|
|
70
133
|
return {
|
|
71
134
|
overview: lf.overview,
|
|
72
135
|
importantFolders: lf.importantFolders,
|
|
73
|
-
|
|
74
|
-
|
|
136
|
+
tooling: lf.tooling,
|
|
137
|
+
keyFiles: lf.keyFiles,
|
|
138
|
+
patterns: lf.patterns,
|
|
139
|
+
architecture: lf.architecture,
|
|
75
140
|
suggestions: lf.suggestions,
|
|
76
141
|
};
|
|
77
142
|
}
|