@dyyz1993/create-agent 2.0.1 → 2.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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +1 -0
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +2 -0
  9. package/templates/agent/package.json +60 -2
  10. package/templates/agent/src/mainview/App.tsx +29 -26
  11. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  12. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  13. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  14. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +3 -3
  15. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  16. package/templates/agent/src/mainview/lib/api-client.ts +1 -4
  17. package/templates/agent/src/mainview/main.tsx +4 -10
  18. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  19. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  20. package/templates/agent/src/server-config.ts +1 -1
  21. package/templates/agent/src/server.ts +1 -2
  22. package/templates/agent/src/shared/handlers/chat.ts +5 -5
  23. package/templates/agent/src/shared/handlers/debug.ts +5 -1
  24. package/templates/agent/src/shared/handlers/git.ts +286 -243
  25. package/templates/agent/src/shared/http-routes.ts +1 -1
  26. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  27. package/templates/agent/tsconfig.ipc.json +5 -1
  28. package/templates/agent/tsconfig.json +3 -1
  29. package/templates/chat/package.json +3 -0
  30. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  31. package/templates/chat/src/mainview/lib/api-client.ts +2 -5
  32. package/templates/chat/src/mainview/main.tsx +10 -7
  33. package/templates/chat/src/server-config.ts +1 -1
  34. package/templates/chat/src/server.ts +1 -2
  35. package/templates/chat/src/shared/handlers/chat.ts +5 -5
  36. package/templates/chat/src/shared/handlers/debug.ts +5 -1
  37. package/templates/chat/src/shared/http-routes.ts +1 -1
  38. package/templates/chat/tsconfig.ipc.json +5 -1
  39. package/templates/chat/tsconfig.json +12 -2
  40. package/templates/general/package.json +3 -0
  41. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  42. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  43. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  44. package/templates/general/src/mainview/lib/api-client.ts +2 -5
  45. package/templates/general/src/mainview/main.tsx +10 -7
  46. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  47. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  48. package/templates/general/src/server-config.ts +1 -1
  49. package/templates/general/src/server.ts +1 -2
  50. package/templates/general/src/shared/handlers/chat.ts +5 -5
  51. package/templates/general/src/shared/handlers/debug.ts +5 -1
  52. package/templates/general/src/shared/handlers/git.ts +286 -243
  53. package/templates/general/src/shared/http-routes.ts +1 -1
  54. package/templates/general/tsconfig.ipc.json +5 -1
  55. package/templates/general/tsconfig.json +12 -2
  56. package/templates/shared/components/ErrorBoundary.tsx +50 -49
  57. package/templates/shared/http-routes.ts +210 -190
@@ -4,260 +4,303 @@ import type { RPCMethods, HandlerOptions } from "../rpc-schema";
4
4
  import type { GitFileChange } from "../modules/git";
5
5
 
6
6
  type RegisterFn = <K extends keyof RPCMethods & string>(
7
- method: K,
8
- handler: (params: MethodParams<RPCMethods, K>) => Promise<MethodResult<RPCMethods, K>>,
7
+ method: K,
8
+ handler: (params: MethodParams<RPCMethods, K>) => Promise<MethodResult<RPCMethods, K>>
9
9
  ) => void;
10
10
 
11
11
  function execGit(args: string[], cwd: string, allowNonZero = false): string {
12
- const proc = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
13
- if (proc.exitCode !== 0 && !allowNonZero) {
14
- throw new Error(proc.stderr.toString().trim() || `git ${args[0]} failed`);
15
- }
16
- return proc.stdout.toString();
12
+ const proc = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
13
+ if (proc.exitCode !== 0 && !allowNonZero) {
14
+ throw new Error(proc.stderr.toString().trim() || `git ${args[0]} failed`);
15
+ }
16
+ return proc.stdout.toString();
17
17
  }
18
18
 
19
19
  function getRepoRoot(cwd: string): string {
20
- return execGit(["rev-parse", "--show-toplevel"], cwd).trim();
20
+ return execGit(["rev-parse", "--show-toplevel"], cwd).trim();
21
21
  }
22
22
 
