@ridit/lens 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.
- package/LENS.md +25 -0
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/index.js +49363 -0
- package/package.json +38 -0
- package/src/colors.ts +1 -0
- package/src/commands/chat.tsx +23 -0
- package/src/commands/provider.tsx +224 -0
- package/src/commands/repo.tsx +120 -0
- package/src/commands/review.tsx +294 -0
- package/src/commands/task.tsx +36 -0
- package/src/commands/timeline.tsx +22 -0
- package/src/components/chat/ChatMessage.tsx +176 -0
- package/src/components/chat/ChatOverlays.tsx +329 -0
- package/src/components/chat/ChatRunner.tsx +732 -0
- package/src/components/provider/ApiKeyStep.tsx +243 -0
- package/src/components/provider/ModelStep.tsx +73 -0
- package/src/components/provider/ProviderTypeStep.tsx +54 -0
- package/src/components/provider/RemoveProviderStep.tsx +83 -0
- package/src/components/repo/DiffViewer.tsx +175 -0
- package/src/components/repo/FileReviewer.tsx +70 -0
- package/src/components/repo/FileViewer.tsx +60 -0
- package/src/components/repo/IssueFixer.tsx +666 -0
- package/src/components/repo/LensFileMenu.tsx +122 -0
- package/src/components/repo/NoProviderPrompt.tsx +28 -0
- package/src/components/repo/PreviewRunner.tsx +217 -0
- package/src/components/repo/ProviderPicker.tsx +76 -0
- package/src/components/repo/RepoAnalysis.tsx +343 -0
- package/src/components/repo/StepRow.tsx +69 -0
- package/src/components/task/TaskRunner.tsx +396 -0
- package/src/components/timeline/CommitDetail.tsx +274 -0
- package/src/components/timeline/CommitList.tsx +174 -0
- package/src/components/timeline/TimelineChat.tsx +167 -0
- package/src/components/timeline/TimelineRunner.tsx +1209 -0
- package/src/index.tsx +60 -0
- package/src/types/chat.ts +69 -0
- package/src/types/config.ts +20 -0
- package/src/types/repo.ts +42 -0
- package/src/utils/ai.ts +233 -0
- package/src/utils/chat.ts +833 -0
- package/src/utils/config.ts +61 -0
- package/src/utils/files.ts +104 -0
- package/src/utils/git.ts +155 -0
- package/src/utils/history.ts +86 -0
- package/src/utils/lensfile.ts +77 -0
- package/src/utils/llm.ts +81 -0
- package/src/utils/preview.ts +119 -0
- package/src/utils/repo.ts +69 -0
- package/src/utils/stats.ts +174 -0
- package/src/utils/thinking.tsx +191 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { writeFileSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { ACCENT } from "../../colors";
|
|
9
|
+
import { requestFileList, analyzeRepo } from "../../utils/ai";
|
|
10
|
+
import { ProviderPicker } from "./ProviderPicker";
|
|
11
|
+
import { PreviewRunner } from "./PreviewRunner";
|
|
12
|
+
import { IssueFixer } from "./IssueFixer";
|
|
13
|
+
import { writeLensFile } from "../../utils/lensfile";
|
|
14
|
+
import type { Provider } from "../../types/config";
|
|
15
|
+
import type { AnalysisResult, ImportantFile } from "../../types/repo";
|
|
16
|
+
import { useThinkingPhrase } from "../../utils/thinking";
|
|
17
|
+
|
|
18
|
+
type AnalysisStage =
|
|
19
|
+
| { type: "picking-provider" }
|
|
20
|
+
| { type: "requesting-files" }
|
|
21
|
+
| { type: "analyzing" }
|
|
22
|
+
| { type: "done"; result: AnalysisResult }
|
|
23
|
+
| { type: "writing" }
|
|
24
|
+
| { type: "written"; filePath: string }
|
|
25
|
+
| { type: "previewing" }
|
|
26
|
+
| { type: "fixing"; result: AnalysisResult }
|
|
27
|
+
| { type: "error"; message: string };
|
|
28
|
+
|
|
29
|
+
const OUTPUT_FILES = ["CLAUDE.md", "copilot-instructions.md"] as const;
|
|
30
|
+
type OutputFile = (typeof OUTPUT_FILES)[number];
|
|
31
|
+
|
|
32
|
+
function buildMarkdown(repoUrl: string, result: AnalysisResult): string {
|
|
33
|
+
return `# Repository Analysis
|
|
34
|
+
|
|
35
|
+
> ${repoUrl}
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
${result.overview}
|
|
39
|
+
|
|
40
|
+
## Important Folders
|
|
41
|
+
${result.importantFolders.map((f) => `- ${f}`).join("\n")}
|
|
42
|
+
|
|
43
|
+
## Missing Configs
|
|
44
|
+
${
|
|
45
|
+
result.missingConfigs.length > 0
|
|
46
|
+
? result.missingConfigs.map((f) => `- ${f}`).join("\n")
|
|
47
|
+
: "- None detected"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
## Security Issues
|
|
51
|
+
${
|
|
52
|
+
result.securityIssues.length > 0
|
|
53
|
+
? result.securityIssues.map((s) => `- ⚠️ ${s}`).join("\n")
|
|
54
|
+
: "- None detected"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
## Suggestions
|
|
58
|
+
${result.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function AskingFilesStep() {
|
|
63
|
+
const phrase = useThinkingPhrase(true, "model");
|
|
64
|
+
return (
|
|
65
|
+
<Box gap={1}>
|
|
66
|
+
<Text color={ACCENT}>
|
|
67
|
+
<Spinner />
|
|
68
|
+
</Text>
|
|
69
|
+
<Text color={ACCENT}>{phrase}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function AnalyzingStep() {
|
|
75
|
+
const phrase = useThinkingPhrase(true, "summary");
|
|
76
|
+
return (
|
|
77
|
+
<Box gap={1}>
|
|
78
|
+
<Text color={ACCENT}>
|
|
79
|
+
<Spinner />
|
|
80
|
+
</Text>
|
|
81
|
+
<Text color={ACCENT}>{phrase}</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const RepoAnalysis = ({
|
|
87
|
+
repoUrl,
|
|
88
|
+
repoPath,
|
|
89
|
+
fileTree,
|
|
90
|
+
files: initialFiles,
|
|
91
|
+
preloadedResult,
|
|
92
|
+
onExit,
|
|
93
|
+
}: {
|
|
94
|
+
repoUrl: string;
|
|
95
|
+
repoPath: string;
|
|
96
|
+
fileTree: string[];
|
|
97
|
+
files: ImportantFile[];
|
|
98
|
+
preloadedResult?: AnalysisResult;
|
|
99
|
+
onExit?: () => void;
|
|
100
|
+
}) => {
|
|
101
|
+
const [stage, setStage] = useState<AnalysisStage>(
|
|
102
|
+
preloadedResult
|
|
103
|
+
? { type: "done", result: preloadedResult }
|
|
104
|
+
: { type: "picking-provider" },
|
|
105
|
+
);
|
|
106
|
+
const [selectedOutput, setSelectedOutput] = useState<0 | 1 | 2 | 3>(0);
|
|
107
|
+
const [requestedFiles, setRequestedFiles] = useState<ImportantFile[]>([]);
|
|
108
|
+
const [provider, setProvider] = useState<Provider | null>(null);
|
|
109
|
+
|
|
110
|
+
const OPTIONS = [...OUTPUT_FILES, "Preview repo", "Fix issues"] as const;
|
|
111
|
+
|
|
112
|
+
const handleProviderDone = (p: Provider) => {
|
|
113
|
+
setProvider(p);
|
|
114
|
+
setStage({ type: "requesting-files" });
|
|
115
|
+
requestFileList(repoUrl, repoPath, fileTree, p)
|
|
116
|
+
.then((files) => {
|
|
117
|
+
setRequestedFiles(files);
|
|
118
|
+
setStage({ type: "analyzing" });
|
|
119
|
+
return analyzeRepo(repoUrl, files.length > 0 ? files : initialFiles, p);
|
|
120
|
+
})
|
|
121
|
+
.then((result) => {
|
|
122
|
+
writeLensFile(repoPath, result);
|
|
123
|
+
setStage({ type: "done", result });
|
|
124
|
+
})
|
|
125
|
+
.catch((err: unknown) =>
|
|
126
|
+
setStage({
|
|
127
|
+
type: "error",
|
|
128
|
+
message: err instanceof Error ? err.message : "Analysis failed",
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
useInput((_, key) => {
|
|
134
|
+
if (stage.type !== "done") return;
|
|
135
|
+
if (key.leftArrow)
|
|
136
|
+
setSelectedOutput((i) => Math.max(0, i - 1) as 0 | 1 | 2 | 3);
|
|
137
|
+
if (key.rightArrow)
|
|
138
|
+
setSelectedOutput(
|
|
139
|
+
(i) => Math.min(OPTIONS.length - 1, i + 1) as 0 | 1 | 2 | 3,
|
|
140
|
+
);
|
|
141
|
+
if (key.return) {
|
|
142
|
+
if (selectedOutput === 2) {
|
|
143
|
+
setStage({ type: "previewing" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (selectedOutput === 3) {
|
|
147
|
+
setStage({ type: "fixing", result: stage.result });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const fileName = OUTPUT_FILES[selectedOutput] as OutputFile;
|
|
151
|
+
setStage({ type: "writing" });
|
|
152
|
+
try {
|
|
153
|
+
const filePath = path.join(repoPath, fileName);
|
|
154
|
+
writeFileSync(filePath, buildMarkdown(repoUrl, stage.result), "utf-8");
|
|
155
|
+
setStage({ type: "written", filePath });
|
|
156
|
+
} catch (err: unknown) {
|
|
157
|
+
setStage({
|
|
158
|
+
type: "error",
|
|
159
|
+
message: err instanceof Error ? err.message : "Write failed",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (key.escape) setStage({ type: "written", filePath: "" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (stage.type === "picking-provider") {
|
|
167
|
+
return <ProviderPicker onDone={handleProviderDone} />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (stage.type === "requesting-files") {
|
|
171
|
+
return <AskingFilesStep />;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (stage.type === "analyzing") {
|
|
175
|
+
return (
|
|
176
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
177
|
+
<AnalyzingStep />
|
|
178
|
+
{requestedFiles.length > 0 && (
|
|
179
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
180
|
+
<Text color="gray">Reading {requestedFiles.length} files:</Text>
|
|
181
|
+
{requestedFiles.map((f) => (
|
|
182
|
+
<Text key={f.path} color="gray">
|
|
183
|
+
{figures.bullet} {f.path}
|
|
184
|
+
</Text>
|
|
185
|
+
))}
|
|
186
|
+
</Box>
|
|
187
|
+
)}
|
|
188
|
+
</Box>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (stage.type === "writing") {
|
|
193
|
+
return (
|
|
194
|
+
<Box marginTop={1}>
|
|
195
|
+
<Text color={ACCENT}>
|
|
196
|
+
<Spinner />
|
|
197
|
+
</Text>
|
|
198
|
+
<Box marginLeft={1}>
|
|
199
|
+
<Text>Writing file...</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (stage.type === "written") {
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (onExit) onExit();
|
|
208
|
+
else {
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
}, 100);
|
|
212
|
+
return (
|
|
213
|
+
<Text color="green">
|
|
214
|
+
{figures.tick}{" "}
|
|
215
|
+
{stage.filePath ? `Written to ${stage.filePath}` : "Skipped"}
|
|
216
|
+
</Text>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (stage.type === "previewing") {
|
|
221
|
+
return (
|
|
222
|
+
<Box flexDirection="column">
|
|
223
|
+
<Text color="cyan" bold>
|
|
224
|
+
{figures.play} Preview — {repoPath}
|
|
225
|
+
</Text>
|
|
226
|
+
<PreviewRunner
|
|
227
|
+
repoPath={repoPath}
|
|
228
|
+
onExit={() => {
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (onExit) onExit();
|
|
231
|
+
else {
|
|
232
|
+
process.exit(0);
|
|
233
|
+
}
|
|
234
|
+
}, 100);
|
|
235
|
+
}}
|
|
236
|
+
/>
|
|
237
|
+
</Box>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (stage.type === "fixing") {
|
|
242
|
+
return (
|
|
243
|
+
<IssueFixer
|
|
244
|
+
repoPath={repoPath}
|
|
245
|
+
result={stage.result}
|
|
246
|
+
requestedFiles={requestedFiles}
|
|
247
|
+
provider={provider!}
|
|
248
|
+
onDone={() => setStage({ type: "done", result: stage.result })}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (stage.type === "error") {
|
|
254
|
+
return (
|
|
255
|
+
<Text color="red">
|
|
256
|
+
{figures.cross} {stage.message}
|
|
257
|
+
</Text>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { result } = stage;
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
265
|
+
<Box flexDirection="column">
|
|
266
|
+
<Text bold color="cyan">
|
|
267
|
+
{figures.info} Overview
|
|
268
|
+
</Text>
|
|
269
|
+
<Text color="white">{result.overview}</Text>
|
|
270
|
+
</Box>
|
|
271
|
+
|
|
272
|
+
<Box flexDirection="column">
|
|
273
|
+
<Text bold color="cyan">
|
|
274
|
+
{figures.pointerSmall} Important Folders
|
|
275
|
+
</Text>
|
|
276
|
+
{result.importantFolders.map((f) => (
|
|
277
|
+
<Text key={f} color="white">
|
|
278
|
+
{" "}
|
|
279
|
+
{figures.bullet} {f}
|
|
280
|
+
</Text>
|
|
281
|
+
))}
|
|
282
|
+
</Box>
|
|
283
|
+
|
|
284
|
+
<Box flexDirection="column">
|
|
285
|
+
<Text bold color="yellow">
|
|
286
|
+
{figures.warning} Missing Configs
|
|
287
|
+
</Text>
|
|
288
|
+
{result.missingConfigs.length > 0 ? (
|
|
289
|
+
result.missingConfigs.map((f) => (
|
|
290
|
+
<Text key={f} color="yellow">
|
|
291
|
+
{" "}
|
|
292
|
+
{figures.bullet} {f}
|
|
293
|
+
</Text>
|
|
294
|
+
))
|
|
295
|
+
) : (
|
|
296
|
+
<Text color="gray"> None detected</Text>
|
|
297
|
+
)}
|
|
298
|
+
</Box>
|
|
299
|
+
|
|
300
|
+
<Box flexDirection="column">
|
|
301
|
+
<Text bold color="red">
|
|
302
|
+
{figures.cross} Security Issues
|
|
303
|
+
</Text>
|
|
304
|
+
{result.securityIssues.length > 0 ? (
|
|
305
|
+
result.securityIssues.map((s) => (
|
|
306
|
+
<Text key={s} color="red">
|
|
307
|
+
{" "}
|
|
308
|
+
{figures.bullet} {s}
|
|
309
|
+
</Text>
|
|
310
|
+
))
|
|
311
|
+
) : (
|
|
312
|
+
<Text color="gray"> None detected</Text>
|
|
313
|
+
)}
|
|
314
|
+
</Box>
|
|
315
|
+
|
|
316
|
+
<Box flexDirection="column">
|
|
317
|
+
<Text bold color="green">
|
|
318
|
+
{figures.tick} Suggestions
|
|
319
|
+
</Text>
|
|
320
|
+
{result.suggestions.map((s) => (
|
|
321
|
+
<Text key={s} color="white">
|
|
322
|
+
{" "}
|
|
323
|
+
{figures.bullet} {s}
|
|
324
|
+
</Text>
|
|
325
|
+
))}
|
|
326
|
+
</Box>
|
|
327
|
+
|
|
328
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
329
|
+
<Text bold color="cyan">
|
|
330
|
+
Actions
|
|
331
|
+
</Text>
|
|
332
|
+
<Box gap={2}>
|
|
333
|
+
{OPTIONS.map((f, i) => (
|
|
334
|
+
<Text key={f} color={selectedOutput === i ? "cyan" : "gray"}>
|
|
335
|
+
{selectedOutput === i ? figures.arrowRight : " "} {f}
|
|
336
|
+
</Text>
|
|
337
|
+
))}
|
|
338
|
+
</Box>
|
|
339
|
+
<Text color="gray">← → switch · enter to select · esc to skip</Text>
|
|
340
|
+
</Box>
|
|
341
|
+
</Box>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import Spinner from "ink-spinner";
|
|
3
|
+
import { ACCENT } from "../../colors";
|
|
4
|
+
import { useThinkingPhrase, type ThinkingKind } from "../../utils/thinking";
|
|
5
|
+
import type { Step } from "../../types/repo";
|
|
6
|
+
|
|
7
|
+
const LABELS: Record<string, string> = {
|
|
8
|
+
cloning: "cloning repository",
|
|
9
|
+
"fetching-tree": "fetching repository structure",
|
|
10
|
+
"reading-files": "reading important files",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const kindMap: Record<string, ThinkingKind> = {
|
|
14
|
+
cloning: "cloning",
|
|
15
|
+
"fetching-tree": "analyzing",
|
|
16
|
+
"reading-files": "analyzing",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function ActiveStep({ type }: { type: string }) {
|
|
20
|
+
const phrase = useThinkingPhrase(true, kindMap[type], 4321);
|
|
21
|
+
const label = LABELS[type] ?? type;
|
|
22
|
+
return (
|
|
23
|
+
<Box gap={1}>
|
|
24
|
+
<Text color={ACCENT}>
|
|
25
|
+
<Spinner />
|
|
26
|
+
</Text>
|
|
27
|
+
<Text color={ACCENT}>{phrase}</Text>
|
|
28
|
+
</Box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const StepRow = ({ step }: { step: Step }) => {
|
|
33
|
+
if (step.type === "error") {
|
|
34
|
+
return (
|
|
35
|
+
<Box gap={1}>
|
|
36
|
+
<Text color="red">✗</Text>
|
|
37
|
+
<Text color="red">{step.message}</Text>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (step.type === "folder-exists") {
|
|
43
|
+
return (
|
|
44
|
+
<Box flexDirection="column">
|
|
45
|
+
<Box gap={1}>
|
|
46
|
+
<Text color="yellow">!</Text>
|
|
47
|
+
<Text color="gray">folder already exists at </Text>
|
|
48
|
+
<Text color="white">{step.repoPath}</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
<Box gap={1} marginLeft={2}>
|
|
51
|
+
<Text color="gray">y re-clone · n use existing</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const label = LABELS[step.type] ?? step.type;
|
|
58
|
+
|
|
59
|
+
if (step.status === "done") {
|
|
60
|
+
return (
|
|
61
|
+
<Box gap={1}>
|
|
62
|
+
<Text color="green">✓</Text>
|
|
63
|
+
<Text color="gray">{label}</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return <ActiveStep type={step.type} />;
|
|
69
|
+
};
|