git-reverse-cli 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/cli.js +2202 -0
  4. package/package.json +62 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2202 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+
6
+ // src/ui/App.tsx
7
+ import { useState as useState8, useEffect as useEffect2 } from "react";
8
+ import { Box as Box13, useInput as useInput4, useApp as useApp2 } from "ink";
9
+
10
+ // src/ui/components/Header.tsx
11
+ import { Box, Text } from "ink";
12
+
13
+ // src/utils/format.ts
14
+ function truncate(str, maxLen) {
15
+ if (str.length <= maxLen) return str;
16
+ return str.slice(0, maxLen - 3) + "...";
17
+ }
18
+ function timeAgo(isoDate) {
19
+ const diff = Date.now() - new Date(isoDate).getTime();
20
+ const mins = Math.floor(diff / 6e4);
21
+ const hours = Math.floor(mins / 60);
22
+ const days = Math.floor(hours / 24);
23
+ if (days > 0) return `${days}d ago`;
24
+ if (hours > 0) return `${hours}h ago`;
25
+ if (mins > 0) return `${mins}m ago`;
26
+ return "just now";
27
+ }
28
+ function formatModelId(id) {
29
+ const parts = id.split("/");
30
+ const name = parts[parts.length - 1] ?? id;
31
+ return name.replace(/:free$/, "").replace(/-it$/, "");
32
+ }
33
+ function formatModelProvider(id) {
34
+ return id.split("/")[0] ?? id;
35
+ }
36
+ function countWords(text) {
37
+ return text.trim().split(/\s+/).filter(Boolean).length;
38
+ }
39
+
40
+ // src/ui/components/Header.tsx
41
+ import { jsx, jsxs } from "react/jsx-runtime";
42
+ var LOGO = [
43
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
44
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
45
+ "\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 ",
46
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D ",
47
+ "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
48
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
49
+ ];
50
+ function Header({ username, model, version = "1.0.0", newModelCount = 0 }) {
51
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
52
+ LOGO.map((line, i) => /* @__PURE__ */ jsx(Text, { color: "cyan", bold: i < 4, dimColor: i >= 4, children: line }, i)),
53
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "row", gap: 2, children: [
54
+ /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
55
+ " ",
56
+ "v",
57
+ version
58
+ ] }),
59
+ model && /* @__PURE__ */ jsxs(Box, { children: [
60
+ /* @__PURE__ */ jsx(Text, { color: "white", dimColor: true, children: "\xB7" }),
61
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", dimColor: true, children: [
62
+ " ",
63
+ formatModelProvider(model)
64
+ ] }),
65
+ /* @__PURE__ */ jsx(Text, { color: "white", dimColor: true, children: "/" }),
66
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: formatModelId(model) })
67
+ ] }),
68
+ newModelCount > 0 && /* @__PURE__ */ jsxs(Box, { children: [
69
+ /* @__PURE__ */ jsx(Text, { color: "white", dimColor: true, children: "\xB7" }),
70
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
71
+ " ",
72
+ newModelCount,
73
+ " new model",
74
+ newModelCount > 1 ? "s" : "",
75
+ " available"
76
+ ] })
77
+ ] })
78
+ ] }),
79
+ username && /* @__PURE__ */ jsxs(Box, { marginTop: 0, paddingLeft: 2, children: [
80
+ /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
81
+ "Hello,",
82
+ " "
83
+ ] }),
84
+ /* @__PURE__ */ jsxs(Text, { color: "white", children: [
85
+ username,
86
+ "."
87
+ ] })
88
+ ] }),
89
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "white", dimColor: true, children: "\u2500".repeat(79) }) })
90
+ ] });
91
+ }
92
+
93
+ // src/ui/screens/SetupUsername.tsx
94
+ import { useState } from "react";
95
+ import { Box as Box2, Text as Text2 } from "ink";
96
+ import TextInput from "ink-text-input";
97
+
98
+ // src/config/store.ts
99
+ import Conf from "conf";
100
+ var store = new Conf({
101
+ projectName: "git-reverse",
102
+ projectVersion: "1.0.0",
103
+ defaults: {
104
+ user: {
105
+ name: "",
106
+ apiKey: "",
107
+ selectedModel: "",
108
+ seenModelIds: [],
109
+ setupComplete: false,
110
+ setupStep: "username"
111
+ },
112
+ cachedModels: {
113
+ models: [],
114
+ fetchedAt: 0
115
+ },
116
+ sessions: {},
117
+ lastLaunchVersion: ""
118
+ },
119
+ schema: {
120
+ user: {
121
+ type: "object",
122
+ properties: {
123
+ name: { type: "string" },
124
+ apiKey: { type: "string" },
125
+ selectedModel: { type: "string" },
126
+ seenModelIds: { type: "array", items: { type: "string" } },
127
+ setupComplete: { type: "boolean" },
128
+ setupStep: { type: "string" }
129
+ }
130
+ },
131
+ cachedModels: {
132
+ type: "object",
133
+ properties: {
134
+ models: { type: "array" },
135
+ fetchedAt: { type: "number" }
136
+ }
137
+ },
138
+ sessions: { type: "object" },
139
+ lastLaunchVersion: { type: "string" }
140
+ }
141
+ });
142
+ function getUser() {
143
+ return store.get("user");
144
+ }
145
+ function setUserName(name) {
146
+ const user = getUser();
147
+ store.set("user", { ...user, name: name.trim() });
148
+ }
149
+ function setApiKey(apiKey) {
150
+ const user = getUser();
151
+ store.set("user", { ...user, apiKey });
152
+ }
153
+ function setSelectedModel(modelId) {
154
+ const user = getUser();
155
+ store.set("user", { ...user, selectedModel: modelId });
156
+ }
157
+ function setSetupStep(step) {
158
+ const user = getUser();
159
+ store.set("user", { ...user, setupStep: step });
160
+ }
161
+ function markSetupComplete() {
162
+ const user = getUser();
163
+ store.set("user", { ...user, setupComplete: true, setupStep: "done" });
164
+ }
165
+ function markModelsSeen(modelIds) {
166
+ const user = getUser();
167
+ const combined = Array.from(/* @__PURE__ */ new Set([...user.seenModelIds, ...modelIds]));
168
+ store.set("user", { ...user, seenModelIds: combined });
169
+ }
170
+ function getCachedModels() {
171
+ return store.get("cachedModels");
172
+ }
173
+ function setCachedModels(models) {
174
+ store.set("cachedModels", { models, fetchedAt: Date.now() });
175
+ }
176
+ function getNewModels(fetchedModels) {
177
+ const { seenModelIds } = getUser();
178
+ return fetchedModels.filter((m) => !seenModelIds.includes(m.id));
179
+ }
180
+ function saveSession(session) {
181
+ const sessions = store.get("sessions");
182
+ store.set("sessions", { ...sessions, [session.id]: session });
183
+ }
184
+ function getSession(id) {
185
+ const sessions = store.get("sessions");
186
+ return sessions[id] ?? null;
187
+ }
188
+ function getAllSessions() {
189
+ const sessions = store.get("sessions");
190
+ return Object.values(sessions).sort(
191
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
192
+ );
193
+ }
194
+ function getStorePath() {
195
+ return store.path;
196
+ }
197
+
198
+ // src/ui/screens/SetupUsername.tsx
199
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
200
+ function SetupUsername({ onComplete }) {
201
+ const [value, setValue] = useState("");
202
+ const [error, setError] = useState("");
203
+ const handleSubmit = (val) => {
204
+ const trimmed = val.trim();
205
+ if (trimmed.length < 2) {
206
+ setError("Name must be at least 2 characters.");
207
+ return;
208
+ }
209
+ if (trimmed.length > 32) {
210
+ setError("Name must be 32 characters or fewer.");
211
+ return;
212
+ }
213
+ setUserName(trimmed);
214
+ setSetupStep("apikey");
215
+ onComplete(trimmed);
216
+ };
217
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, children: [
218
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "white", dimColor: true, children: "First time setup. This takes about 60 seconds." }) }),
219
+ /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
220
+ /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
221
+ "Step 1 of 3 \u2014 ",
222
+ " "
223
+ ] }),
224
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "Who are you?" })
225
+ ] }),
226
+ /* @__PURE__ */ jsxs2(Box2, { children: [
227
+ /* @__PURE__ */ jsx2(Text2, { color: "white", dimColor: true, children: " name " }),
228
+ /* @__PURE__ */ jsx2(
229
+ TextInput,
230
+ {
231
+ value,
232
+ onChange: (v) => {
233
+ setValue(v);
234
+ setError("");
235
+ },
236
+ onSubmit: handleSubmit,
237
+ placeholder: "Enter your name"
238
+ }
239
+ )
240
+ ] }),
241
+ error && /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
242
+ " ",
243
+ error
244
+ ] }) }),
245
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
246
+ " ",
247
+ "Press Enter to continue"
248
+ ] }) })
249
+ ] });
250
+ }
251
+
252
+ // src/ui/screens/SetupApiKey.tsx
253
+ import { useState as useState2 } from "react";
254
+ import { Box as Box4, Text as Text4 } from "ink";
255
+ import TextInput2 from "ink-text-input";
256
+ import terminalLink from "terminal-link";
257
+
258
+ // src/api/OpenRouterClient.ts
259
+ import axios from "axios";
260
+ var BASE_URL = "https://openrouter.ai/api/v1";
261
+ var CACHE_TTL_MS = 1e3 * 60 * 60;
262
+ var OpenRouterClient = class {
263
+ client;
264
+ apiKey;
265
+ constructor(apiKey) {
266
+ this.apiKey = apiKey;
267
+ this.client = axios.create({
268
+ baseURL: BASE_URL,
269
+ headers: {
270
+ Authorization: `Bearer ${apiKey}`,
271
+ "Content-Type": "application/json",
272
+ "HTTP-Referer": "https://github.com/git-reverse/git-reverse",
273
+ "X-Title": "git-reverse"
274
+ },
275
+ timeout: 3e4
276
+ });
277
+ }
278
+ // ── Validation ────────────────────────────────────────────
279
+ async validateKey() {
280
+ try {
281
+ const res = await this.client.get("/auth/key");
282
+ if (res.status === 200 && res.data?.data) {
283
+ return { valid: true };
284
+ }
285
+ return { valid: false, error: "Invalid API key response" };
286
+ } catch (err) {
287
+ if (axios.isAxiosError(err)) {
288
+ if (err.response?.status === 401) {
289
+ return { valid: false, error: "Invalid API key \u2014 check your key and try again." };
290
+ }
291
+ if (err.response?.status === 429) {
292
+ return { valid: false, error: "Rate limited \u2014 wait a moment and try again." };
293
+ }
294
+ return { valid: false, error: err.message };
295
+ }
296
+ return { valid: false, error: "Network error \u2014 check your connection." };
297
+ }
298
+ }
299
+ // ── Models ────────────────────────────────────────────────
300
+ async fetchModels() {
301
+ const res = await this.client.get("/models");
302
+ const models = res.data?.data ?? [];
303
+ return models;
304
+ }
305
+ async fetchFreeModels() {
306
+ const all = await this.fetchModels();
307
+ return all.filter((m) => {
308
+ const promptPrice = parseFloat(m.pricing?.prompt ?? "1");
309
+ const completionPrice = parseFloat(m.pricing?.completion ?? "1");
310
+ return promptPrice === 0 && completionPrice === 0;
311
+ });
312
+ }
313
+ // ── Streaming Completion ──────────────────────────────────
314
+ async *streamCompletion(model, messages, options = {}) {
315
+ const { maxTokens = 4096, temperature = 0.3 } = options;
316
+ const response = await this.client.post(
317
+ "/chat/completions",
318
+ {
319
+ model,
320
+ messages,
321
+ stream: true,
322
+ max_tokens: maxTokens,
323
+ temperature
324
+ },
325
+ {
326
+ responseType: "stream",
327
+ timeout: 12e4
328
+ }
329
+ );
330
+ const stream = response.data;
331
+ let buffer = "";
332
+ let receivedDone = false;
333
+ try {
334
+ for await (const chunk of stream) {
335
+ if (receivedDone) break;
336
+ buffer += chunk.toString("utf-8");
337
+ const lines = buffer.split("\n");
338
+ buffer = lines.pop() ?? "";
339
+ for (const line of lines) {
340
+ const trimmed = line.trim();
341
+ if (trimmed === "data: [DONE]") {
342
+ const done = { content: "", done: true };
343
+ options.onChunk?.(done);
344
+ yield done;
345
+ receivedDone = true;
346
+ break;
347
+ }
348
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
349
+ try {
350
+ const json = JSON.parse(trimmed.slice(6));
351
+ const content = json?.choices?.[0]?.delta?.content ?? "";
352
+ if (content) {
353
+ const streamChunk = { content, done: false };
354
+ options.onChunk?.(streamChunk);
355
+ yield streamChunk;
356
+ }
357
+ } catch {
358
+ }
359
+ }
360
+ }
361
+ } catch (err) {
362
+ if (receivedDone) return;
363
+ const msg = err instanceof Error ? err.message : String(err);
364
+ const isStreamClose = msg === "aborted" || msg.includes("ECONNRESET") || msg.includes("socket hang up") || msg.includes("premature close") || err instanceof Error && err.name === "AbortError";
365
+ if (!isStreamClose) throw err;
366
+ }
367
+ }
368
+ // ── One-shot Completion (non-streaming) ───────────────────
369
+ async complete(model, messages, options = {}) {
370
+ const { maxTokens = 4096, temperature = 0.3 } = options;
371
+ const res = await this.client.post("/chat/completions", {
372
+ model,
373
+ messages,
374
+ stream: false,
375
+ max_tokens: maxTokens,
376
+ temperature
377
+ });
378
+ return res.data?.choices?.[0]?.message?.content ?? "";
379
+ }
380
+ static isCacheStale(fetchedAt) {
381
+ return Date.now() - fetchedAt > CACHE_TTL_MS;
382
+ }
383
+ };
384
+
385
+ // src/ui/components/Spinner.tsx
386
+ import { Box as Box3, Text as Text3 } from "ink";
387
+ import InkSpinner from "ink-spinner";
388
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
389
+ function Spinner({ label, color = "cyan" }) {
390
+ return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 2, children: [
391
+ /* @__PURE__ */ jsx3(Text3, { color, children: /* @__PURE__ */ jsx3(InkSpinner, { type: "dots" }) }),
392
+ /* @__PURE__ */ jsxs3(Text3, { color: "white", dimColor: true, children: [
393
+ " ",
394
+ label
395
+ ] })
396
+ ] });
397
+ }
398
+
399
+ // src/ui/screens/SetupApiKey.tsx
400
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
401
+ var OPENROUTER_KEYS_URL = "https://openrouter.ai/keys";
402
+ var OPENROUTER_LINK = terminalLink("openrouter.ai/keys", OPENROUTER_KEYS_URL, {
403
+ fallback: (_, url) => url
404
+ });
405
+ function SetupApiKey({ onComplete }) {
406
+ const [value, setValue] = useState2("");
407
+ const [phase, setPhase] = useState2("input");
408
+ const [error, setError] = useState2("");
409
+ const [models, setModels] = useState2([]);
410
+ const handleSubmit = async (val) => {
411
+ const trimmed = val.trim();
412
+ if (!trimmed.startsWith("sk-or-")) {
413
+ setError('OpenRouter keys start with "sk-or-". Check your key.');
414
+ return;
415
+ }
416
+ setPhase("validating");
417
+ setError("");
418
+ try {
419
+ const client = new OpenRouterClient(trimmed);
420
+ const { valid, error: validError } = await client.validateKey();
421
+ if (!valid) {
422
+ setPhase("error");
423
+ setError(validError ?? "Validation failed.");
424
+ return;
425
+ }
426
+ const freeModels = await client.fetchFreeModels();
427
+ setApiKey(trimmed);
428
+ setCachedModels(freeModels);
429
+ markModelsSeen(freeModels.map((m) => m.id));
430
+ setSetupStep("model");
431
+ setModels(freeModels);
432
+ setPhase("done");
433
+ setTimeout(() => onComplete(freeModels), 600);
434
+ } catch (err) {
435
+ setPhase("error");
436
+ setError(err instanceof Error ? err.message : "Unknown error during validation.");
437
+ }
438
+ };
439
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingLeft: 2, children: [
440
+ /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
441
+ /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
442
+ "Step 2 of 3 \u2014 ",
443
+ " "
444
+ ] }),
445
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "OpenRouter API Key" })
446
+ ] }),
447
+ /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, flexDirection: "column", children: [
448
+ /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
449
+ " ",
450
+ "Get a free key at",
451
+ " ",
452
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: OPENROUTER_LINK })
453
+ ] }),
454
+ /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
455
+ " ",
456
+ "Create an account \u2192 API Keys \u2192 Create Key \u2192 copy it below."
457
+ ] })
458
+ ] }),
459
+ phase === "input" || phase === "error" ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
460
+ /* @__PURE__ */ jsxs4(Box4, { children: [
461
+ /* @__PURE__ */ jsx4(Text4, { color: "white", dimColor: true, children: " key " }),
462
+ /* @__PURE__ */ jsx4(
463
+ TextInput2,
464
+ {
465
+ value,
466
+ onChange: (v) => {
467
+ setValue(v);
468
+ setError("");
469
+ if (phase === "error") setPhase("input");
470
+ },
471
+ onSubmit: handleSubmit,
472
+ placeholder: "sk-or-v1-...",
473
+ mask: "*"
474
+ }
475
+ )
476
+ ] }),
477
+ error && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
478
+ " ",
479
+ error
480
+ ] }) }),
481
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
482
+ " ",
483
+ "Press Enter to validate"
484
+ ] }) })
485
+ ] }) : phase === "validating" ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
486
+ /* @__PURE__ */ jsx4(Spinner, { label: "Validating key..." }),
487
+ /* @__PURE__ */ jsx4(Spinner, { label: "Fetching free models...", color: "white" })
488
+ ] }) : /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
489
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: "green", children: [
490
+ " ",
491
+ "\u2713 Key valid"
492
+ ] }) }),
493
+ /* @__PURE__ */ jsxs4(Box4, { children: [
494
+ /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
495
+ " ",
496
+ "Found",
497
+ " "
498
+ ] }),
499
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: models.length }),
500
+ /* @__PURE__ */ jsxs4(Text4, { color: "white", dimColor: true, children: [
501
+ " ",
502
+ "free model",
503
+ models.length !== 1 ? "s" : ""
504
+ ] })
505
+ ] })
506
+ ] })
507
+ ] });
508
+ }
509
+
510
+ // src/ui/screens/SetupModel.tsx
511
+ import { useState as useState3, useMemo } from "react";
512
+ import { Box as Box5, Text as Text5 } from "ink";
513
+ import SelectInput from "ink-select-input";
514
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
515
+ function SetupModel({ models, isSettings = false, onComplete }) {
516
+ const [selected, setSelected] = useState3(null);
517
+ const sortedModels = useMemo(
518
+ () => [...models].sort((a, b) => (b.context_length ?? 0) - (a.context_length ?? 0)),
519
+ [models]
520
+ );
521
+ const modelMap = useMemo(
522
+ () => new Map(sortedModels.map((m) => [m.id, m])),
523
+ [sortedModels]
524
+ );
525
+ const items = sortedModels.map((m) => ({
526
+ label: formatItem(m),
527
+ value: m.id
528
+ }));
529
+ const handleSelect = (item) => {
530
+ const model = modelMap.get(item.value);
531
+ if (!model) return;
532
+ setSelectedModel(item.value);
533
+ if (!isSettings) markSetupComplete();
534
+ setSelected(model);
535
+ setTimeout(() => onComplete(model), 300);
536
+ };
537
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingLeft: 2, children: [
538
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
539
+ /* @__PURE__ */ jsx5(Text5, { color: "white", dimColor: true, children: isSettings ? "Change active model \u2014 " : "Step 3 of 3 \u2014 " }),
540
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "Select a model" })
541
+ ] }),
542
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "white", dimColor: true, children: [
543
+ " ",
544
+ models.length,
545
+ " free model",
546
+ models.length !== 1 ? "s" : "",
547
+ " available \xB7 \u2191\u2193 navigate \xB7 Enter select"
548
+ ] }) }),
549
+ selected ? /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, children: [
550
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u2713 " }),
551
+ /* @__PURE__ */ jsx5(Text5, { color: "white", children: formatModelId(selected.id) }),
552
+ /* @__PURE__ */ jsxs5(Text5, { color: "white", dimColor: true, children: [
553
+ " ",
554
+ "set as default"
555
+ ] })
556
+ ] }) : /* @__PURE__ */ jsxs5(Box5, { paddingLeft: 2, flexDirection: "column", children: [
557
+ /* @__PURE__ */ jsx5(Box5, { marginBottom: 0, children: /* @__PURE__ */ jsxs5(Text5, { color: "white", dimColor: true, children: [
558
+ " PROVIDER/MODEL".padEnd(36),
559
+ "CTX"
560
+ ] }) }),
561
+ /* @__PURE__ */ jsx5(
562
+ SelectInput,
563
+ {
564
+ items,
565
+ onSelect: handleSelect,
566
+ itemComponent: ModelItem
567
+ }
568
+ )
569
+ ] })
570
+ ] });
571
+ }
572
+ function formatItem(m) {
573
+ const ctx = m.context_length ? `${(m.context_length / 1e3).toFixed(0)}k` : "\u2014";
574
+ const name = `${formatModelProvider(m.id)}/${formatModelId(m.id)}`;
575
+ return `${truncate(name, 34).padEnd(36)}${ctx}`;
576
+ }
577
+ function ModelItem({
578
+ isSelected,
579
+ label
580
+ }) {
581
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: isSelected ? "cyan" : "white", dimColor: !isSelected, children: [
582
+ isSelected ? "\u203A " : " ",
583
+ label
584
+ ] }) });
585
+ }
586
+
587
+ // src/ui/screens/Dashboard.tsx
588
+ import { useState as useState4 } from "react";
589
+ import { Box as Box8, Text as Text8, useInput as useInput2 } from "ink";
590
+ import TextInput3 from "ink-text-input";
591
+
592
+ // src/ui/components/CommandPalette.tsx
593
+ import { Box as Box6, Text as Text6 } from "ink";
594
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
595
+ var COMMANDS = [
596
+ {
597
+ name: "settings",
598
+ aliases: ["\\settings"],
599
+ description: "to open settings page for any modification",
600
+ hint: "settings"
601
+ },
602
+ {
603
+ name: "compact",
604
+ aliases: ["\\compact", "\\summarize"],
605
+ description: "to summarize the session",
606
+ hint: "compact"
607
+ },
608
+ {
609
+ name: "deepdive",
610
+ aliases: ["\\deepdive", "\\learn"],
611
+ description: "for deeply analyzing and giving a very detailed prompt in such a way that user can understand and also the following prompt can be used the same project from scratch according to the user",
612
+ hint: "deepdive"
613
+ },
614
+ {
615
+ name: "resume",
616
+ aliases: ["\\resume <id>"],
617
+ description: "Resume a previous session by its ID",
618
+ hint: "resume"
619
+ },
620
+ {
621
+ name: "sessions",
622
+ aliases: ["\\sessions"],
623
+ description: "List all saved sessions",
624
+ hint: "sessions"
625
+ },
626
+ {
627
+ name: "clear",
628
+ aliases: ["\\clear"],
629
+ description: "Clear the current output buffer",
630
+ hint: "clear"
631
+ },
632
+ {
633
+ name: "quit",
634
+ aliases: ["\\quit", "\\exit"],
635
+ description: "Save session and exit",
636
+ hint: "quit"
637
+ }
638
+ ];
639
+ function CommandPalette({ visible }) {
640
+ if (!visible) return null;
641
+ const COL1 = 24;
642
+ return /* @__PURE__ */ jsxs6(
643
+ Box6,
644
+ {
645
+ flexDirection: "column",
646
+ borderStyle: "single",
647
+ borderColor: "cyan",
648
+ paddingX: 2,
649
+ paddingY: 0,
650
+ marginBottom: 1,
651
+ marginLeft: 2,
652
+ children: [
653
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", dimColor: true, children: "Commands" }),
654
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 0, children: /* @__PURE__ */ jsx6(Text6, { color: "white", dimColor: true, children: "\u2500".repeat(40) }) }),
655
+ COMMANDS.map((cmd) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", children: [
656
+ /* @__PURE__ */ jsx6(Box6, { width: COL1, children: /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: cmd.aliases.join(", ") }) }),
657
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { color: "white", dimColor: true, children: cmd.description }) })
658
+ ] }, cmd.name)),
659
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 0, children: /* @__PURE__ */ jsx6(Text6, { color: "white", dimColor: true, children: "\u2500".repeat(40) }) }),
660
+ /* @__PURE__ */ jsx6(Text6, { color: "white", dimColor: true, children: "esc close palette" })
661
+ ]
662
+ }
663
+ );
664
+ }
665
+
666
+ // src/ui/components/Notification.tsx
667
+ import { Box as Box7, Text as Text7 } from "ink";
668
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
669
+ function Notification({ newModels }) {
670
+ if (newModels.length === 0) return null;
671
+ return /* @__PURE__ */ jsxs7(
672
+ Box7,
673
+ {
674
+ flexDirection: "column",
675
+ borderStyle: "single",
676
+ borderColor: "green",
677
+ paddingX: 2,
678
+ paddingY: 0,
679
+ marginBottom: 1,
680
+ marginLeft: 2,
681
+ children: [
682
+ /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
683
+ "\u2191 ",
684
+ newModels.length,
685
+ " new free model",
686
+ newModels.length > 1 ? "s" : "",
687
+ " on OpenRouter"
688
+ ] }),
689
+ newModels.slice(0, 3).map((m) => /* @__PURE__ */ jsxs7(Text7, { color: "white", dimColor: true, children: [
690
+ " ",
691
+ formatModelProvider(m.id),
692
+ "/",
693
+ formatModelId(m.id),
694
+ " ",
695
+ /* @__PURE__ */ jsx7(Text7, { color: "white", dimColor: true, children: m.context_length ? `${(m.context_length / 1e3).toFixed(0)}k ctx` : "" })
696
+ ] }, m.id)),
697
+ newModels.length > 3 && /* @__PURE__ */ jsxs7(Text7, { color: "white", dimColor: true, children: [
698
+ " ",
699
+ "+",
700
+ newModels.length - 3,
701
+ " more \xB7 run \\settings to view all"
702
+ ] })
703
+ ]
704
+ }
705
+ );
706
+ }
707
+
708
+ // src/utils/github.ts
709
+ function parseGitHubInput(input) {
710
+ const trimmed = input.trim();
711
+ const urlMatch = trimmed.match(/(https?:\/\/github\.com\/[^\s]+)/);
712
+ if (!urlMatch) {
713
+ return { parsed: null, query: trimmed };
714
+ }
715
+ const urlStr = urlMatch[1];
716
+ const urlIndex = trimmed.indexOf(urlStr);
717
+ const before = trimmed.slice(0, urlIndex).trim();
718
+ const after = trimmed.slice(urlIndex + urlStr.length).trim();
719
+ const query = [before, after].filter(Boolean).join(" ");
720
+ const urlPattern = /github\.com\/([^/\s]+)\/([^/\s?#]+)(?:\/tree\/([^/\s?#]+))?/;
721
+ const match = urlStr.match(urlPattern);
722
+ if (!match) return { parsed: null, query };
723
+ return {
724
+ parsed: {
725
+ owner: match[1],
726
+ repo: match[2].replace(/\.git$/, ""),
727
+ branch: match[3],
728
+ raw: urlStr
729
+ },
730
+ query
731
+ };
732
+ }
733
+
734
+ // src/core/SessionManager.ts
735
+ import { nanoid } from "nanoid";
736
+ var SessionManager = class {
737
+ currentSession = null;
738
+ sigintHandler = null;
739
+ // ── Create new session ────────────────────────────────────
740
+ createSession(params) {
741
+ const now = (/* @__PURE__ */ new Date()).toISOString();
742
+ const session = {
743
+ id: nanoid(8),
744
+ createdAt: now,
745
+ updatedAt: now,
746
+ repoUrl: params.repoUrl,
747
+ query: params.query,
748
+ model: params.model,
749
+ messages: [],
750
+ analysisOutput: "",
751
+ repoMeta: params.repoMeta,
752
+ mode: params.mode
753
+ };
754
+ this.currentSession = session;
755
+ return session;
756
+ }
757
+ // ── Load existing session ─────────────────────────────────
758
+ loadSession(id) {
759
+ const session = getSession(id);
760
+ if (session) {
761
+ this.currentSession = session;
762
+ }
763
+ return session;
764
+ }
765
+ // ── Update session ────────────────────────────────────────
766
+ appendMessage(message) {
767
+ if (!this.currentSession) return;
768
+ this.currentSession.messages.push(message);
769
+ this.currentSession.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
770
+ }
771
+ appendOutput(chunk) {
772
+ if (!this.currentSession) return;
773
+ this.currentSession.analysisOutput += chunk;
774
+ this.currentSession.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
775
+ }
776
+ setAnalysisOutput(output) {
777
+ if (!this.currentSession) return;
778
+ this.currentSession.analysisOutput = output;
779
+ this.currentSession.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
780
+ }
781
+ updateRepoMeta(meta) {
782
+ if (!this.currentSession) return;
783
+ this.currentSession.repoMeta = meta;
784
+ }
785
+ // ── Persist session ───────────────────────────────────────
786
+ save() {
787
+ if (!this.currentSession) return null;
788
+ saveSession(this.currentSession);
789
+ return this.currentSession;
790
+ }
791
+ getCurrent() {
792
+ return this.currentSession;
793
+ }
794
+ // ── Static helpers ────────────────────────────────────────
795
+ static listSessions() {
796
+ return getAllSessions();
797
+ }
798
+ static getSession(id) {
799
+ return getSession(id);
800
+ }
801
+ static formatSessionId(id) {
802
+ return `#${id}`;
803
+ }
804
+ // ── SIGINT handler ────────────────────────────────────────
805
+ // Registers a handler that saves the session on Ctrl+C
806
+ // and calls the provided onExit callback with the session ID
807
+ registerSigintHandler(onExit) {
808
+ this.sigintHandler = () => {
809
+ const saved = this.save();
810
+ onExit(saved?.id ?? null);
811
+ process.exit(0);
812
+ };
813
+ process.on("SIGINT", this.sigintHandler);
814
+ }
815
+ removeSigintHandler() {
816
+ if (this.sigintHandler) {
817
+ process.removeListener("SIGINT", this.sigintHandler);
818
+ this.sigintHandler = null;
819
+ }
820
+ }
821
+ };
822
+
823
+ // src/ui/screens/Dashboard.tsx
824
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
825
+ function Dashboard({ username, model, newModels, onAnalyze, onCommand }) {
826
+ const [inputValue, setInputValue] = useState4("");
827
+ const [error, setError] = useState4("");
828
+ const [showPalette, setShowPalette] = useState4(false);
829
+ const [mode, setMode] = useState4("standard");
830
+ const recentSessions = SessionManager.listSessions().slice(0, 3);
831
+ useInput2((input, key) => {
832
+ if (key.escape && showPalette) {
833
+ setShowPalette(false);
834
+ return;
835
+ }
836
+ if (input === "\\" && !showPalette) {
837
+ setShowPalette(true);
838
+ return;
839
+ }
840
+ });
841
+ const handleSubmit = (val) => {
842
+ const trimmed = val.trim();
843
+ if (!trimmed) return;
844
+ if (trimmed.startsWith("\\")) {
845
+ handleCommand(trimmed);
846
+ return;
847
+ }
848
+ const { parsed, query } = parseGitHubInput(trimmed);
849
+ if (!parsed) {
850
+ setError("No GitHub URL detected. Paste a URL like: https://github.com/owner/repo");
851
+ return;
852
+ }
853
+ setError("");
854
+ const detectedMode = detectMode(query);
855
+ onAnalyze(parsed.raw, query, detectedMode);
856
+ };
857
+ const handleCommand = (cmd) => {
858
+ const lower = cmd.toLowerCase().trim();
859
+ setShowPalette(false);
860
+ setInputValue("");
861
+ if (lower === "\\settings") return onCommand("settings");
862
+ if (lower === "\\compact" || lower === "\\summarize") return onCommand("compact");
863
+ if (lower === "\\deepdive" || lower === "\\learn") {
864
+ setMode("deep");
865
+ setInputValue("");
866
+ return;
867
+ }
868
+ if (lower === "\\sessions") return onCommand("sessions");
869
+ if (lower === "\\clear") return onCommand("clear");
870
+ if (lower.startsWith("\\resume ")) {
871
+ const id = lower.slice(8).trim();
872
+ return onCommand(`resume:${id}`);
873
+ }
874
+ if (lower === "\\quit" || lower === "\\exit") return onCommand("quit");
875
+ setError(`Unknown command: ${cmd}. Press \\ to see all commands.`);
876
+ };
877
+ const detectMode = (query) => {
878
+ if (mode === "deep") return "deep";
879
+ const lower = query.toLowerCase();
880
+ if (/deep.?dive|in.?depth|detailed|understand|learn|explain/.test(lower)) return "deep";
881
+ return "standard";
882
+ };
883
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingLeft: 2, children: [
884
+ newModels.length > 0 && /* @__PURE__ */ jsx8(Notification, { newModels }),
885
+ /* @__PURE__ */ jsx8(CommandPalette, { visible: showPalette }),
886
+ mode === "deep" && /* @__PURE__ */ jsxs8(Box8, { marginBottom: 1, children: [
887
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: " \u25C8 Deep-dive mode active" }),
888
+ /* @__PURE__ */ jsx8(Text8, { color: "white", dimColor: true, children: " \u2014 detailed educational output" })
889
+ ] }),
890
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
891
+ " ",
892
+ "Paste a GitHub URL to reverse-engineer it."
893
+ ] }) }),
894
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
895
+ /* @__PURE__ */ jsxs8(Box8, { children: [
896
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", dimColor: true, children: " \u203A " }),
897
+ /* @__PURE__ */ jsx8(
898
+ TextInput3,
899
+ {
900
+ value: inputValue,
901
+ onChange: (v) => {
902
+ setInputValue(v);
903
+ setError("");
904
+ },
905
+ onSubmit: handleSubmit,
906
+ placeholder: "https://github.com/owner/repo [optional: your question]"
907
+ }
908
+ )
909
+ ] }),
910
+ error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
911
+ " ",
912
+ error
913
+ ] }) }),
914
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
915
+ /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
916
+ " ",
917
+ "Enter to analyze \xB7 \\\\ for commands \xB7 \\deepdive for detailed mode"
918
+ ] }),
919
+ mode === "deep" && /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
920
+ " ",
921
+ "\\\\ + Enter to exit deep-dive mode"
922
+ ] })
923
+ ] })
924
+ ] }),
925
+ recentSessions.length > 0 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 2, children: [
926
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 0, children: /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
927
+ " ",
928
+ "\u2500".repeat(44)
929
+ ] }) }),
930
+ /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
931
+ " ",
932
+ "Recent sessions"
933
+ ] }),
934
+ recentSessions.map((s) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", paddingLeft: 2, children: [
935
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", dimColor: true, children: [
936
+ "#",
937
+ s.id,
938
+ " "
939
+ ] }),
940
+ /* @__PURE__ */ jsx8(Text8, { color: "white", dimColor: true, children: s.repoUrl.replace("https://github.com/", "") }),
941
+ /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
942
+ " ",
943
+ timeAgo(s.updatedAt)
944
+ ] })
945
+ ] }, s.id)),
946
+ /* @__PURE__ */ jsxs8(Text8, { color: "white", dimColor: true, children: [
947
+ " ",
948
+ "\\\\resume ",
949
+ "<id>",
950
+ " to continue"
951
+ ] })
952
+ ] })
953
+ ] });
954
+ }
955
+
956
+ // src/ui/screens/Analysis.tsx
957
+ import { useEffect, useState as useState5, useRef } from "react";
958
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
959
+
960
+ // src/api/GitHubClient.ts
961
+ import { Octokit } from "@octokit/rest";
962
+ var KEY_FILE_PATTERNS = [
963
+ { pattern: /^README(\.(md|txt|rst))?$/i, role: "readme" },
964
+ { pattern: /^package\.json$/, role: "package" },
965
+ { pattern: /^pyproject\.toml$/, role: "package" },
966
+ { pattern: /^Cargo\.toml$/, role: "package" },
967
+ { pattern: /^go\.mod$/, role: "package" },
968
+ { pattern: /^pom\.xml$/, role: "package" },
969
+ { pattern: /^build\.gradle(\.kts)?$/, role: "package" },
970
+ { pattern: /^Gemfile$/, role: "package" },
971
+ { pattern: /^requirements\.txt$/, role: "package" },
972
+ { pattern: /^tsconfig\.json$/, role: "config" },
973
+ { pattern: /^\.eslintrc(\.(js|json|yml|yaml))?$/, role: "config" },
974
+ { pattern: /^eslint\.config\.(js|ts|mjs)$/, role: "config" },
975
+ { pattern: /^vite\.config\.(js|ts)$/, role: "config" },
976
+ { pattern: /^webpack\.config\.(js|ts)$/, role: "config" },
977
+ { pattern: /^next\.config\.(js|ts|mjs)$/, role: "config" },
978
+ { pattern: /^docker-compose\.(yml|yaml)$/, role: "config" },
979
+ { pattern: /^Dockerfile$/, role: "config" },
980
+ { pattern: /^\.env\.example$/, role: "config" },
981
+ { pattern: /^main\.(ts|js|py|go|rs|rb)$/, role: "entrypoint" },
982
+ { pattern: /^index\.(ts|js|tsx|jsx)$/, role: "entrypoint" },
983
+ { pattern: /^app\.(ts|js|tsx|jsx|py)$/, role: "entrypoint" },
984
+ { pattern: /^server\.(ts|js|py)$/, role: "entrypoint" },
985
+ { pattern: /^schema\.(prisma|graphql|sql)$/, role: "schema" },
986
+ { pattern: /^models?\.(ts|js|py)$/, role: "schema" }
987
+ ];
988
+ var MAX_FILE_SIZE_BYTES = 8e4;
989
+ var MAX_KEY_FILES = 12;
990
+ var MAX_TREE_DEPTH = 4;
991
+ var GitHubClient = class _GitHubClient {
992
+ octokit;
993
+ constructor(githubToken) {
994
+ this.octokit = new Octokit({
995
+ auth: githubToken,
996
+ userAgent: "git-reverse/1.0.0"
997
+ });
998
+ }
999
+ // ── URL Parsing ───────────────────────────────────────────
1000
+ static parseUrl(url) {
1001
+ const parts = url.trim().split(/\s+/);
1002
+ const rawUrl = parts[0] ?? "";
1003
+ const patterns = [
1004
+ /github\.com\/([^/]+)\/([^/?\s]+)(?:\/tree\/([^/?\s]+))?(?:\/(.+))?/
1005
+ ];
1006
+ for (const pattern of patterns) {
1007
+ const match = rawUrl.match(pattern);
1008
+ if (match) {
1009
+ return {
1010
+ owner: match[1],
1011
+ repo: match[2].replace(/\.git$/, ""),
1012
+ branch: match[3],
1013
+ subPath: match[4],
1014
+ raw: rawUrl
1015
+ };
1016
+ }
1017
+ }
1018
+ return null;
1019
+ }
1020
+ // ── Repo Metadata ─────────────────────────────────────────
1021
+ async fetchRepoMeta(owner, repo) {
1022
+ const { data } = await this.octokit.repos.get({ owner, repo });
1023
+ return {
1024
+ owner,
1025
+ repo,
1026
+ description: data.description ?? "",
1027
+ stars: data.stargazers_count ?? 0,
1028
+ forks: data.forks_count ?? 0,
1029
+ language: data.language ?? "Unknown",
1030
+ topics: data.topics ?? [],
1031
+ defaultBranch: data.default_branch ?? "main",
1032
+ size: data.size ?? 0,
1033
+ createdAt: data.created_at ?? "",
1034
+ updatedAt: data.updated_at ?? ""
1035
+ };
1036
+ }
1037
+ // ── File Tree ─────────────────────────────────────────────
1038
+ async fetchRepoTree(owner, repo, branch) {
1039
+ try {
1040
+ const { data } = await this.octokit.git.getTree({
1041
+ owner,
1042
+ repo,
1043
+ tree_sha: branch,
1044
+ recursive: "1"
1045
+ });
1046
+ return (data.tree ?? []).filter((item) => item.path && item.type).filter((item) => {
1047
+ const depth = (item.path ?? "").split("/").length;
1048
+ const isArtifact = /^(node_modules|\.git|dist|build|\.next|__pycache__|\.venv|vendor|target)\//.test(
1049
+ item.path ?? ""
1050
+ );
1051
+ return depth <= MAX_TREE_DEPTH && !isArtifact;
1052
+ }).map((item) => ({
1053
+ path: item.path,
1054
+ type: item.type === "tree" ? "dir" : "file",
1055
+ size: item.size,
1056
+ sha: item.sha
1057
+ }));
1058
+ } catch {
1059
+ const { data } = await this.octokit.repos.getContent({ owner, repo, path: "" });
1060
+ if (!Array.isArray(data)) return [];
1061
+ return data.map((item) => ({
1062
+ path: item.path,
1063
+ type: item.type === "dir" ? "dir" : "file",
1064
+ size: "size" in item ? item.size : void 0,
1065
+ sha: item.sha
1066
+ }));
1067
+ }
1068
+ }
1069
+ // ── Key File Detection ────────────────────────────────────
1070
+ static identifyKeyFiles(tree) {
1071
+ const identified = [];
1072
+ const ARTIFACT_RE = /^(node_modules|\.git|dist|build|\.next|__pycache__|\.venv|vendor|target)\//;
1073
+ for (const file of tree) {
1074
+ if (file.type !== "file") continue;
1075
+ if (ARTIFACT_RE.test(file.path)) continue;
1076
+ const filename = file.path.split("/").pop() ?? "";
1077
+ const depth = file.path.split("/").length;
1078
+ for (let i = 0; i < KEY_FILE_PATTERNS.length; i++) {
1079
+ const { pattern, role } = KEY_FILE_PATTERNS[i];
1080
+ if (pattern.test(filename)) {
1081
+ identified.push({ path: file.path, role, priority: i + depth * 10 });
1082
+ break;
1083
+ }
1084
+ }
1085
+ }
1086
+ return identified.sort((a, b) => a.priority - b.priority).slice(0, MAX_KEY_FILES).map(({ path, role }) => ({ path, role }));
1087
+ }
1088
+ // ── File Content Fetch ────────────────────────────────────
1089
+ async fetchFileContent(owner, repo, path) {
1090
+ try {
1091
+ const { data } = await this.octokit.repos.getContent({ owner, repo, path });
1092
+ if ("content" in data && typeof data.content === "string") {
1093
+ if ((data.size ?? 0) > MAX_FILE_SIZE_BYTES) {
1094
+ return `[File too large to display \u2014 ${data.size} bytes]`;
1095
+ }
1096
+ return Buffer.from(data.content, "base64").toString("utf-8");
1097
+ }
1098
+ return null;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ // ── Full Analysis Fetch ───────────────────────────────────
1104
+ async analyzeRepo(owner, repo) {
1105
+ const meta = await this.fetchRepoMeta(owner, repo);
1106
+ const tree = await this.fetchRepoTree(owner, repo, meta.defaultBranch);
1107
+ const keyFilePaths = _GitHubClient.identifyKeyFiles(tree);
1108
+ const keyFiles = [];
1109
+ const chunks = chunkArray(keyFilePaths, 5);
1110
+ for (const chunk of chunks) {
1111
+ const results = await Promise.allSettled(
1112
+ chunk.map(async ({ path, role }) => {
1113
+ const content = await this.fetchFileContent(owner, repo, path);
1114
+ return { path, content: content ?? "", role };
1115
+ })
1116
+ );
1117
+ for (const result of results) {
1118
+ if (result.status === "fulfilled" && result.value.content) {
1119
+ keyFiles.push(result.value);
1120
+ }
1121
+ }
1122
+ }
1123
+ const dependencyMap = parseDependencies(keyFiles);
1124
+ const techStack = detectTechStack(meta, tree, keyFiles, dependencyMap);
1125
+ const entryPoints = tree.filter((f) => keyFilePaths.some((k) => k.path === f.path && k.role === "entrypoint")).map((f) => f.path);
1126
+ const configFiles = tree.filter((f) => keyFilePaths.some((k) => k.path === f.path && k.role === "config")).map((f) => f.path);
1127
+ return { meta, tree, keyFiles, dependencyMap, techStack, entryPoints, configFiles };
1128
+ }
1129
+ };
1130
+ function chunkArray(arr, size) {
1131
+ const chunks = [];
1132
+ for (let i = 0; i < arr.length; i += size) {
1133
+ chunks.push(arr.slice(i, i + size));
1134
+ }
1135
+ return chunks;
1136
+ }
1137
+ function parseDependencies(keyFiles) {
1138
+ const pkgFile = keyFiles.find((f) => f.path === "package.json" || f.path.endsWith("/package.json"));
1139
+ if (pkgFile) {
1140
+ try {
1141
+ const pkg = JSON.parse(pkgFile.content);
1142
+ return {
1143
+ runtime: pkg.dependencies ?? {},
1144
+ dev: pkg.devDependencies ?? {},
1145
+ peers: pkg.peerDependencies,
1146
+ packageManager: pkg.packageManager,
1147
+ lockFile: "package-lock.json / yarn.lock / pnpm-lock.yaml"
1148
+ };
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ return { runtime: {}, dev: {} };
1153
+ }
1154
+ function detectTechStack(meta, tree, keyFiles, deps) {
1155
+ const stack = /* @__PURE__ */ new Set();
1156
+ if (meta.language) stack.add(meta.language);
1157
+ const allDeps = { ...deps.runtime, ...deps.dev };
1158
+ const depKeys = Object.keys(allDeps);
1159
+ const techMap = {
1160
+ React: ["react", "react-dom"],
1161
+ "Next.js": ["next"],
1162
+ "Vue.js": ["vue"],
1163
+ "Svelte": ["svelte"],
1164
+ "Angular": ["@angular/core"],
1165
+ "Vite": ["vite"],
1166
+ "Express": ["express"],
1167
+ "Fastify": ["fastify"],
1168
+ "NestJS": ["@nestjs/core"],
1169
+ "Prisma": ["prisma", "@prisma/client"],
1170
+ "TypeORM": ["typeorm"],
1171
+ "GraphQL": ["graphql", "apollo-server"],
1172
+ "tRPC": ["@trpc/server"],
1173
+ "Tailwind CSS": ["tailwindcss"],
1174
+ "Framer Motion": ["framer-motion"],
1175
+ "Ink (CLI)": ["ink"],
1176
+ "Electron": ["electron"],
1177
+ "Tauri": ["@tauri-apps/api"],
1178
+ "Vitest": ["vitest"],
1179
+ "Jest": ["jest"],
1180
+ "ESLint": ["eslint"],
1181
+ "TypeScript": ["typescript"],
1182
+ "Zod": ["zod"],
1183
+ "Redux": ["redux", "@reduxjs/toolkit"],
1184
+ "Zustand": ["zustand"],
1185
+ "Axios": ["axios"],
1186
+ "Socket.io": ["socket.io"],
1187
+ "Docker": [],
1188
+ "Kubernetes": []
1189
+ };
1190
+ for (const [tech, pkgs] of Object.entries(techMap)) {
1191
+ if (pkgs.some((p) => depKeys.includes(p))) {
1192
+ stack.add(tech);
1193
+ }
1194
+ }
1195
+ const paths = tree.map((f) => f.path);
1196
+ if (paths.some((p) => p.endsWith("Dockerfile") || p.includes("docker-compose"))) {
1197
+ stack.add("Docker");
1198
+ }
1199
+ if (paths.some((p) => p.includes("k8s/") || p.endsWith(".yaml") && p.includes("kubernetes"))) {
1200
+ stack.add("Kubernetes");
1201
+ }
1202
+ if (paths.some((p) => p.endsWith(".prisma"))) {
1203
+ stack.add("Prisma");
1204
+ }
1205
+ return Array.from(stack);
1206
+ }
1207
+
1208
+ // src/core/PromptBuilder.ts
1209
+ var PromptBuilder = class _PromptBuilder {
1210
+ // ── System Prompt ─────────────────────────────────────────
1211
+ static buildSystemPrompt() {
1212
+ return {
1213
+ role: "system",
1214
+ content: `You are git-reverse, an expert software architect and reverse-engineering analyst.
1215
+
1216
+ Your task is to analyze GitHub repository data and produce a comprehensive, accurate, structured recreation prompt that:
1217
+ 1. Explains exactly how the project was originally conceived and built
1218
+ 2. Documents the architecture, design decisions, and rationale
1219
+ 3. Provides enough detail that a developer could recreate it from scratch
1220
+ 4. Is honest about inferred vs. confirmed information
1221
+
1222
+ ## Output Format Rules
1223
+ - Use markdown with clear section headers
1224
+ - Be specific: name actual files, packages, patterns
1225
+ - Do NOT use filler phrases like "leverages", "seamlessly", "robust", "cutting-edge"
1226
+ - Do NOT invent features not evidenced in the repository data
1227
+ - Prefix any inferred information with [Inferred]
1228
+ - Keep technical accuracy above completeness
1229
+
1230
+ ## Sections to Include (always)
1231
+ 1. Project Overview (what it is, core purpose, target user)
1232
+ 2. Tech Stack (every confirmed dependency with purpose)
1233
+ 3. Architecture (how components relate, data flow)
1234
+ 4. Directory Structure (annotated)
1235
+ 5. Key Design Decisions (why these choices were made)
1236
+ 6. Build & Dev Process (scripts, tooling, CI)
1237
+ 7. Recreation Steps (numbered, from git init to working state)
1238
+ 8. Notable Patterns (code patterns, conventions used)
1239
+
1240
+ ## Additional Sections (if query provided)
1241
+ - Direct Answer to query, grounded in repo evidence`
1242
+ };
1243
+ }
1244
+ // ── Standard Analysis Prompt ──────────────────────────────
1245
+ static buildAnalysisPrompt(analysis, userQuery) {
1246
+ const { meta, tree, keyFiles, dependencyMap, techStack, entryPoints, configFiles } = analysis;
1247
+ const treeStr = buildTreeString(tree, 3);
1248
+ const depsStr = formatDeps(dependencyMap);
1249
+ const keyFilesStr = keyFiles.map((f) => `### ${f.path} [${f.role}]
1250
+ \`\`\`
1251
+ ${f.content.slice(0, 3e3)}
1252
+ \`\`\``).join("\n\n");
1253
+ const querySection = userQuery?.trim() ? `
1254
+
1255
+ ## User Query
1256
+ ${userQuery.trim()}
1257
+
1258
+ After your full analysis, provide a direct, evidence-based answer to this query in a section titled "## Direct Answer".` : "";
1259
+ return {
1260
+ role: "user",
1261
+ content: `# Repository: ${meta.owner}/${meta.repo}
1262
+
1263
+ ## Metadata
1264
+ - Description: ${meta.description || "No description"}
1265
+ - Primary Language: ${meta.language}
1266
+ - Stars: ${meta.stars.toLocaleString()} | Forks: ${meta.forks.toLocaleString()}
1267
+ - Topics: ${meta.topics.join(", ") || "none"}
1268
+ - Created: ${meta.createdAt.split("T")[0]} | Last Updated: ${meta.updatedAt.split("T")[0]}
1269
+ - Repo Size: ${meta.size} KB
1270
+ - Default Branch: ${meta.defaultBranch}
1271
+
1272
+ ## Detected Tech Stack
1273
+ ${techStack.map((t) => `- ${t}`).join("\n")}
1274
+
1275
+ ## Entry Points
1276
+ ${entryPoints.length ? entryPoints.map((e) => `- \`${e}\``).join("\n") : "- Not detected"}
1277
+
1278
+ ## Config Files
1279
+ ${configFiles.length ? configFiles.map((c) => `- \`${c}\``).join("\n") : "- None found"}
1280
+
1281
+ ## Dependencies
1282
+ ${depsStr}
1283
+
1284
+ ## Directory Structure (filtered)
1285
+ \`\`\`
1286
+ ${treeStr}
1287
+ \`\`\`
1288
+
1289
+ ## Key Files Content
1290
+ ${keyFilesStr}
1291
+ ${querySection}
1292
+
1293
+ ---
1294
+
1295
+ Produce the full structured recreation prompt now. Be precise, developer-grade, and actionable.`
1296
+ };
1297
+ }
1298
+ // ── Deep Dive Prompt ──────────────────────────────────────
1299
+ static buildDeepDivePrompt(analysis, userQuery) {
1300
+ const base = _PromptBuilder.buildAnalysisPrompt(analysis, userQuery);
1301
+ return {
1302
+ ...base,
1303
+ content: base.content + `
1304
+
1305
+ ## Deep Dive Mode
1306
+ You are in DEEP DIVE mode. In addition to the standard analysis:
1307
+ - Explain every architectural decision in depth with rationale
1308
+ - Document all design patterns identified (Factory, Observer, Repository, etc.)
1309
+ - Provide a learning path: what concepts must be understood to build this
1310
+ - Add a "Pitfalls & Non-obvious Issues" section
1311
+ - Add a "Scaling Considerations" section
1312
+ - Add estimated time to recreate broken down by phase
1313
+ - Make this suitable as a comprehensive technical document, not just a reference`
1314
+ };
1315
+ }
1316
+ // ── Compact / Summarize Prompt ────────────────────────────
1317
+ static buildSummarizePrompt(messages) {
1318
+ const sessionText = messages.filter((m) => m.role !== "system").map((m) => `[${m.role.toUpperCase()}]: ${m.content}`).join("\n\n---\n\n");
1319
+ return {
1320
+ role: "user",
1321
+ content: `Summarize this git-reverse session concisely. Include:
1322
+ 1. Repository analyzed (owner/repo, stack, purpose)
1323
+ 2. Key findings from the analysis
1324
+ 3. Questions or queries the user had and how they were answered
1325
+ 4. Any next steps or action items mentioned
1326
+
1327
+ Session transcript:
1328
+ ---
1329
+ ${sessionText}
1330
+ ---
1331
+ Produce a clean, scannable summary.`
1332
+ };
1333
+ }
1334
+ // ── Build Full Message Chain ──────────────────────────────
1335
+ static buildMessages(analysis, mode, userQuery) {
1336
+ const system = _PromptBuilder.buildSystemPrompt();
1337
+ switch (mode) {
1338
+ case "deep":
1339
+ return [system, _PromptBuilder.buildDeepDivePrompt(analysis, userQuery)];
1340
+ default:
1341
+ return [system, _PromptBuilder.buildAnalysisPrompt(analysis, userQuery)];
1342
+ }
1343
+ }
1344
+ };
1345
+ function buildTreeString(files, maxDepth) {
1346
+ const lines = [];
1347
+ const dirs = /* @__PURE__ */ new Set();
1348
+ for (const f of files) {
1349
+ const parts = f.path.split("/");
1350
+ for (let i = 1; i < parts.length; i++) {
1351
+ dirs.add(parts.slice(0, i).join("/"));
1352
+ }
1353
+ }
1354
+ const sorted = [...files].sort((a, b) => {
1355
+ const aDepth = a.path.split("/").length;
1356
+ const bDepth = b.path.split("/").length;
1357
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
1358
+ return aDepth - bDepth || a.path.localeCompare(b.path);
1359
+ });
1360
+ const seen = /* @__PURE__ */ new Set();
1361
+ for (const f of sorted) {
1362
+ const depth = f.path.split("/").length - 1;
1363
+ if (depth > maxDepth) continue;
1364
+ if (seen.has(f.path)) continue;
1365
+ seen.add(f.path);
1366
+ const indent = " ".repeat(depth);
1367
+ const name = f.path.split("/").pop() ?? f.path;
1368
+ const suffix = f.type === "dir" ? "/" : "";
1369
+ lines.push(`${indent}${name}${suffix}`);
1370
+ }
1371
+ return lines.slice(0, 120).join("\n");
1372
+ }
1373
+ function formatDeps(deps) {
1374
+ const lines = [];
1375
+ const rt = Object.entries(deps.runtime);
1376
+ const dev = Object.entries(deps.dev);
1377
+ if (rt.length) {
1378
+ lines.push("**Runtime:**");
1379
+ for (const [name, version] of rt.slice(0, 30)) {
1380
+ lines.push(` ${name}: ${version}`);
1381
+ }
1382
+ }
1383
+ if (dev.length) {
1384
+ lines.push("**Dev:**");
1385
+ for (const [name, version] of dev.slice(0, 20)) {
1386
+ lines.push(` ${name}: ${version}`);
1387
+ }
1388
+ }
1389
+ if (!rt.length && !dev.length) lines.push(" No package manifest found");
1390
+ return lines.join("\n");
1391
+ }
1392
+
1393
+ // src/core/AnalysisService.ts
1394
+ var AnalysisService = class {
1395
+ githubClient;
1396
+ openrouterClient;
1397
+ constructor(apiKey, githubToken) {
1398
+ this.githubClient = new GitHubClient(githubToken);
1399
+ this.openrouterClient = new OpenRouterClient(apiKey);
1400
+ }
1401
+ async *analyze(input, onProgress) {
1402
+ const { repoUrl, query, mode, model } = input;
1403
+ onProgress({ phase: "parsing-url", message: "Parsing repository URL..." });
1404
+ const parsed = GitHubClient.parseUrl(repoUrl);
1405
+ if (!parsed) {
1406
+ throw new Error(
1407
+ `Could not parse GitHub URL: "${repoUrl}"
1408
+ Expected format: https://github.com/owner/repo`
1409
+ );
1410
+ }
1411
+ onProgress({
1412
+ phase: "fetching-meta",
1413
+ message: `Fetching ${parsed.owner}/${parsed.repo} metadata...`
1414
+ });
1415
+ let analysis;
1416
+ try {
1417
+ const meta = await this.githubClient.fetchRepoMeta(parsed.owner, parsed.repo);
1418
+ onProgress({
1419
+ phase: "fetching-tree",
1420
+ message: `Mapping file tree (${meta.defaultBranch})...`,
1421
+ repoMeta: meta
1422
+ });
1423
+ const tree = await this.githubClient.fetchRepoTree(
1424
+ parsed.owner,
1425
+ parsed.repo,
1426
+ meta.defaultBranch
1427
+ );
1428
+ onProgress({
1429
+ phase: "fetching-files",
1430
+ message: `Reading ${Math.min(tree.filter((f) => f.type === "file").length, 12)} key files...`,
1431
+ repoMeta: meta
1432
+ });
1433
+ analysis = await this.githubClient.analyzeRepo(parsed.owner, parsed.repo);
1434
+ } catch (err) {
1435
+ const msg = err instanceof Error ? err.message : String(err);
1436
+ if (msg.includes("404")) {
1437
+ throw new Error(
1438
+ `Repository not found: ${parsed.owner}/${parsed.repo}
1439
+ Check the URL and ensure the repository is public.`
1440
+ );
1441
+ }
1442
+ if (msg.includes("403") || msg.includes("rate limit")) {
1443
+ throw new Error(
1444
+ "GitHub API rate limit reached.\nWait a few minutes, or set a GITHUB_TOKEN environment variable for higher limits."
1445
+ );
1446
+ }
1447
+ throw err;
1448
+ }
1449
+ onProgress({
1450
+ phase: "building-prompt",
1451
+ message: "Building analysis prompt...",
1452
+ repoMeta: analysis.meta
1453
+ });
1454
+ let messages;
1455
+ if (mode === "compact") {
1456
+ messages = PromptBuilder.buildMessages(analysis, "standard", query);
1457
+ } else {
1458
+ messages = PromptBuilder.buildMessages(analysis, mode, query);
1459
+ }
1460
+ onProgress({
1461
+ phase: "streaming",
1462
+ message: `Analyzing with ${model.split("/").pop()}...`,
1463
+ repoMeta: analysis.meta
1464
+ });
1465
+ yield* this.openrouterClient.streamCompletion(model, messages, {
1466
+ maxTokens: mode === "deep" ? 8192 : 4096,
1467
+ temperature: mode === "deep" ? 0.4 : 0.3
1468
+ });
1469
+ onProgress({ phase: "done", message: "Analysis complete.", repoMeta: analysis.meta });
1470
+ }
1471
+ // ── Summarize existing session ────────────────────────────
1472
+ async *summarizeSession(messages, model) {
1473
+ const summarizeMsg = PromptBuilder.buildSummarizePrompt(messages);
1474
+ yield* this.openrouterClient.streamCompletion(model, [summarizeMsg], {
1475
+ maxTokens: 1024,
1476
+ temperature: 0.2
1477
+ });
1478
+ }
1479
+ // ── Static helper for repo meta display ──────────────────
1480
+ static formatRepoStats(meta) {
1481
+ return `${meta.owner}/${meta.repo} \xB7 ${meta.language} \xB7 \u2605 ${meta.stars.toLocaleString()}`;
1482
+ }
1483
+ };
1484
+
1485
+ // src/ui/components/OutputRenderer.tsx
1486
+ import { Box as Box9, Text as Text9 } from "ink";
1487
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1488
+ function OutputRenderer({ content, isStreaming = false, wordCount }) {
1489
+ const lines = content.split("\n");
1490
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingLeft: 2, children: [
1491
+ lines.map((line, i) => /* @__PURE__ */ jsx9(RenderedLine, { line }, i)),
1492
+ isStreaming && /* @__PURE__ */ jsx9(Text9, { color: "cyan", dimColor: true, children: "\u258B" }),
1493
+ !isStreaming && content.length > 0 && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
1494
+ /* @__PURE__ */ jsxs9(Text9, { color: "white", dimColor: true, children: [
1495
+ "\u2500".repeat(44),
1496
+ " "
1497
+ ] }),
1498
+ /* @__PURE__ */ jsxs9(Text9, { color: "white", dimColor: true, children: [
1499
+ wordCount ?? content.trim().split(/\s+/).filter(Boolean).length,
1500
+ " words"
1501
+ ] })
1502
+ ] })
1503
+ ] });
1504
+ }
1505
+ function RenderedLine({ line }) {
1506
+ if (/^# /.test(line)) {
1507
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: line.slice(2) }) });
1508
+ }
1509
+ if (/^## /.test(line)) {
1510
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "white", bold: true, children: line.slice(3) }) });
1511
+ }
1512
+ if (/^### /.test(line)) {
1513
+ return /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
1514
+ " ",
1515
+ line.slice(4)
1516
+ ] });
1517
+ }
1518
+ if (/^```/.test(line)) {
1519
+ const lang = line.slice(3).trim();
1520
+ return /* @__PURE__ */ jsx9(Text9, { color: "white", dimColor: true, children: lang ? ` \u250C\u2500 ${lang}` : " \u250C\u2500" });
1521
+ }
1522
+ if (/^---+$/.test(line.trim())) {
1523
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { color: "white", dimColor: true, children: [
1524
+ " ",
1525
+ "\u2500".repeat(40)
1526
+ ] }) });
1527
+ }
1528
+ if (/^[-*] /.test(line)) {
1529
+ return /* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
1530
+ " \xB7 ",
1531
+ /* @__PURE__ */ jsx9(InlineText, { text: line.slice(2) })
1532
+ ] });
1533
+ }
1534
+ if (/^\d+\. /.test(line)) {
1535
+ const match = line.match(/^(\d+)\. (.+)$/);
1536
+ if (match) {
1537
+ return /* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
1538
+ " ",
1539
+ /* @__PURE__ */ jsxs9(Text9, { color: "cyan", dimColor: true, children: [
1540
+ match[1],
1541
+ "."
1542
+ ] }),
1543
+ " ",
1544
+ /* @__PURE__ */ jsx9(InlineText, { text: match[2] })
1545
+ ] });
1546
+ }
1547
+ }
1548
+ if (line.trim() === "") {
1549
+ return /* @__PURE__ */ jsx9(Text9, { children: "" });
1550
+ }
1551
+ return /* @__PURE__ */ jsx9(Text9, { color: "white", dimColor: true, children: /* @__PURE__ */ jsx9(InlineText, { text: line }) });
1552
+ }
1553
+ function InlineText({ text }) {
1554
+ return /* @__PURE__ */ jsx9(Text9, { color: "white", children: text });
1555
+ }
1556
+
1557
+ // src/ui/screens/Analysis.tsx
1558
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1559
+ function AnalysisScreen({ input, onComplete, onBack, onError }) {
1560
+ const [phase, setPhase] = useState5("running");
1561
+ const [progressMsg, setProgressMsg] = useState5("Starting...");
1562
+ const [output, setOutput] = useState5("");
1563
+ const [repoMeta, setRepoMeta] = useState5(null);
1564
+ const [errorMsg, setErrorMsg] = useState5("");
1565
+ const [sessionId, setSessionId] = useState5("");
1566
+ const repoMetaRef = useRef(null);
1567
+ const sessionRef = useRef(new SessionManager());
1568
+ const abortRef = useRef(false);
1569
+ useInput3((_, key) => {
1570
+ if (key.escape && phase === "done") {
1571
+ onBack();
1572
+ }
1573
+ });
1574
+ useEffect(() => {
1575
+ let accum = "";
1576
+ abortRef.current = false;
1577
+ const run = async () => {
1578
+ const service = new AnalysisService(input.apiKey);
1579
+ try {
1580
+ const gen = service.analyze(input, (progress) => {
1581
+ if (abortRef.current) return;
1582
+ setProgressMsg(progress.message);
1583
+ if (progress.repoMeta) {
1584
+ setRepoMeta(progress.repoMeta);
1585
+ repoMetaRef.current = progress.repoMeta;
1586
+ }
1587
+ if (progress.phase === "streaming") setPhase("streaming");
1588
+ });
1589
+ for await (const chunk of gen) {
1590
+ if (abortRef.current) break;
1591
+ if (chunk.done) break;
1592
+ accum += chunk.content;
1593
+ setOutput(accum);
1594
+ }
1595
+ if (abortRef.current) return;
1596
+ const session = sessionRef.current.createSession({
1597
+ repoUrl: input.repoUrl,
1598
+ query: input.query ?? "",
1599
+ model: input.model,
1600
+ mode: input.mode,
1601
+ repoMeta: repoMetaRef.current ?? void 0
1602
+ });
1603
+ sessionRef.current.setAnalysisOutput(accum);
1604
+ sessionRef.current.appendMessage({ role: "assistant", content: accum });
1605
+ sessionRef.current.save();
1606
+ setSessionId(session.id);
1607
+ setPhase("done");
1608
+ onComplete(accum, session.id);
1609
+ } catch (err) {
1610
+ if (abortRef.current) return;
1611
+ const msg = err instanceof Error ? err.message : String(err);
1612
+ setErrorMsg(msg);
1613
+ setPhase("error");
1614
+ onError(msg);
1615
+ }
1616
+ };
1617
+ run();
1618
+ return () => {
1619
+ abortRef.current = true;
1620
+ };
1621
+ }, []);
1622
+ const spinnerLabel = {
1623
+ "parsing-url": "Parsing URL...",
1624
+ "fetching-meta": progressMsg,
1625
+ "fetching-tree": progressMsg,
1626
+ "fetching-files": progressMsg,
1627
+ "building-prompt": progressMsg,
1628
+ streaming: progressMsg,
1629
+ done: "Complete",
1630
+ idle: "Initializing...",
1631
+ error: "Error"
1632
+ };
1633
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingLeft: 2, children: [
1634
+ repoMeta && /* @__PURE__ */ jsxs10(Box10, { marginBottom: 1, flexDirection: "row", gap: 2, children: [
1635
+ /* @__PURE__ */ jsxs10(Text10, { color: "cyan", children: [
1636
+ repoMeta.owner,
1637
+ "/",
1638
+ repoMeta.repo
1639
+ ] }),
1640
+ /* @__PURE__ */ jsx10(Text10, { color: "white", dimColor: true, children: "\xB7" }),
1641
+ /* @__PURE__ */ jsx10(Text10, { color: "white", dimColor: true, children: repoMeta.language }),
1642
+ /* @__PURE__ */ jsx10(Text10, { color: "white", dimColor: true, children: "\xB7" }),
1643
+ /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
1644
+ "\u2605 ",
1645
+ repoMeta.stars.toLocaleString()
1646
+ ] })
1647
+ ] }),
1648
+ input.mode !== "standard" && /* @__PURE__ */ jsx10(Box10, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "yellow", dimColor: true, children: [
1649
+ "\u25C8 ",
1650
+ input.mode === "deep" ? "deep-dive" : input.mode,
1651
+ " mode"
1652
+ ] }) }),
1653
+ input.query && /* @__PURE__ */ jsxs10(Box10, { marginBottom: 1, children: [
1654
+ /* @__PURE__ */ jsx10(Text10, { color: "white", dimColor: true, children: "Query: " }),
1655
+ /* @__PURE__ */ jsx10(Text10, { color: "white", children: input.query })
1656
+ ] }),
1657
+ (phase === "running" || phase === "streaming") && /* @__PURE__ */ jsx10(Spinner, { label: progressMsg, color: phase === "streaming" ? "white" : "cyan" }),
1658
+ output.length > 0 && /* @__PURE__ */ jsx10(Box10, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(
1659
+ OutputRenderer,
1660
+ {
1661
+ content: output,
1662
+ isStreaming: phase === "streaming",
1663
+ wordCount: countWords(output)
1664
+ }
1665
+ ) }),
1666
+ phase === "done" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
1667
+ /* @__PURE__ */ jsxs10(Box10, { children: [
1668
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: "\u2713 Analysis complete" }),
1669
+ /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
1670
+ " ",
1671
+ "Session",
1672
+ " "
1673
+ ] }),
1674
+ /* @__PURE__ */ jsxs10(Text10, { color: "cyan", children: [
1675
+ "#",
1676
+ sessionId
1677
+ ] }),
1678
+ /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
1679
+ " ",
1680
+ "saved"
1681
+ ] })
1682
+ ] }),
1683
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 0, children: /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
1684
+ " ",
1685
+ "Esc to return to dashboard"
1686
+ ] }) })
1687
+ ] }),
1688
+ phase === "error" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
1689
+ /* @__PURE__ */ jsx10(Text10, { color: "red", children: "\u2717 Error" }),
1690
+ /* @__PURE__ */ jsx10(Box10, { paddingLeft: 2, children: /* @__PURE__ */ jsx10(Text10, { color: "white", dimColor: true, children: errorMsg }) }),
1691
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "white", dimColor: true, children: [
1692
+ " ",
1693
+ "Esc to return to dashboard"
1694
+ ] }) })
1695
+ ] })
1696
+ ] });
1697
+ }
1698
+
1699
+ // src/ui/screens/Settings.tsx
1700
+ import { useState as useState6 } from "react";
1701
+ import { Box as Box11, Text as Text11 } from "ink";
1702
+ import SelectInput2 from "ink-select-input";
1703
+ import TextInput4 from "ink-text-input";
1704
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1705
+ var MENU_ITEMS = [
1706
+ { label: "Change username", value: "change-name" },
1707
+ { label: "Change API key", value: "change-key" },
1708
+ { label: "Change active model", value: "change-model" },
1709
+ { label: "Refresh model list", value: "refresh-models" },
1710
+ { label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", value: "sep", disabled: true },
1711
+ { label: "Back to dashboard", value: "back" }
1712
+ ];
1713
+ function SettingsScreen({ onBack, onModelChanged, onUsernameChanged }) {
1714
+ const user = getUser();
1715
+ const cached = getCachedModels();
1716
+ const [view, setView] = useState6("menu");
1717
+ const [nameValue, setNameValue] = useState6(user.name);
1718
+ const [keyValue, setKeyValue] = useState6("");
1719
+ const [models, setModels] = useState6(cached.models);
1720
+ const [status, setStatus] = useState6("");
1721
+ const [error, setError] = useState6("");
1722
+ const handleMenuSelect = async (item) => {
1723
+ if (item.value === "back") {
1724
+ onBack();
1725
+ return;
1726
+ }
1727
+ if (item.value === "sep") return;
1728
+ if (item.value === "refresh-models") {
1729
+ setView("refreshing");
1730
+ try {
1731
+ const client = new OpenRouterClient(user.apiKey);
1732
+ const fresh = await client.fetchFreeModels();
1733
+ setCachedModels(fresh);
1734
+ markModelsSeen(fresh.map((m) => m.id));
1735
+ setModels(fresh);
1736
+ setStatus(`${fresh.length} models refreshed.`);
1737
+ setTimeout(() => {
1738
+ setStatus("");
1739
+ setView("menu");
1740
+ }, 1500);
1741
+ } catch (err) {
1742
+ setError(err instanceof Error ? err.message : "Refresh failed.");
1743
+ setView("menu");
1744
+ }
1745
+ return;
1746
+ }
1747
+ setView(item.value);
1748
+ };
1749
+ const handleNameSubmit = (val) => {
1750
+ const trimmed = val.trim();
1751
+ if (trimmed.length < 2) {
1752
+ setError("Name must be at least 2 characters.");
1753
+ return;
1754
+ }
1755
+ setUserName(trimmed);
1756
+ onUsernameChanged(trimmed);
1757
+ setStatus(`Username updated to "${trimmed}".`);
1758
+ setTimeout(() => {
1759
+ setStatus("");
1760
+ setView("menu");
1761
+ }, 1200);
1762
+ };
1763
+ const handleKeySubmit = async (val) => {
1764
+ const trimmed = val.trim();
1765
+ if (!trimmed.startsWith("sk-or-")) {
1766
+ setError("Invalid key format.");
1767
+ return;
1768
+ }
1769
+ setView("refreshing");
1770
+ try {
1771
+ const client = new OpenRouterClient(trimmed);
1772
+ const { valid, error: ve } = await client.validateKey();
1773
+ if (!valid) {
1774
+ setError(ve ?? "Validation failed.");
1775
+ setView("change-key");
1776
+ return;
1777
+ }
1778
+ const fresh = await client.fetchFreeModels();
1779
+ setApiKey(trimmed);
1780
+ setCachedModels(fresh);
1781
+ markModelsSeen(fresh.map((m) => m.id));
1782
+ setModels(fresh);
1783
+ setStatus("Key updated and models refreshed.");
1784
+ setTimeout(() => {
1785
+ setStatus("");
1786
+ setView("menu");
1787
+ }, 1500);
1788
+ } catch (err) {
1789
+ setError(err instanceof Error ? err.message : "Failed.");
1790
+ setView("change-key");
1791
+ }
1792
+ };
1793
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingLeft: 2, children: [
1794
+ /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, children: [
1795
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "Settings" }),
1796
+ /* @__PURE__ */ jsxs11(Text11, { color: "white", dimColor: true, children: [
1797
+ " \xB7 ",
1798
+ user.name,
1799
+ " \xB7 ",
1800
+ truncate(user.selectedModel.split("/").pop() ?? user.selectedModel, 30)
1801
+ ] })
1802
+ ] }),
1803
+ /* @__PURE__ */ jsx11(Box11, { marginBottom: 0, children: /* @__PURE__ */ jsxs11(Text11, { color: "white", dimColor: true, children: [
1804
+ " Config: ",
1805
+ getStorePath()
1806
+ ] }) }),
1807
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "white", dimColor: true, children: "\u2500".repeat(44) }) }),
1808
+ status && /* @__PURE__ */ jsx11(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: "green", children: [
1809
+ " ",
1810
+ "\u2713 ",
1811
+ status
1812
+ ] }) }),
1813
+ error && /* @__PURE__ */ jsx11(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
1814
+ " ",
1815
+ "\u2717 ",
1816
+ error
1817
+ ] }) }),
1818
+ view === "menu" && /* @__PURE__ */ jsx11(Box11, { paddingLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsx11(
1819
+ SelectInput2,
1820
+ {
1821
+ items: MENU_ITEMS.filter((i) => !("disabled" in i && i.disabled)),
1822
+ onSelect: handleMenuSelect,
1823
+ itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsxs11(Text11, { color: isSelected ? "cyan" : "white", dimColor: !isSelected, children: [
1824
+ isSelected ? "\u203A " : " ",
1825
+ label
1826
+ ] })
1827
+ }
1828
+ ) }),
1829
+ view === "change-name" && /* @__PURE__ */ jsxs11(Box11, { paddingLeft: 2, marginTop: 1, flexDirection: "column", children: [
1830
+ /* @__PURE__ */ jsx11(Text11, { color: "white", dimColor: true, children: "New username:" }),
1831
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(
1832
+ TextInput4,
1833
+ {
1834
+ value: nameValue,
1835
+ onChange: (v) => {
1836
+ setNameValue(v);
1837
+ setError("");
1838
+ },
1839
+ onSubmit: handleNameSubmit,
1840
+ placeholder: user.name
1841
+ }
1842
+ ) })
1843
+ ] }),
1844
+ view === "change-key" && /* @__PURE__ */ jsxs11(Box11, { paddingLeft: 2, marginTop: 1, flexDirection: "column", children: [
1845
+ /* @__PURE__ */ jsx11(Text11, { color: "white", dimColor: true, children: "New OpenRouter API key:" }),
1846
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(
1847
+ TextInput4,
1848
+ {
1849
+ value: keyValue,
1850
+ onChange: (v) => {
1851
+ setKeyValue(v);
1852
+ setError("");
1853
+ },
1854
+ onSubmit: handleKeySubmit,
1855
+ placeholder: "sk-or-v1-...",
1856
+ mask: "*"
1857
+ }
1858
+ ) })
1859
+ ] }),
1860
+ view === "change-model" && /* @__PURE__ */ jsx11(
1861
+ SetupModel,
1862
+ {
1863
+ models,
1864
+ isSettings: true,
1865
+ onComplete: (m) => {
1866
+ onModelChanged(m);
1867
+ setStatus(`Model set to ${m.id}`);
1868
+ setTimeout(() => {
1869
+ setStatus("");
1870
+ setView("menu");
1871
+ }, 1e3);
1872
+ }
1873
+ }
1874
+ ),
1875
+ view === "refreshing" && /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Spinner, { label: "Working..." }) }),
1876
+ view === "menu" && /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: "white", dimColor: true, children: [
1877
+ " ",
1878
+ "Esc to go back"
1879
+ ] }) })
1880
+ ] });
1881
+ }
1882
+
1883
+ // src/ui/screens/ResumeSession.tsx
1884
+ import { useState as useState7 } from "react";
1885
+ import { Box as Box12, Text as Text12 } from "ink";
1886
+ import TextInput5 from "ink-text-input";
1887
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1888
+ function ResumeSession({ initialId = "", onResume, onBack }) {
1889
+ const [idValue, setIdValue] = useState7(initialId);
1890
+ const [error, setError] = useState7("");
1891
+ const recentSessions = SessionManager.listSessions().slice(0, 8);
1892
+ const handleSubmit = (val) => {
1893
+ const id = val.trim().replace(/^#/, "");
1894
+ if (!id) {
1895
+ setError("Enter a session ID.");
1896
+ return;
1897
+ }
1898
+ const session = SessionManager.getSession(id);
1899
+ if (!session) {
1900
+ setError(`No session found with ID: ${id}`);
1901
+ return;
1902
+ }
1903
+ onResume(session);
1904
+ };
1905
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingLeft: 2, children: [
1906
+ /* @__PURE__ */ jsx12(Box12, { marginBottom: 1, children: /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: "Resume Session" }) }),
1907
+ /* @__PURE__ */ jsxs12(Box12, { marginBottom: 1, flexDirection: "column", children: [
1908
+ /* @__PURE__ */ jsxs12(Text12, { color: "white", dimColor: true, children: [
1909
+ " ",
1910
+ "Enter a session ID to resume:"
1911
+ ] }),
1912
+ /* @__PURE__ */ jsxs12(Box12, { marginTop: 1, children: [
1913
+ /* @__PURE__ */ jsx12(Text12, { color: "white", dimColor: true, children: " id " }),
1914
+ /* @__PURE__ */ jsx12(
1915
+ TextInput5,
1916
+ {
1917
+ value: idValue,
1918
+ onChange: (v) => {
1919
+ setIdValue(v);
1920
+ setError("");
1921
+ },
1922
+ onSubmit: handleSubmit,
1923
+ placeholder: "e.g. abc12345"
1924
+ }
1925
+ )
1926
+ ] }),
1927
+ error && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
1928
+ " ",
1929
+ error
1930
+ ] }) })
1931
+ ] }),
1932
+ recentSessions.length > 0 && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginTop: 1, children: [
1933
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsxs12(Text12, { color: "white", dimColor: true, children: [
1934
+ " ",
1935
+ "\u2500".repeat(44)
1936
+ ] }) }),
1937
+ /* @__PURE__ */ jsxs12(Text12, { color: "white", dimColor: true, children: [
1938
+ " ",
1939
+ "Saved sessions:"
1940
+ ] }),
1941
+ /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", marginTop: 0, paddingLeft: 2, children: recentSessions.map((s) => /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", children: [
1942
+ /* @__PURE__ */ jsxs12(Text12, { color: "cyan", children: [
1943
+ "#",
1944
+ s.id,
1945
+ " "
1946
+ ] }),
1947
+ /* @__PURE__ */ jsx12(Text12, { color: "white", dimColor: true, children: s.repoUrl.replace("https://github.com/", "").slice(0, 28).padEnd(30) }),
1948
+ /* @__PURE__ */ jsx12(Text12, { color: "white", dimColor: true, children: s.mode !== "standard" ? `[${s.mode}] ` : " " }),
1949
+ /* @__PURE__ */ jsx12(Text12, { color: "white", dimColor: true, children: timeAgo(s.updatedAt) })
1950
+ ] }, s.id)) })
1951
+ ] }),
1952
+ /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text12, { color: "white", dimColor: true, children: [
1953
+ " ",
1954
+ "Esc to go back"
1955
+ ] }) })
1956
+ ] });
1957
+ }
1958
+
1959
+ // src/ui/App.tsx
1960
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1961
+ var VERSION = "1.0.0";
1962
+ function App({ resumeSessionId }) {
1963
+ const { exit } = useApp2();
1964
+ const user = getUser();
1965
+ const cached = getCachedModels();
1966
+ const initialScreen = (() => {
1967
+ if (!user.setupComplete) return user.setupStep === "username" ? "setup-username" : "setup-apikey";
1968
+ if (resumeSessionId) return "resume-session";
1969
+ return "dashboard";
1970
+ })();
1971
+ const [screen, setScreen] = useState8(initialScreen);
1972
+ const [prevScreen, setPrevScreen] = useState8("dashboard");
1973
+ const [username, setUsername] = useState8(user.name);
1974
+ const [activeModel, setActiveModel] = useState8(user.selectedModel);
1975
+ const [freeModels, setFreeModels] = useState8(cached.models);
1976
+ const [newModels, setNewModels] = useState8([]);
1977
+ const [pendingAnalysis, setPendingAnalysis] = useState8(null);
1978
+ const [exitSessionId, setExitSessionId] = useState8(null);
1979
+ const [exitRepoUrl, setExitRepoUrl] = useState8(null);
1980
+ const [resumeId2, setResumeId] = useState8(resumeSessionId ?? "");
1981
+ useEffect2(() => {
1982
+ if (!user.setupComplete || !user.apiKey) return;
1983
+ const refresh = async () => {
1984
+ try {
1985
+ const client = new OpenRouterClient(user.apiKey);
1986
+ if (!OpenRouterClient.isCacheStale(cached.fetchedAt)) return;
1987
+ const fresh = await client.fetchFreeModels();
1988
+ setCachedModels(fresh);
1989
+ setFreeModels(fresh);
1990
+ const unseen = getNewModels(fresh);
1991
+ if (unseen.length > 0) setNewModels(unseen);
1992
+ } catch {
1993
+ }
1994
+ };
1995
+ refresh();
1996
+ }, []);
1997
+ useEffect2(() => {
1998
+ const sessionMgr = new SessionManager();
1999
+ sessionMgr.registerSigintHandler((savedId) => {
2000
+ if (savedId) {
2001
+ process.stdout.write(`
2002
+
2003
+ Session saved: #${savedId}
2004
+ `);
2005
+ process.stdout.write(` Resume: git-reverse --resume ${savedId}
2006
+
2007
+ `);
2008
+ }
2009
+ process.exit(0);
2010
+ });
2011
+ return () => sessionMgr.removeSigintHandler();
2012
+ }, []);
2013
+ useInput4((_, key) => {
2014
+ if (key.escape) {
2015
+ if (screen === "settings" || screen === "resume-session") {
2016
+ navigateTo("dashboard");
2017
+ }
2018
+ }
2019
+ });
2020
+ const navigateTo = (next) => {
2021
+ setPrevScreen(screen);
2022
+ setScreen(next);
2023
+ };
2024
+ const handleUsernameComplete = (name) => {
2025
+ setUsername(name);
2026
+ navigateTo("setup-apikey");
2027
+ };
2028
+ const handleApiKeyComplete = (models) => {
2029
+ setFreeModels(models);
2030
+ navigateTo("setup-model");
2031
+ };
2032
+ const handleModelComplete = (model) => {
2033
+ setActiveModel(model.id);
2034
+ navigateTo("dashboard");
2035
+ };
2036
+ const handleAnalyze = (repoUrl, query, mode) => {
2037
+ const currentUser = getUser();
2038
+ setPendingAnalysis({
2039
+ repoUrl,
2040
+ query,
2041
+ mode,
2042
+ model: currentUser.selectedModel,
2043
+ apiKey: currentUser.apiKey
2044
+ });
2045
+ navigateTo("analysis");
2046
+ };
2047
+ const handleCommand = (cmd) => {
2048
+ if (cmd === "settings") return navigateTo("settings");
2049
+ if (cmd === "sessions") return navigateTo("resume-session");
2050
+ if (cmd === "quit") {
2051
+ exit();
2052
+ return;
2053
+ }
2054
+ if (cmd.startsWith("resume:")) {
2055
+ const id = cmd.slice(7);
2056
+ setResumeId(id);
2057
+ navigateTo("resume-session");
2058
+ }
2059
+ };
2060
+ const handleAnalysisComplete = (output, sessionId) => {
2061
+ setExitSessionId(sessionId);
2062
+ setExitRepoUrl(pendingAnalysis?.repoUrl ?? null);
2063
+ };
2064
+ const handleAnalysisBack = () => {
2065
+ setExitSessionId(null);
2066
+ navigateTo("dashboard");
2067
+ };
2068
+ const handleResumeSession = (session) => {
2069
+ setPendingAnalysis({
2070
+ repoUrl: session.repoUrl,
2071
+ query: session.query,
2072
+ mode: session.mode,
2073
+ model: session.model,
2074
+ apiKey: getUser().apiKey
2075
+ });
2076
+ navigateTo("analysis");
2077
+ };
2078
+ const handleModelChanged = (model) => {
2079
+ setActiveModel(model.id);
2080
+ markModelsSeen(newModels.map((m) => m.id));
2081
+ setNewModels([]);
2082
+ };
2083
+ const handleUsernameChanged = (name) => {
2084
+ setUsername(name);
2085
+ };
2086
+ const showHeader = screen !== "analysis";
2087
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
2088
+ showHeader && /* @__PURE__ */ jsx13(
2089
+ Header,
2090
+ {
2091
+ username: screen === "dashboard" || screen === "settings" ? username : void 0,
2092
+ model: activeModel || void 0,
2093
+ version: VERSION,
2094
+ newModelCount: newModels.length
2095
+ }
2096
+ ),
2097
+ screen === "setup-username" && /* @__PURE__ */ jsx13(SetupUsername, { onComplete: handleUsernameComplete }),
2098
+ screen === "setup-apikey" && /* @__PURE__ */ jsx13(SetupApiKey, { onComplete: handleApiKeyComplete }),
2099
+ screen === "setup-model" && /* @__PURE__ */ jsx13(SetupModel, { models: freeModels, onComplete: handleModelComplete }),
2100
+ screen === "dashboard" && /* @__PURE__ */ jsx13(
2101
+ Dashboard,
2102
+ {
2103
+ username,
2104
+ model: activeModel,
2105
+ newModels,
2106
+ onAnalyze: handleAnalyze,
2107
+ onCommand: handleCommand
2108
+ }
2109
+ ),
2110
+ screen === "analysis" && pendingAnalysis && /* @__PURE__ */ jsx13(
2111
+ AnalysisScreen,
2112
+ {
2113
+ input: pendingAnalysis,
2114
+ onComplete: handleAnalysisComplete,
2115
+ onBack: handleAnalysisBack,
2116
+ onError: () => {
2117
+ }
2118
+ }
2119
+ ),
2120
+ screen === "settings" && /* @__PURE__ */ jsx13(
2121
+ SettingsScreen,
2122
+ {
2123
+ onBack: () => navigateTo("dashboard"),
2124
+ onModelChanged: handleModelChanged,
2125
+ onUsernameChanged: handleUsernameChanged
2126
+ }
2127
+ ),
2128
+ screen === "resume-session" && /* @__PURE__ */ jsx13(
2129
+ ResumeSession,
2130
+ {
2131
+ initialId: resumeId2,
2132
+ onResume: handleResumeSession,
2133
+ onBack: () => navigateTo("dashboard")
2134
+ }
2135
+ )
2136
+ ] });
2137
+ }
2138
+
2139
+ // src/cli.tsx
2140
+ import { jsx as jsx14 } from "react/jsx-runtime";
2141
+ var args = process.argv.slice(2);
2142
+ function getFlag(flag) {
2143
+ const idx = args.findIndex((a) => a === flag || a === `--${flag}`);
2144
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
2145
+ const prefixed = args.find((a) => a.startsWith(`--${flag}=`));
2146
+ if (prefixed) return prefixed.split("=")[1];
2147
+ return void 0;
2148
+ }
2149
+ function hasFlag(flag) {
2150
+ return args.includes(`--${flag}`) || args.includes(`-${flag[0]}`);
2151
+ }
2152
+ if (hasFlag("version") || hasFlag("v")) {
2153
+ console.log("git-reverse v1.0.0");
2154
+ process.exit(0);
2155
+ }
2156
+ if (hasFlag("help") || hasFlag("h")) {
2157
+ console.log(`
2158
+ git-reverse \u2014 Reverse-engineer any GitHub repository
2159
+
2160
+ Usage:
2161
+ git-reverse Launch interactive CLI
2162
+ git-reverse --resume <id> Resume a saved session
2163
+ git-reverse --version Show version
2164
+ git-reverse --help Show this help
2165
+
2166
+ In the CLI:
2167
+ Paste a GitHub URL to analyze it.
2168
+ Optionally add a question after the URL.
2169
+
2170
+ Commands (type \\ to see palette):
2171
+ \\settings Modify username / API key / model
2172
+ \\deepdive Switch to deep-dive mode
2173
+ \\compact Summarize current session
2174
+ \\resume <id> Resume a past session
2175
+ \\sessions List all saved sessions
2176
+ \\quit Exit and save session
2177
+
2178
+ Examples:
2179
+ https://github.com/sindresorhus/ora
2180
+ https://github.com/vitejs/vite how does the plugin system work?
2181
+ https://github.com/vercel/next.js can I convert this to an Electron app?
2182
+
2183
+ Sessions are stored locally at:
2184
+ ~/.config/git-reverse/ (Linux/Mac)
2185
+ %APPDATA%\\git-reverse\\ (Windows)
2186
+ `);
2187
+ process.exit(0);
2188
+ }
2189
+ var resumeId = getFlag("resume");
2190
+ var { waitUntilExit } = render(
2191
+ /* @__PURE__ */ jsx14(App, { resumeSessionId: resumeId }),
2192
+ {
2193
+ exitOnCtrlC: false,
2194
+ // Handled manually by SessionManager for graceful save
2195
+ debug: false
2196
+ }
2197
+ );
2198
+ waitUntilExit().then(() => {
2199
+ process.exit(0);
2200
+ }).catch(() => {
2201
+ process.exit(1);
2202
+ });