@dyyz1993/create-agent 2.0.0 → 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 (165) 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 +24 -1
  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 +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -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
  }
@@ -5,3 +5,4 @@ export { register as timer } from "./timer";
5
5
  export { register as chat } from "./chat";
6
6
  export { register as git } from "./git";
7
7
  export { register as feed } from "./feed";
8
+ export { register as debug } from "./debug";