@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,122 @@
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import figures from "figures";
4
+ import { useState } from "react";
5
+ import type { LensFile } from "../../utils/lensfile";
6
+
7
+ type MenuOption =
8
+ | { id: "use-cached"; label: string; description: string }
9
+ | { id: "re-analyze"; label: string; description: string }
10
+ | { id: "fix-issues"; label: string; description: string }
11
+ | { id: "security"; label: string; description: string }
12
+ | { id: "preview"; label: string; description: string };
13
+
14
+ export type LensMenuChoice =
15
+ | "use-cached"
16
+ | "re-analyze"
17
+ | "fix-issues"
18
+ | "security"
19
+ | "preview";
20
+
21
+ const buildOptions = (lf: LensFile): MenuOption[] => {
22
+ const opts: MenuOption[] = [
23
+ {
24
+ id: "use-cached",
25
+ label: "View existing analysis",
26
+ description: "Show the saved summary",
27
+ },
28
+ {
29
+ id: "re-analyze",
30
+ label: "Re-analyze",
31
+ description: "Run a fresh AI analysis",
32
+ },
33
+ ];
34
+ if (lf.suggestions.length > 0 || lf.missingConfigs.length > 0) {
35
+ opts.push({
36
+ id: "fix-issues",
37
+ label: "Fix issues",
38
+ description: `${lf.suggestions.length + lf.missingConfigs.length} issues found`,
39
+ });
40
+ }
41
+ if (lf.securityIssues.length > 0) {
42
+ opts.push({
43
+ id: "security",
44
+ label: "Review security issues",
45
+ description: `${lf.securityIssues.length} issue(s) found`,
46
+ });
47
+ }
48
+ opts.push({
49
+ id: "preview",
50
+ label: "Preview repo",
51
+ description: "Install deps and run dev server",
52
+ });
53
+ return opts;
54
+ };
55
+
56
+ export const LensFileMenu = ({
57
+ repoPath,
58
+ lensFile,
59
+ onChoice,
60
+ }: {
61
+ repoPath: string;
62
+ lensFile: LensFile;
63
+ onChoice: (choice: LensMenuChoice) => void;
64
+ }) => {
65
+ const [index, setIndex] = useState(0);
66
+ const options = buildOptions(lensFile);
67
+
68
+ useInput((_, key) => {
69
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
70
+ if (key.downArrow) setIndex((i) => Math.min(options.length - 1, i + 1));
71
+ if (key.return) onChoice(options[index]!.id as LensMenuChoice);
72
+ });
73
+
74
+ const age = (() => {
75
+ try {
76
+ const ms = Date.now() - new Date(lensFile.generatedAt).getTime();
77
+ const mins = Math.floor(ms / 60000);
78
+ const hours = Math.floor(mins / 60);
79
+ const days = Math.floor(hours / 24);
80
+ if (days > 0) return `${days}d ago`;
81
+ if (hours > 0) return `${hours}h ago`;
82
+ if (mins > 0) return `${mins}m ago`;
83
+ return "just now";
84
+ } catch {
85
+ return "unknown";
86
+ }
87
+ })();
88
+
89
+ return (
90
+ <Box flexDirection="column" marginTop={1} gap={1}>
91
+ <Box gap={2}>
92
+ <Text bold color="cyan">
93
+ {figures.info} LENS.md found
94
+ </Text>
95
+ <Text color="gray">analyzed {age}</Text>
96
+ </Box>
97
+ <Text color="gray" dimColor>
98
+ {lensFile.overview.slice(0, 100)}
99
+ {lensFile.overview.length > 100 ? "…" : ""}
100
+ </Text>
101
+ <Box flexDirection="column" gap={0}>
102
+ {options.map((opt, i) => {
103
+ const isSelected = i === index;
104
+ return (
105
+ <Box key={opt.id} marginLeft={1}>
106
+ <Text color={isSelected ? "cyan" : "white"}>
107
+ {isSelected ? figures.arrowRight : " "}
108
+ {" "}
109
+ <Text bold={isSelected}>{opt.label}</Text>
110
+ <Text color="gray">
111
+ {" "}
112
+ {opt.description}
113
+ </Text>
114
+ </Text>
115
+ </Box>
116
+ );
117
+ })}
118
+ </Box>
119
+ <Text color="gray">↑↓ navigate · enter to select</Text>
120
+ </Box>
121
+ );
122
+ };
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import figures from "figures";
4
+
5
+ export const NoProviderPrompt = ({
6
+ onAccept,
7
+ onDecline,
8
+ }: {
9
+ onAccept: () => void;
10
+ onDecline: () => void;
11
+ }) => {
12
+ useInput((input, key) => {
13
+ if (input === "y" || input === "Y" || key.return) onAccept();
14
+ if (input === "n" || input === "N" || key.escape) onDecline();
15
+ });
16
+
17
+ return (
18
+ <Box flexDirection="column" gap={1}>
19
+ <Text color="yellow">{figures.warning} No API provider configured.</Text>
20
+ <Text>
21
+ Run setup now?{" "}
22
+ <Text color="green">[y] yes</Text>
23
+ {" "}
24
+ <Text color="red">[n] skip</Text>
25
+ </Text>
26
+ </Box>
27
+ );
28
+ };
@@ -0,0 +1,217 @@
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 { useEffect, useState, useRef } from "react";
6
+ import { ACCENT } from "../../colors";
7
+ import { detectPreview, runInstall, runDev } from "../../utils/preview";
8
+ import type { PreviewProcess } from "../../utils/preview";
9
+
10
+ type PreviewStage =
11
+ | { type: "detecting" }
12
+ | { type: "not-supported" }
13
+ | { type: "installing"; logs: string[] }
14
+ | { type: "starting"; logs: string[] }
15
+ | { type: "running"; port: number | null; logs: string[] }
16
+ | { type: "error"; message: string };
17
+
18
+ const MAX_LOGS = 8;
19
+
20
+ export const PreviewRunner = ({
21
+ repoPath,
22
+ onExit,
23
+ }: {
24
+ repoPath: string;
25
+ onExit: () => void;
26
+ }) => {
27
+ const [stage, setStage] = useState<PreviewStage>({ type: "detecting" });
28
+ const devProcess = useRef<PreviewProcess | null>(null);
29
+
30
+ useInput((_, key) => {
31
+ if (key.escape || (key.ctrl && _.toLowerCase() === "c")) {
32
+ devProcess.current?.kill();
33
+ onExit();
34
+ }
35
+ });
36
+
37
+ useEffect(() => {
38
+ const info = detectPreview(repoPath);
39
+ if (!info) {
40
+ setStage({ type: "not-supported" });
41
+ return;
42
+ }
43
+
44
+ setStage({ type: "installing", logs: [] });
45
+ const installer = runInstall(repoPath, info.installCmd);
46
+ const installLogs: string[] = [];
47
+
48
+ installer.onLog((line) => {
49
+ installLogs.push(line);
50
+ setStage({ type: "installing", logs: [...installLogs].slice(-MAX_LOGS) });
51
+ });
52
+ installer.onError((line) => {
53
+ installLogs.push(line);
54
+ setStage({ type: "installing", logs: [...installLogs].slice(-MAX_LOGS) });
55
+ });
56
+ installer.onExit((code) => {
57
+ if (code !== 0 && code !== null) {
58
+ setStage({
59
+ type: "error",
60
+ message: `Install failed with exit code ${code}`,
61
+ });
62
+ return;
63
+ }
64
+
65
+ setStage({ type: "starting", logs: [] });
66
+ const dev = runDev(repoPath, info.devCmd);
67
+ devProcess.current = dev;
68
+ const devLogs: string[] = [];
69
+
70
+ dev.onLog((line) => {
71
+ devLogs.push(line);
72
+ const isRunning =
73
+ line.toLowerCase().includes("localhost") ||
74
+ line.toLowerCase().includes("ready") ||
75
+ line.toLowerCase().includes("started") ||
76
+ line.toLowerCase().includes("listening");
77
+
78
+ if (isRunning) {
79
+ setStage({
80
+ type: "running",
81
+ port: info.port,
82
+ logs: [...devLogs].slice(-MAX_LOGS),
83
+ });
84
+ } else {
85
+ setStage((s) =>
86
+ s.type === "running"
87
+ ? { ...s, logs: [...devLogs].slice(-MAX_LOGS) }
88
+ : { type: "starting", logs: [...devLogs].slice(-MAX_LOGS) },
89
+ );
90
+ }
91
+ });
92
+
93
+ dev.onError((line) => {
94
+ devLogs.push(line);
95
+ setStage((s) =>
96
+ s.type === "running"
97
+ ? { ...s, logs: [...devLogs].slice(-MAX_LOGS) }
98
+ : { type: "starting", logs: [...devLogs].slice(-MAX_LOGS) },
99
+ );
100
+ });
101
+
102
+ dev.onExit((code) => {
103
+ if (code !== 0 && code !== null) {
104
+ setStage({
105
+ type: "error",
106
+ message: `Dev server exited with code ${code}`,
107
+ });
108
+ }
109
+ });
110
+ });
111
+
112
+ return () => {
113
+ devProcess.current?.kill();
114
+ };
115
+ }, [repoPath]);
116
+
117
+ if (stage.type === "detecting") {
118
+ return (
119
+ <Box marginTop={1}>
120
+ <Text color={ACCENT}>
121
+ <Spinner />
122
+ </Text>
123
+ <Box marginLeft={1}>
124
+ <Text>Detecting project type...</Text>
125
+ </Box>
126
+ </Box>
127
+ );
128
+ }
129
+
130
+ if (stage.type === "not-supported") {
131
+ return (
132
+ <Box marginTop={1}>
133
+ <Text color="yellow">
134
+ {figures.warning} No supported run configuration found (no
135
+ package.json, requirements.txt, etc.)
136
+ </Text>
137
+ </Box>
138
+ );
139
+ }
140
+
141
+ if (stage.type === "installing") {
142
+ return (
143
+ <Box flexDirection="column" marginTop={1} gap={1}>
144
+ <Box>
145
+ <Text color={ACCENT}>
146
+ <Spinner />
147
+ </Text>
148
+ <Box marginLeft={1}>
149
+ <Text>Installing dependencies...</Text>
150
+ </Box>
151
+ </Box>
152
+ <Box flexDirection="column" marginLeft={2}>
153
+ {stage.logs.map((log, i) => (
154
+ <Text key={i} color="gray">
155
+ {log}
156
+ </Text>
157
+ ))}
158
+ </Box>
159
+ </Box>
160
+ );
161
+ }
162
+
163
+ if (stage.type === "starting") {
164
+ return (
165
+ <Box flexDirection="column" marginTop={1} gap={1}>
166
+ <Box>
167
+ <Text color={ACCENT}>
168
+ <Spinner />
169
+ </Text>
170
+ <Box marginLeft={1}>
171
+ <Text>Starting dev server...</Text>
172
+ </Box>
173
+ </Box>
174
+ <Box flexDirection="column" marginLeft={2}>
175
+ {stage.logs.map((log, i) => (
176
+ <Text key={i} color="gray">
177
+ {log}
178
+ </Text>
179
+ ))}
180
+ </Box>
181
+ </Box>
182
+ );
183
+ }
184
+
185
+ if (stage.type === "running") {
186
+ return (
187
+ <Box flexDirection="column" marginTop={1} gap={1}>
188
+ <Box gap={1}>
189
+ <Text color="green">{figures.tick} Dev server running</Text>
190
+ {stage.port && (
191
+ <Text color="cyan">→ http://localhost:{stage.port}</Text>
192
+ )}
193
+ </Box>
194
+ <Box flexDirection="column" marginLeft={2}>
195
+ {stage.logs.map((log, i) => (
196
+ <Text key={i} color="gray">
197
+ {log}
198
+ </Text>
199
+ ))}
200
+ </Box>
201
+ <Text color="gray">ctrl+c or esc to stop</Text>
202
+ </Box>
203
+ );
204
+ }
205
+
206
+ if (stage.type === "error") {
207
+ return (
208
+ <Box marginTop={1}>
209
+ <Text color="red">
210
+ {figures.cross} {stage.message}
211
+ </Text>
212
+ </Box>
213
+ );
214
+ }
215
+
216
+ return null;
217
+ };
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import figures from "figures";
4
+ import { useEffect, useState } from "react";
5
+ import { loadConfig } from "../../utils/config";
6
+ import type { Provider } from "../../types/config";
7
+
8
+ export const ProviderPicker = ({
9
+ onDone,
10
+ }: {
11
+ onDone: (provider: Provider) => void;
12
+ }) => {
13
+ const [index, setIndex] = useState(0);
14
+ const config = loadConfig();
15
+ const providers = config.providers;
16
+
17
+ useEffect(() => {
18
+ if (providers.length === 1) {
19
+ onDone(providers[0]!);
20
+ }
21
+ }, []);
22
+
23
+ useInput((_, key) => {
24
+ if (providers.length <= 1) return;
25
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
26
+ if (key.downArrow) setIndex((i) => Math.min(providers.length - 1, i + 1));
27
+ if (key.return) {
28
+ onDone(providers[index]!);
29
+ }
30
+ });
31
+
32
+ if (providers.length === 0) {
33
+ return (
34
+ <Box marginTop={1}>
35
+ <Text color="red">
36
+ {figures.cross} No providers configured. Run{" "}
37
+ <Text color="cyan">lens init</Text> first.
38
+ </Text>
39
+ </Box>
40
+ );
41
+ }
42
+
43
+ if (providers.length === 1) {
44
+ return (
45
+ <Box marginTop={1}>
46
+ <Text color="gray">
47
+ {figures.arrowRight} Using{" "}
48
+ <Text color="cyan">{providers[0]!.name}</Text>
49
+ </Text>
50
+ </Box>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <Box flexDirection="column" marginTop={1} gap={1}>
56
+ <Text bold color="cyan">
57
+ Select provider
58
+ </Text>
59
+ {providers.map((p, i) => (
60
+ <Box key={p.id} marginLeft={1}>
61
+ <Text color={i === index ? "cyan" : "white"}>
62
+ {i === index ? figures.arrowRight : " "}
63
+ {" "}
64
+ <Text bold={i === index}>{p.name}</Text>
65
+ <Text color="gray">
66
+ {" "}
67
+ {p.type} · {p.model}
68
+ {config.defaultProviderId === p.id ? " · default" : ""}
69
+ </Text>
70
+ </Text>
71
+ </Box>
72
+ ))}
73
+ <Text color="gray">↑↓ navigate · enter to select</Text>
74
+ </Box>
75
+ );
76
+ };