23
23
  function parseStatus(output: string): {
24
- staged: GitFileChange[];
25
- changed: GitFileChange[];
26
- untracked: string[];
24
+ staged: GitFileChange[];
25
+ changed: GitFileChange[];
26
+ untracked: string[];
27
27
  } {
28
- const staged: GitFileChange[] = [];
29
- const changed: GitFileChange[] = [];
30
- const untracked: string[] = [];
31
-
32
- for (const line of output.split("\n")) {
33
- if (!line.trim()) continue;
34
- const xy = line.slice(0, 2);
35
- const filePath = line.slice(3).trim();
36
-
37
- const statusMap: Record<string, "modified" | "added" | "deleted" | "renamed" | "copied"> = {
38
- M: "modified", A: "added", D: "deleted", R: "renamed", C: "copied",
39
- };
40
-
41
- // Index (staged) - first char
42
- const indexStatus = xy[0];
43
- if (indexStatus !== " " && indexStatus !== "?" && statusMap[indexStatus]) {
44
- const path = indexStatus === "R" ? filePath.split(" -> ")[1] : filePath;
45
- staged.push({ path, status: statusMap[indexStatus] });
46
- }
47
-
48
- // Working tree - second char
49
- const wtStatus = xy[1];
50
- if (wtStatus !== " " && wtStatus !== "?" && statusMap[wtStatus]) {
51
- changed.push({ path: filePath, status: statusMap[wtStatus] });
52
- }
53
-
54
- // Untracked
55
- if (xy === "??") {
56
- untracked.push(filePath);
57
- }
58
- }
59
-
60
- return { staged, changed, untracked };
28
+ const staged: GitFileChange[] = [];
29
+ const changed: GitFileChange[] = [];
30
+ const untracked: string[] = [];
31
+
32
+ for (const line of output.split("\n")) {
33
+ if (!line.trim()) continue;
34
+ const xy = line.slice(0, 2);
35
+ const filePath = line.slice(3).trim();
36
+
37
+ const statusMap: Record<string, "modified" | "added" | "deleted" | "renamed" | "copied"> = {
38
+ M: "modified",
39
+ A: "added",
40
+ D: "deleted",
41
+ R: "renamed",
42
+ C: "copied",
43
+ };
44
+
45
+ // Index (staged) - first char
46
+ const indexStatus = xy[0];
47
+ if (
48
+ indexStatus !== " " &&
49
+ indexStatus !== "?" &&
50
+ indexStatus !== undefined &&
51
+ statusMap[indexStatus]
52
+ ) {
53
+ const path = indexStatus === "R" ? filePath.split(" -> ")[1]! : filePath;
54
+ staged.push({ path, status: statusMap[indexStatus]! });
55
+ }
56
+
57
+ // Working tree - second char
58
+ const wtStatus = xy[1];
59
+ if (wtStatus !== " " && wtStatus !== "?" && wtStatus !== undefined && statusMap[wtStatus]) {
60
+ changed.push({ path: filePath, status: statusMap[wtStatus]! });
61
+ }
62
+
63
+ // Untracked
64
+ if (xy === "??") {
65
+ untracked.push(filePath);
66
+ }
67
+ }
68
+
69
+ return { staged, changed, untracked };
61
70
  }
62
71
 
