@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.
Files changed (51) hide show
  1. package/LENS.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -0
  4. package/dist/index.js +49363 -0
  5. package/package.json +38 -0
  6. package/src/colors.ts +1 -0
  7. package/src/commands/chat.tsx +23 -0
  8. package/src/commands/provider.tsx +224 -0
  9. package/src/commands/repo.tsx +120 -0
  10. package/src/commands/review.tsx +294 -0
  11. package/src/commands/task.tsx +36 -0
  12. package/src/commands/timeline.tsx +22 -0
  13. package/src/components/chat/ChatMessage.tsx +176 -0
  14. package/src/components/chat/ChatOverlays.tsx +329 -0
  15. package/src/components/chat/ChatRunner.tsx +732 -0
  16. package/src/components/provider/ApiKeyStep.tsx +243 -0
  17. package/src/components/provider/ModelStep.tsx +73 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +54 -0
  19. package/src/components/provider/RemoveProviderStep.tsx +83 -0
  20. package/src/components/repo/DiffViewer.tsx +175 -0
  21. package/src/components/repo/FileReviewer.tsx +70 -0
  22. package/src/components/repo/FileViewer.tsx +60 -0
  23. package/src/components/repo/IssueFixer.tsx +666 -0
  24. package/src/components/repo/LensFileMenu.tsx +122 -0
  25. package/src/components/repo/NoProviderPrompt.tsx +28 -0
  26. package/src/components/repo/PreviewRunner.tsx +217 -0
  27. package/src/components/repo/ProviderPicker.tsx +76 -0
  28. package/src/components/repo/RepoAnalysis.tsx +343 -0
  29. package/src/components/repo/StepRow.tsx +69 -0
  30. package/src/components/task/TaskRunner.tsx +396 -0
  31. package/src/components/timeline/CommitDetail.tsx +274 -0
  32. package/src/components/timeline/CommitList.tsx +174 -0
  33. package/src/components/timeline/TimelineChat.tsx +167 -0
  34. package/src/components/timeline/TimelineRunner.tsx +1209 -0
  35. package/src/index.tsx +60 -0
  36. package/src/types/chat.ts +69 -0
  37. package/src/types/config.ts +20 -0
  38. package/src/types/repo.ts +42 -0
  39. package/src/utils/ai.ts +233 -0
  40. package/src/utils/chat.ts +833 -0
  41. package/src/utils/config.ts +61 -0
  42. package/src/utils/files.ts +104 -0
  43. package/src/utils/git.ts +155 -0
  44. package/src/utils/history.ts +86 -0
  45. package/src/utils/lensfile.ts +77 -0
  46. package/src/utils/llm.ts +81 -0
  47. package/src/utils/preview.ts +119 -0
  48. package/src/utils/repo.ts +69 -0
  49. package/src/utils/stats.ts +174 -0
  50. package/src/utils/thinking.tsx +191 -0
  51. 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
+ };