63
72
  export function register(server: RPCServer, _options: HandlerOptions): void {
64
- const r: RegisterFn = (method, handler) => {
65
- server.register(method, handler as (params: unknown) => Promise<unknown>);
66
- };
67
-
68
- r("git.status", async (params) => {
69
- const repoRoot = getRepoRoot(params.repoPath);
70
- const output = execGit(["status", "--porcelain=v1", "--branch"], repoRoot);
71
- const lines = output.split("\n");
72
-
73
- // Parse branch info from first line
74
- const branchLine = lines[0] || "";
75
- const branchMatch = branchLine.match(/^## (.+?)(?:\.\.\.(\S+))?(?:\s+\[(ahead\s+(\d+))?(?:,\s*)?(behind\s+(\d+))?\])?$/);
76
- const branch = branchMatch?.[1]?.replace("HEAD detached", "").replace(/[()]/g, "").trim() || "unknown";
77
- const ahead = branchMatch?.[3] ? parseInt(branchMatch[3]) : 0;
78
- const behind = branchMatch?.[5] ? parseInt(branchMatch[5]) : 0;
79
-
80
- const { staged, changed, untracked } = parseStatus(lines.slice(1).join("\n"));
81
-
82
- return { staged, changed, untracked, branch, ahead, behind };
83
- });
84
-
85
- r("git.diff", async (params) => {
86
- const repoRoot = getRepoRoot(params.repoPath);
87
- let diff = "";
88
- if (params.staged) {
89
- diff = execGit(["diff", "--cached", "--", params.filePath], repoRoot);
90
- } else {
91
- diff = execGit(["diff", "--", params.filePath], repoRoot);
92
- if (!diff) {
93
- try {
94
- diff = execGit(["diff", "--no-index", "/dev/null", params.filePath], repoRoot, true);
95
- } catch {
96
- // ignore
97
- }
98
- }
99
- }
100
-
101
- // Get old content (HEAD version) and new content (working tree)
102
- let oldContent = "";
103
- let newContent = "";
104
- try {
105
- oldContent = execGit(["show", `HEAD:${params.filePath}`], repoRoot);
106
- } catch {
107
- // New file — no old content
108
- }
109
- try {
110
- const { readFile } = await import("fs/promises");
111
- const { join } = await import("path");
112
- newContent = (await readFile(join(repoRoot, params.filePath))).toString();
113
- } catch {
114
- // Deleted file no new content
115
- }
116
-
117
- return { filePath: params.filePath, diff, oldContent, newContent };
118
- });
119
-
120
- r("git.log", async (params) => {
121
- const repoRoot = getRepoRoot(params.repoPath);
122
- const count = params.maxCount || 50;
123
- const output = execGit([
124
- "log", `--max-count=${count}`,
125
- "--pretty=format:%H|%h|%s|%an|%aI",
126
- ], repoRoot);
127
-
128
- const commits = output.split("\n").filter(Boolean).map((line) => {
129
- const [hash, shortHash, message, author, date] = line.split("|");
130
- return { hash, shortHash, message, author, date };
131
- });
132
-
133
- return { commits };
134
- });
135
-
136
- r("git.commitFiles", async (params) => {
137
- const repoRoot = getRepoRoot(params.repoPath);
138
- const output = execGit([
139
- "diff-tree", "--no-commit-id", "--name-status", "-r", params.hash,
140
- ], repoRoot);
141
-
142
- const statusMap: Record<string, GitFileChange["status"]> = {
143
- M: "modified", A: "added", D: "deleted", R: "renamed", C: "copied",
144
- };
145
-
146
- const files: GitFileChange[] = output.split("\n").filter(Boolean).map((line) => {
147
- const [status, ...pathParts] = line.split("\t");
148
- const path = pathParts.join("\t"); // handle paths with tabs (renames: old\tnew)
149
- return { path: status === "R" ? path.split("\t").pop()! : path, status: statusMap[status] || "modified" };
150
- });
151
-
152
- return { files };
153
- });
154
-
155
- r("git.commitFileDiff", async (params) => {
156
- const repoRoot = getRepoRoot(params.repoPath);
157
- const { hash, filePath } = params;
158
-
159
- // Get the diff for this file in this commit
160
- const diff = execGit(["diff", `${hash}^..${hash}`, "--", filePath], repoRoot, true);
161
-
162
- // Get old content (parent commit version)
163
- let oldContent = "";
164
- try {
165
- oldContent = execGit(["show", `${hash}^:${filePath}`], repoRoot);
166
- } catch {
167
- // File was added in this commit — no old content
168
- }
169
-
170
- // Get new content (this commit version)
171
- let newContent = "";
172
- try {
173
- newContent = execGit(["show", `${hash}:${filePath}`], repoRoot);
174
- } catch {
175
- // File was deleted in this commit — no new content
176
- }
177
-
178
- return { filePath, diff, oldContent, newContent };
179
- });
180
-
181
- r("git.branches", async (params) => {
182
- const repoRoot = getRepoRoot(params.repoPath);
183
- const output = execGit(["branch", "-a", "--no-color"], repoRoot);
184
- const branches = output.split("\n").filter(Boolean).map((line) => {
185
- const isCurrent = line.startsWith("*");
186
- const name = line.replace(/^\*?\s+/, "").trim();
187
- const isRemote = name.startsWith("remotes/");
188
- return { name, isCurrent, isRemote };
189
- });
190
- return { branches };
191
- });
192
-
193
- r("git.checkout", async (params) => {
194
- const repoRoot = getRepoRoot(params.repoPath);
195
- execGit(["checkout", params.branch], repoRoot);
196
- return { ok: true };
197
- });
198
-
199
- r("git.add", async (params) => {
200
- const repoRoot = getRepoRoot(params.repoPath);
201
- execGit(["add", ...params.paths], repoRoot);
202
- return { ok: true };
203
- });
204
-
205
- r("git.reset", async (params) => {
206
- const repoRoot = getRepoRoot(params.repoPath);
207
- execGit(["reset", "HEAD", "--", ...params.paths], repoRoot);
208
- return { ok: true };
209
- });
210
-
211
- r("git.commit", async (params) => {
212
- const repoRoot = getRepoRoot(params.repoPath);
213
- const output = execGit(["commit", "-m", params.message], repoRoot);
214
- // Extract hash from output like "[main abc1234] message"
215
- const hashMatch = output.match(/\[[\w\-/.]+\s+([0-9a-f]{7,40})\]/);
216
- const shortHash = hashMatch?.[1] || "";
217
- let hash = "";
218
- if (shortHash) {
219
- hash = execGit(["rev-parse", shortHash], repoRoot).trim();
220
- }
221
- return { hash, shortHash };
222
- });
223
-
224
- r("git.push", async (params) => {
225
- const repoRoot = getRepoRoot(params.repoPath);
226
- execGit(["push"], repoRoot);
227
- return { ok: true };
228
- });
229
-
230
- r("git.pull", async (params) => {
231
- const repoRoot = getRepoRoot(params.repoPath);
232
- execGit(["pull"], repoRoot);
233
- return { ok: true };
234
- });
235
-
236
- r("git.worktreeList", async (params) => {
237
- const repoRoot = getRepoRoot(params.repoPath);
238
- const output = execGit(["worktree", "list", "--porcelain"], repoRoot);
239
- const worktrees: { path: string; branch: string; isMain: boolean }[] = [];
240
- let current: Partial<typeof worktrees[0]> = {};
241
-
242
- for (const line of output.split("\n")) {
243
- if (line.startsWith("worktree ")) {
244
- if (current.path) {
245
- worktrees.push({ path: current.path!, branch: current.branch || "", isMain: !!current.isMain });
246
- }
247
- current = { path: line.slice(9), isMain: false };
248
- } else if (line.startsWith("branch ")) {
249
- current.branch = line.slice(7).replace("refs/heads/", "");
250
- } else if (line === "bare") {
251
- current.isMain = false;
252
- } else if (line === "" && current.path) {
253
- // first worktree is main
254
- if (worktrees.length === 0) current.isMain = true;
255
- }
256
- }
257
- if (current.path) {
258
- worktrees.push({ path: current.path!, branch: current.branch || "", isMain: !!current.isMain });
259
- }
260
-
261
- return { worktrees };
262
- });
73
+ const r: RegisterFn = (method, handler) => {
74
+ server.register(method, handler as (params: unknown) => Promise<unknown>);
75
+ };
76
+
77
+ r("git.status", async (params) => {
78
+ const repoRoot = getRepoRoot(params.repoPath);
79
+ const output = execGit(["status", "--porcelain=v1", "--branch"], repoRoot);
80
+ const lines = output.split("\n");
81
+
82
+ // Parse branch info from first line
83
+ const branchLine = lines[0] || "";
84
+ const branchMatch = branchLine.match(
85
+ /^## (.+?)(?:\.\.\.(\S+))?(?:\s+\[(ahead\s+(\d+))?(?:,\s*)?(behind\s+(\d+))?\])?$/
86
+ );
87
+ const branch =
88
+ branchMatch?.[1]?.replace("HEAD detached", "").replace(/[()]/g, "").trim() || "unknown";
89
+ const ahead = branchMatch?.[3] ? parseInt(branchMatch[3]) : 0;
90
+ const behind = branchMatch?.[5] ? parseInt(branchMatch[5]) : 0;
91
+
92
+ const { staged, changed, untracked } = parseStatus(lines.slice(1).join("\n"));
93
+
94
+ return { staged, changed, untracked, branch, ahead, behind };
95
+ });
96
+
97
+ r("git.diff", async (params) => {
98
+ const repoRoot = getRepoRoot(params.repoPath);
99
+ let diff = "";
100
+ if (params.staged) {
101
+ diff = execGit(["diff", "--cached", "--", params.filePath], repoRoot);
102
+ } else {
103
+ diff = execGit(["diff", "--", params.filePath], repoRoot);
104
+ if (!diff) {
105
+ try {
106
+ diff = execGit(["diff", "--no-index", "/dev/null", params.filePath], repoRoot, true);
107
+ } catch {
108
+ // ignore
109
+ }
110
+ }
111
+ }
112
+
113
+ // Get old content (HEAD version) and new content (working tree)
114
+ let oldContent = "";
115
+ let newContent = "";
116
+ try {
117
+ oldContent = execGit(["show", `HEAD:${params.filePath}`], repoRoot);
118
+ } catch {
119
+ // New file no old content
120
+ }
121
+ try {
122
+ const { readFile } = await import("fs/promises");
123
+ const { join } = await import("path");
124
+ newContent = (await readFile(join(repoRoot, params.filePath))).toString();
125
+ } catch {
126
+ // Deleted file no new content
127
+ }
128
+
129
+ return { filePath: params.filePath, diff, oldContent, newContent };
130
+ });
131
+
132
+ r("git.log", async (params) => {
133
+ const repoRoot = getRepoRoot(params.repoPath);
134
+ const count = params.maxCount || 50;
135
+ const output = execGit(
136
+ ["log", `--max-count=${count}`, "--pretty=format:%H|%h|%s|%an|%aI"],
137
+ repoRoot
138
+ );
139
+
140
+ const commits = output
141
+ .split("\n")
142
+ .filter(Boolean)
143
+ .map((line) => {
144
+ const parts = line.split("|");
145
+ return {
146
+ hash: parts[0]!,
147
+ shortHash: parts[1]!,
148
+ message: parts[2]!,
149
+ author: parts[3]!,
150
+ date: parts[4]!,
151
+ };
152
+ });
153
+
154
+ return { commits };
155
+ });
156
+
157
+ r("git.commitFiles", async (params) => {
158
+ const repoRoot = getRepoRoot(params.repoPath);
159
+ const output = execGit(
160
+ ["diff-tree", "--no-commit-id", "--name-status", "-r", params.hash],
161
+ repoRoot
162
+ );
163
+
164
+ const statusMap: Record<string, GitFileChange["status"]> = {
165
+ M: "modified",
166
+ A: "added",
167
+ D: "deleted",
168
+ R: "renamed",
169
+ C: "copied",
170
+ };
171
+
172
+ const files: GitFileChange[] = output
173
+ .split("\n")
174
+ .filter(Boolean)
175
+ .map((line) => {
176
+ const [status, ...pathParts] = line.split("\t");
177
+ const path = pathParts.join("\t"); // handle paths with tabs (renames: old\tnew)
178
+ return {
179
+ path: status === "R" ? path.split("\t").pop()! : path,
180
+ status: statusMap[status!] || "modified",
181
+ };
182
+ });
183
+
184
+ return { files };
185
+ });
186
+
187
+ r("git.commitFileDiff", async (params) => {
188
+ const repoRoot = getRepoRoot(params.repoPath);
189
+ const { hash, filePath } = params;
190
+
191
+ // Get the diff for this file in this commit
192
+ const diff = execGit(["diff", `${hash}^..${hash}`, "--", filePath], repoRoot, true);
193
+
194
+ // Get old content (parent commit version)
195
+ let oldContent = "";
196
+ try {
197
+ oldContent = execGit(["show", `${hash}^:${filePath}`], repoRoot);
198
+ } catch {
199
+ // File was added in this commit — no old content
200
+ }
201
+
202
+ // Get new content (this commit version)
203
+ let newContent = "";
204
+ try {
205
+ newContent = execGit(["show", `${hash}:${filePath}`], repoRoot);
206
+ } catch {
207
+ // File was deleted in this commit — no new content
208
+ }
209
+
210
+ return { filePath, diff, oldContent, newContent };
211
+ });
212
+
213
+ r("git.branches", async (params) => {
214
+ const repoRoot = getRepoRoot(params.repoPath);
215
+ const output = execGit(["branch", "-a", "--no-color"], repoRoot);
216
+ const branches = output
217
+ .split("\n")
218
+ .filter(Boolean)
219
+ .map((line) => {
220
+ const isCurrent = line.startsWith("*");
221
+ const name = line.replace(/^\*?\s+/, "").trim();
222
+ const isRemote = name.startsWith("remotes/");
223
+ return { name, isCurrent, isRemote };
224
+ });
225
+ return { branches };
226
+ });
227
+
228
+ r("git.checkout", async (params) => {
229
+ const repoRoot = getRepoRoot(params.repoPath);
230
+ execGit(["checkout", params.branch], repoRoot);
231
+ return { ok: true };
232
+ });
233
+
234
+ r("git.add", async (params) => {
235
+ const repoRoot = getRepoRoot(params.repoPath);
236
+ execGit(["add", ...params.paths], repoRoot);
237
+ return { ok: true };
238
+ });
239
+
240
+ r("git.reset", async (params) => {
241
+ const repoRoot = getRepoRoot(params.repoPath);
242
+ execGit(["reset", "HEAD", "--", ...params.paths], repoRoot);
243
+ return { ok: true };
244
+ });
245
+
246
+ r("git.commit", async (params) => {
247
+ const repoRoot = getRepoRoot(params.repoPath);
248
+ const output = execGit(["commit", "-m", params.message], repoRoot);
249
+ // Extract hash from output like "[main abc1234] message"
250
+ const hashMatch = output.match(/\[[\w\-/.]+\s+([0-9a-f]{7,40})\]/);
251
+ const shortHash = hashMatch?.[1] || "";
252
+ let hash = "";
253
+ if (shortHash) {
254
+ hash = execGit(["rev-parse", shortHash], repoRoot).trim();
255
+ }
256
+ return { hash, shortHash };
257
+ });
258
+
259
+ r("git.push", async (params) => {
260
+ const repoRoot = getRepoRoot(params.repoPath);
261
+ execGit(["push"], repoRoot);
262
+ return { ok: true };
263
+ });
264
+
265
+ r("git.pull", async (params) => {
266
+ const repoRoot = getRepoRoot(params.repoPath);
267
+ execGit(["pull"], repoRoot);
268
+ return { ok: true };
269
+ });
270
+
271
+ r("git.worktreeList", async (params) => {
272
+ const repoRoot = getRepoRoot(params.repoPath);
273
+ const output = execGit(["worktree", "list", "--porcelain"], repoRoot);
274
+ const worktrees: { path: string; branch: string; isMain: boolean }[] = [];
275
+ let current: Partial<(typeof worktrees)[0]> = {};
276
+
277
+ for (const line of output.split("\n")) {
278
+ if (line.startsWith("worktree ")) {
279
+ if (current.path) {
280
+ worktrees.push({
281
+ path: current.path!,
282
+ branch: current.branch || "",
283
+ isMain: !!current.isMain,
284
+ });
285
+ }
286
+ current = { path: line.slice(9), isMain: false };
287
+ } else if (line.startsWith("branch ")) {
288
+ current.branch = line.slice(7).replace("refs/heads/", "");
289
+ } else if (line === "bare") {
290
+ current.isMain = false;
291
+ } else if (line === "" && current.path) {
292
+ // first worktree is main
293
+ if (worktrees.length === 0) current.isMain = true;
294
+ }
295
+ }
296
+ if (current.path) {
297
+ worktrees.push({
298
+ path: current.path!,
299
+ branch: current.branch || "",
300
+ isMain: !!current.isMain,
301
+ });
302
+ }
303
+
304
+ return { worktrees };
305
+ });
263
306
  }
@@ -178,7 +178,7 @@ async function handleFileContent(
178
178
  const range = req.headers["range"];
179
179
  if (range) {
180
180
  const parts = range.replace(/bytes=/, "").split("-");
181
- const start = parseInt(parts[0], 10);
181
+ const start = parseInt(parts[0]!, 10);
182
182
  const end = parts[1] ? parseInt(parts[1], 10) : s.size - 1;
183
183
  const chunkSize = end - start + 1;
184
184