claude-ide-bridge 1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/activityLog.d.ts +26 -0
  4. package/dist/activityLog.js +76 -0
  5. package/dist/activityLog.js.map +1 -0
  6. package/dist/bridge.d.ts +19 -0
  7. package/dist/bridge.js +277 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +221 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/errors.d.ts +16 -0
  13. package/dist/errors.js +20 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/extensionClient.d.ts +193 -0
  16. package/dist/extensionClient.js +698 -0
  17. package/dist/extensionClient.js.map +1 -0
  18. package/dist/fileLock.d.ts +12 -0
  19. package/dist/fileLock.js +30 -0
  20. package/dist/fileLock.js.map +1 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +38 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/lockfile.d.ts +12 -0
  25. package/dist/lockfile.js +127 -0
  26. package/dist/lockfile.js.map +1 -0
  27. package/dist/logger.d.ts +16 -0
  28. package/dist/logger.js +68 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/probe.d.ts +22 -0
  31. package/dist/probe.js +45 -0
  32. package/dist/probe.js.map +1 -0
  33. package/dist/server.d.ts +25 -0
  34. package/dist/server.js +265 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/tools/activityLog.d.ts +39 -0
  37. package/dist/tools/activityLog.js +49 -0
  38. package/dist/tools/activityLog.js.map +1 -0
  39. package/dist/tools/aiComments.d.ts +26 -0
  40. package/dist/tools/aiComments.js +196 -0
  41. package/dist/tools/aiComments.js.map +1 -0
  42. package/dist/tools/bridgeStatus.d.ts +21 -0
  43. package/dist/tools/bridgeStatus.js +41 -0
  44. package/dist/tools/bridgeStatus.js.map +1 -0
  45. package/dist/tools/checkDocumentDirty.d.ts +28 -0
  46. package/dist/tools/checkDocumentDirty.js +61 -0
  47. package/dist/tools/checkDocumentDirty.js.map +1 -0
  48. package/dist/tools/clipboard.d.ts +50 -0
  49. package/dist/tools/clipboard.js +82 -0
  50. package/dist/tools/clipboard.js.map +1 -0
  51. package/dist/tools/closeTabs.d.ts +49 -0
  52. package/dist/tools/closeTabs.js +77 -0
  53. package/dist/tools/closeTabs.js.map +1 -0
  54. package/dist/tools/debug.d.ts +154 -0
  55. package/dist/tools/debug.js +248 -0
  56. package/dist/tools/debug.js.map +1 -0
  57. package/dist/tools/decorations.d.ts +92 -0
  58. package/dist/tools/decorations.js +150 -0
  59. package/dist/tools/decorations.js.map +1 -0
  60. package/dist/tools/diffDebugger.d.ts +62 -0
  61. package/dist/tools/diffDebugger.js +245 -0
  62. package/dist/tools/diffDebugger.js.map +1 -0
  63. package/dist/tools/editText.d.ts +80 -0
  64. package/dist/tools/editText.js +274 -0
  65. package/dist/tools/editText.js.map +1 -0
  66. package/dist/tools/fileOperations.d.ts +111 -0
  67. package/dist/tools/fileOperations.js +280 -0
  68. package/dist/tools/fileOperations.js.map +1 -0
  69. package/dist/tools/fileWatcher.d.ts +54 -0
  70. package/dist/tools/fileWatcher.js +100 -0
  71. package/dist/tools/fileWatcher.js.map +1 -0
  72. package/dist/tools/findFiles.d.ts +31 -0
  73. package/dist/tools/findFiles.js +119 -0
  74. package/dist/tools/findFiles.js.map +1 -0
  75. package/dist/tools/fixAllLintErrors.d.ts +29 -0
  76. package/dist/tools/fixAllLintErrors.js +114 -0
  77. package/dist/tools/fixAllLintErrors.js.map +1 -0
  78. package/dist/tools/flowGuardian.d.ts +61 -0
  79. package/dist/tools/flowGuardian.js +311 -0
  80. package/dist/tools/flowGuardian.js.map +1 -0
  81. package/dist/tools/formatDocument.d.ts +30 -0
  82. package/dist/tools/formatDocument.js +132 -0
  83. package/dist/tools/formatDocument.js.map +1 -0
  84. package/dist/tools/formatFile.d.ts +28 -0
  85. package/dist/tools/formatFile.js +110 -0
  86. package/dist/tools/formatFile.js.map +1 -0
  87. package/dist/tools/getBufferContent.d.ts +38 -0
  88. package/dist/tools/getBufferContent.js +100 -0
  89. package/dist/tools/getBufferContent.js.map +1 -0
  90. package/dist/tools/getCurrentSelection.d.ts +43 -0
  91. package/dist/tools/getCurrentSelection.js +75 -0
  92. package/dist/tools/getCurrentSelection.js.map +1 -0
  93. package/dist/tools/getDiagnostics.d.ts +38 -0
  94. package/dist/tools/getDiagnostics.js +204 -0
  95. package/dist/tools/getDiagnostics.js.map +1 -0
  96. package/dist/tools/getDocumentSymbols.d.ts +27 -0
  97. package/dist/tools/getDocumentSymbols.js +133 -0
  98. package/dist/tools/getDocumentSymbols.js.map +1 -0
  99. package/dist/tools/getFileTree.d.ts +36 -0
  100. package/dist/tools/getFileTree.js +111 -0
  101. package/dist/tools/getFileTree.js.map +1 -0
  102. package/dist/tools/getGitDiff.d.ts +34 -0
  103. package/dist/tools/getGitDiff.js +57 -0
  104. package/dist/tools/getGitDiff.js.map +1 -0
  105. package/dist/tools/getGitLog.d.ts +30 -0
  106. package/dist/tools/getGitLog.js +65 -0
  107. package/dist/tools/getGitLog.js.map +1 -0
  108. package/dist/tools/getGitStatus.d.ts +26 -0
  109. package/dist/tools/getGitStatus.js +95 -0
  110. package/dist/tools/getGitStatus.js.map +1 -0
  111. package/dist/tools/getOpenEditors.d.ts +21 -0
  112. package/dist/tools/getOpenEditors.js +84 -0
  113. package/dist/tools/getOpenEditors.js.map +1 -0
  114. package/dist/tools/getProjectInfo.d.ts +20 -0
  115. package/dist/tools/getProjectInfo.js +315 -0
  116. package/dist/tools/getProjectInfo.js.map +1 -0
  117. package/dist/tools/getToolCapabilities.d.ts +23 -0
  118. package/dist/tools/getToolCapabilities.js +249 -0
  119. package/dist/tools/getToolCapabilities.js.map +1 -0
  120. package/dist/tools/getWorkspaceFolders.d.ts +21 -0
  121. package/dist/tools/getWorkspaceFolders.js +47 -0
  122. package/dist/tools/getWorkspaceFolders.js.map +1 -0
  123. package/dist/tools/gitHistory.d.ts +78 -0
  124. package/dist/tools/gitHistory.js +151 -0
  125. package/dist/tools/gitHistory.js.map +1 -0
  126. package/dist/tools/gitWrite.d.ts +335 -0
  127. package/dist/tools/gitWrite.js +859 -0
  128. package/dist/tools/gitWrite.js.map +1 -0
  129. package/dist/tools/github/actions.d.ts +67 -0
  130. package/dist/tools/github/actions.js +155 -0
  131. package/dist/tools/github/actions.js.map +1 -0
  132. package/dist/tools/github/index.d.ts +4 -0
  133. package/dist/tools/github/index.js +5 -0
  134. package/dist/tools/github/index.js.map +1 -0
  135. package/dist/tools/github/issues.d.ts +140 -0
  136. package/dist/tools/github/issues.js +279 -0
  137. package/dist/tools/github/issues.js.map +1 -0
  138. package/dist/tools/github/pr.d.ts +101 -0
  139. package/dist/tools/github/pr.js +215 -0
  140. package/dist/tools/github/pr.js.map +1 -0
  141. package/dist/tools/github/review.d.ts +101 -0
  142. package/dist/tools/github/review.js +292 -0
  143. package/dist/tools/github/review.js.map +1 -0
  144. package/dist/tools/github/shared.d.ts +4 -0
  145. package/dist/tools/github/shared.js +12 -0
  146. package/dist/tools/github/shared.js.map +1 -0
  147. package/dist/tools/github.d.ts +308 -0
  148. package/dist/tools/github.js +656 -0
  149. package/dist/tools/github.js.map +1 -0
  150. package/dist/tools/hoverAtCursor.d.ts +22 -0
  151. package/dist/tools/hoverAtCursor.js +51 -0
  152. package/dist/tools/hoverAtCursor.js.map +1 -0
  153. package/dist/tools/httpClient.d.ts +83 -0
  154. package/dist/tools/httpClient.js +335 -0
  155. package/dist/tools/httpClient.js.map +1 -0
  156. package/dist/tools/index.d.ts +7 -0
  157. package/dist/tools/index.js +246 -0
  158. package/dist/tools/index.js.map +1 -0
  159. package/dist/tools/inlayHints.d.ts +38 -0
  160. package/dist/tools/inlayHints.js +56 -0
  161. package/dist/tools/inlayHints.js.map +1 -0
  162. package/dist/tools/linters/biome.d.ts +2 -0
  163. package/dist/tools/linters/biome.js +44 -0
  164. package/dist/tools/linters/biome.js.map +1 -0
  165. package/dist/tools/linters/cargo.d.ts +2 -0
  166. package/dist/tools/linters/cargo.js +45 -0
  167. package/dist/tools/linters/cargo.js.map +1 -0
  168. package/dist/tools/linters/eslint.d.ts +2 -0
  169. package/dist/tools/linters/eslint.js +59 -0
  170. package/dist/tools/linters/eslint.js.map +1 -0
  171. package/dist/tools/linters/govet.d.ts +2 -0
  172. package/dist/tools/linters/govet.js +37 -0
  173. package/dist/tools/linters/govet.js.map +1 -0
  174. package/dist/tools/linters/pyright.d.ts +2 -0
  175. package/dist/tools/linters/pyright.js +34 -0
  176. package/dist/tools/linters/pyright.js.map +1 -0
  177. package/dist/tools/linters/ruff.d.ts +2 -0
  178. package/dist/tools/linters/ruff.js +30 -0
  179. package/dist/tools/linters/ruff.js.map +1 -0
  180. package/dist/tools/linters/types.d.ts +16 -0
  181. package/dist/tools/linters/types.js +2 -0
  182. package/dist/tools/linters/types.js.map +1 -0
  183. package/dist/tools/linters/typescript.d.ts +2 -0
  184. package/dist/tools/linters/typescript.js +38 -0
  185. package/dist/tools/linters/typescript.js.map +1 -0
  186. package/dist/tools/lsp.d.ts +310 -0
  187. package/dist/tools/lsp.js +684 -0
  188. package/dist/tools/lsp.js.map +1 -0
  189. package/dist/tools/notebook.d.ts +95 -0
  190. package/dist/tools/notebook.js +144 -0
  191. package/dist/tools/notebook.js.map +1 -0
  192. package/dist/tools/openDiff.d.ts +41 -0
  193. package/dist/tools/openDiff.js +116 -0
  194. package/dist/tools/openDiff.js.map +1 -0
  195. package/dist/tools/openFile.d.ts +34 -0
  196. package/dist/tools/openFile.js +102 -0
  197. package/dist/tools/openFile.js.map +1 -0
  198. package/dist/tools/organizeImports.d.ts +29 -0
  199. package/dist/tools/organizeImports.js +64 -0
  200. package/dist/tools/organizeImports.js.map +1 -0
  201. package/dist/tools/planPersistence.d.ts +196 -0
  202. package/dist/tools/planPersistence.js +437 -0
  203. package/dist/tools/planPersistence.js.map +1 -0
  204. package/dist/tools/replaceBlock.d.ts +40 -0
  205. package/dist/tools/replaceBlock.js +105 -0
  206. package/dist/tools/replaceBlock.js.map +1 -0
  207. package/dist/tools/runCommand.d.ts +43 -0
  208. package/dist/tools/runCommand.js +141 -0
  209. package/dist/tools/runCommand.js.map +1 -0
  210. package/dist/tools/runTests.d.ts +32 -0
  211. package/dist/tools/runTests.js +160 -0
  212. package/dist/tools/runTests.js.map +1 -0
  213. package/dist/tools/saveDocument.d.ts +28 -0
  214. package/dist/tools/saveDocument.js +58 -0
  215. package/dist/tools/saveDocument.js.map +1 -0
  216. package/dist/tools/searchAndReplace.d.ts +50 -0
  217. package/dist/tools/searchAndReplace.js +203 -0
  218. package/dist/tools/searchAndReplace.js.map +1 -0
  219. package/dist/tools/searchWorkspace.d.ts +52 -0
  220. package/dist/tools/searchWorkspace.js +159 -0
  221. package/dist/tools/searchWorkspace.js.map +1 -0
  222. package/dist/tools/setActiveWorkspaceFolder.d.ts +28 -0
  223. package/dist/tools/setActiveWorkspaceFolder.js +32 -0
  224. package/dist/tools/setActiveWorkspaceFolder.js.map +1 -0
  225. package/dist/tools/tasks.d.ts +56 -0
  226. package/dist/tools/tasks.js +89 -0
  227. package/dist/tools/tasks.js.map +1 -0
  228. package/dist/tools/terminal.d.ts +241 -0
  229. package/dist/tools/terminal.js +539 -0
  230. package/dist/tools/terminal.js.map +1 -0
  231. package/dist/tools/testRunners/cargoTest.d.ts +2 -0
  232. package/dist/tools/testRunners/cargoTest.js +123 -0
  233. package/dist/tools/testRunners/cargoTest.js.map +1 -0
  234. package/dist/tools/testRunners/goTest.d.ts +2 -0
  235. package/dist/tools/testRunners/goTest.js +106 -0
  236. package/dist/tools/testRunners/goTest.js.map +1 -0
  237. package/dist/tools/testRunners/pytest.d.ts +2 -0
  238. package/dist/tools/testRunners/pytest.js +133 -0
  239. package/dist/tools/testRunners/pytest.js.map +1 -0
  240. package/dist/tools/testRunners/types.d.ts +18 -0
  241. package/dist/tools/testRunners/types.js +2 -0
  242. package/dist/tools/testRunners/types.js.map +1 -0
  243. package/dist/tools/testRunners/vitestJest.d.ts +3 -0
  244. package/dist/tools/testRunners/vitestJest.js +178 -0
  245. package/dist/tools/testRunners/vitestJest.js.map +1 -0
  246. package/dist/tools/typeHierarchy.d.ts +45 -0
  247. package/dist/tools/typeHierarchy.js +65 -0
  248. package/dist/tools/typeHierarchy.js.map +1 -0
  249. package/dist/tools/utils.d.ts +54 -0
  250. package/dist/tools/utils.js +267 -0
  251. package/dist/tools/utils.js.map +1 -0
  252. package/dist/tools/vscodeCommands.d.ts +59 -0
  253. package/dist/tools/vscodeCommands.js +108 -0
  254. package/dist/tools/vscodeCommands.js.map +1 -0
  255. package/dist/tools/watchDiagnostics.d.ts +32 -0
  256. package/dist/tools/watchDiagnostics.js +87 -0
  257. package/dist/tools/watchDiagnostics.js.map +1 -0
  258. package/dist/tools/workspaceSettings.d.ts +67 -0
  259. package/dist/tools/workspaceSettings.js +102 -0
  260. package/dist/tools/workspaceSettings.js.map +1 -0
  261. package/dist/tools/workspaceSnapshots.d.ts +174 -0
  262. package/dist/tools/workspaceSnapshots.js +474 -0
  263. package/dist/tools/workspaceSnapshots.js.map +1 -0
  264. package/dist/transport.d.ts +57 -0
  265. package/dist/transport.js +417 -0
  266. package/dist/transport.js.map +1 -0
  267. package/dist/version.d.ts +2 -0
  268. package/dist/version.js +3 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/wsUtils.d.ts +9 -0
  271. package/dist/wsUtils.js +54 -0
  272. package/dist/wsUtils.js.map +1 -0
  273. package/package.json +69 -0
@@ -0,0 +1,859 @@
1
+ import { error, execSafe, optionalBool, optionalString, requireString, resolveFilePath, success, } from "./utils.js";
2
+ const VALID_REF_RE = /^[\w.\-/]+$/;
3
+ async function runGit(args, cwd, opts = {}) {
4
+ const result = await execSafe("git", args, {
5
+ cwd,
6
+ signal: opts.signal,
7
+ timeout: opts.timeout ?? 30_000,
8
+ maxBuffer: opts.maxBuffer,
9
+ });
10
+ if (result.timedOut)
11
+ throw new Error("git command timed out");
12
+ if (result.exitCode !== 0)
13
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "git command failed");
14
+ return result;
15
+ }
16
+ async function checkGitRepo(workspace, signal) {
17
+ const r = await execSafe("git", ["rev-parse", "--git-dir"], {
18
+ cwd: workspace,
19
+ signal,
20
+ });
21
+ return r.exitCode === 0;
22
+ }
23
+ async function currentBranch(workspace, signal) {
24
+ const r = await execSafe("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
25
+ cwd: workspace,
26
+ signal,
27
+ });
28
+ return r.stdout.trim();
29
+ }
30
+ // Validate that all provided paths stay within the workspace.
31
+ // Uses resolveFilePath which also resolves symlinks (preventing symlink-based escapes).
32
+ function validatePaths(files, workspace) {
33
+ for (const f of files) {
34
+ try {
35
+ resolveFilePath(f, workspace);
36
+ }
37
+ catch (err) {
38
+ return err instanceof Error
39
+ ? err.message
40
+ : `Path escapes workspace: ${f}`;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ export function createGitAddTool(workspace) {
46
+ return {
47
+ schema: {
48
+ name: "gitAdd",
49
+ description: "Stage files for the next git commit. " +
50
+ "Pass specific file paths to stage selectively, or omit files to stage all tracked changes (equivalent to 'git add -u'). " +
51
+ "Use addUntracked: true to also stage new files not yet tracked (equivalent to 'git add .'). " +
52
+ "Check what will be staged first with getGitStatus.",
53
+ annotations: { destructiveHint: false },
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ files: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: "File paths to stage (absolute or workspace-relative). If omitted, stages all modified tracked files.",
61
+ },
62
+ addUntracked: {
63
+ type: "boolean",
64
+ description: "Also stage new untracked files. Default: false.",
65
+ },
66
+ },
67
+ additionalProperties: false,
68
+ },
69
+ },
70
+ handler: async (args, signal) => {
71
+ if (!(await checkGitRepo(workspace, signal))) {
72
+ return error("Not a git repository");
73
+ }
74
+ const rawFiles = args.files;
75
+ const addUntracked = optionalBool(args, "addUntracked") ?? false;
76
+ let addArgs;
77
+ if (Array.isArray(rawFiles) && rawFiles.length > 0) {
78
+ const files = rawFiles.map(String);
79
+ const pathErr = validatePaths(files, workspace);
80
+ if (pathErr)
81
+ return error(pathErr);
82
+ addArgs = ["add", "--", ...files];
83
+ }
84
+ else if (addUntracked) {
85
+ addArgs = ["add", "."];
86
+ }
87
+ else {
88
+ addArgs = ["add", "-u"];
89
+ }
90
+ try {
91
+ await runGit(addArgs, workspace, { signal, timeout: 15_000 });
92
+ }
93
+ catch (e) {
94
+ return error(`git add failed: ${e instanceof Error ? e.message : "unknown error"}`);
95
+ }
96
+ // Show what's now staged
97
+ const statusResult = await execSafe("git", ["diff", "--name-only", "--cached"], { cwd: workspace, signal });
98
+ const staged = statusResult.stdout
99
+ .split("\n")
100
+ .map((f) => f.trim())
101
+ .filter(Boolean);
102
+ return success({ staged, count: staged.length });
103
+ },
104
+ };
105
+ }
106
+ export function createGitCommitTool(workspace) {
107
+ return {
108
+ schema: {
109
+ name: "gitCommit",
110
+ description: "Create a git commit from the current staged changes. " +
111
+ "Use gitAdd first to stage files, or pass files here to stage-and-commit in one step. " +
112
+ "Returns the new commit hash, branch, and list of committed files. " +
113
+ "Will fail with a clear error if there is nothing staged and no files are provided.",
114
+ annotations: { destructiveHint: true },
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ message: {
119
+ type: "string",
120
+ description: "Commit message",
121
+ },
122
+ files: {
123
+ type: "array",
124
+ items: { type: "string" },
125
+ description: "Files to stage before committing. If omitted, commits whatever is already staged.",
126
+ },
127
+ addAll: {
128
+ type: "boolean",
129
+ description: "Stage all tracked changes before committing (git add -u). Default: false.",
130
+ },
131
+ },
132
+ required: ["message"],
133
+ additionalProperties: false,
134
+ },
135
+ },
136
+ handler: async (args, signal) => {
137
+ if (!(await checkGitRepo(workspace, signal))) {
138
+ return error("Not a git repository");
139
+ }
140
+ const message = requireString(args, "message", 4096);
141
+ if (message.trim().length === 0) {
142
+ return error("Commit message must not be empty");
143
+ }
144
+ const rawFiles = args.files;
145
+ const addAll = optionalBool(args, "addAll") ?? false;
146
+ // Stage files if requested
147
+ if (Array.isArray(rawFiles) && rawFiles.length > 0) {
148
+ const files = rawFiles.map(String);
149
+ const pathErr = validatePaths(files, workspace);
150
+ if (pathErr)
151
+ return error(pathErr);
152
+ try {
153
+ await runGit(["add", "--", ...files], workspace, {
154
+ signal,
155
+ timeout: 15_000,
156
+ });
157
+ }
158
+ catch (e) {
159
+ return error(`git add failed: ${e instanceof Error ? e.message : "unknown error"}`);
160
+ }
161
+ }
162
+ else if (addAll) {
163
+ try {
164
+ await runGit(["add", "-u"], workspace, { signal, timeout: 15_000 });
165
+ }
166
+ catch (e) {
167
+ return error(`git add -u failed: ${e instanceof Error ? e.message : "unknown error"}`);
168
+ }
169
+ }
170
+ // Check there is something staged
171
+ const diffCheck = await execSafe("git", ["diff", "--cached", "--quiet"], {
172
+ cwd: workspace,
173
+ signal,
174
+ });
175
+ if (diffCheck.exitCode === 0) {
176
+ // exit 0 = nothing staged
177
+ const status = await execSafe("git", ["status", "--short"], {
178
+ cwd: workspace,
179
+ signal,
180
+ });
181
+ return error(`Nothing staged to commit. ${status.stdout.trim()
182
+ ? `Unstaged changes exist:\n${status.stdout.trim()}\nUse gitAdd or pass files to this tool.`
183
+ : "Working tree is clean."}`);
184
+ }
185
+ // List staged files before committing (for return value)
186
+ const stagedResult = await execSafe("git", ["diff", "--name-only", "--cached"], { cwd: workspace, signal });
187
+ const stagedFiles = stagedResult.stdout
188
+ .split("\n")
189
+ .map((f) => f.trim())
190
+ .filter(Boolean);
191
+ // Commit
192
+ try {
193
+ await runGit(["commit", "-m", message], workspace, {
194
+ signal,
195
+ timeout: 30_000,
196
+ });
197
+ }
198
+ catch (e) {
199
+ return error(`git commit failed: ${e instanceof Error ? e.message : "unknown error"}`);
200
+ }
201
+ // Get commit hash
202
+ const hashResult = await execSafe("git", ["rev-parse", "HEAD"], {
203
+ cwd: workspace,
204
+ signal,
205
+ });
206
+ const hash = hashResult.stdout.trim().slice(0, 12);
207
+ const branch = await currentBranch(workspace, signal);
208
+ return success({
209
+ hash,
210
+ branch,
211
+ message,
212
+ files: stagedFiles,
213
+ count: stagedFiles.length,
214
+ });
215
+ },
216
+ };
217
+ }
218
+ export function createGitCheckoutTool(workspace) {
219
+ return {
220
+ schema: {
221
+ name: "gitCheckout",
222
+ description: "Switch to an existing branch, or create and switch to a new branch. " +
223
+ "Use create: true to create a new branch from HEAD (or a specified base). " +
224
+ "Check the current branch and available branches with getGitStatus first.",
225
+ annotations: { destructiveHint: true },
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ branch: {
230
+ type: "string",
231
+ description: "Branch name to switch to or create",
232
+ },
233
+ create: {
234
+ type: "boolean",
235
+ description: "Create the branch if it does not exist. Default: false.",
236
+ },
237
+ base: {
238
+ type: "string",
239
+ description: "Base branch or commit to create from (only used when create: true). Defaults to HEAD.",
240
+ },
241
+ },
242
+ required: ["branch"],
243
+ additionalProperties: false,
244
+ },
245
+ },
246
+ handler: async (args, signal) => {
247
+ if (!(await checkGitRepo(workspace, signal))) {
248
+ return error("Not a git repository");
249
+ }
250
+ const branch = requireString(args, "branch");
251
+ const create = optionalBool(args, "create") ?? false;
252
+ const base = optionalString(args, "base");
253
+ // Validate ref names to prevent git flag injection (e.g. --orphan, -b)
254
+ if (!VALID_REF_RE.test(branch)) {
255
+ return error(`Invalid branch name: "${branch}"`);
256
+ }
257
+ if (base !== undefined && !VALID_REF_RE.test(base)) {
258
+ return error(`Invalid base ref: "${base}"`);
259
+ }
260
+ const prevBranch = await currentBranch(workspace, signal);
261
+ let checkoutArgs;
262
+ if (create) {
263
+ checkoutArgs = base
264
+ ? ["checkout", "-b", branch, base]
265
+ : ["checkout", "-b", branch];
266
+ }
267
+ else {
268
+ checkoutArgs = ["checkout", branch];
269
+ }
270
+ try {
271
+ await runGit(checkoutArgs, workspace, { signal, timeout: 15_000 });
272
+ }
273
+ catch (e) {
274
+ const msg = e instanceof Error ? e.message : "unknown error";
275
+ // Surface helpful hints for common errors
276
+ if (msg.includes("already exists")) {
277
+ return error(`Branch '${branch}' already exists. Use create: false to switch to it, or choose a different name.`);
278
+ }
279
+ if (msg.includes("did not match") || msg.includes("pathspec")) {
280
+ return error(`Branch '${branch}' not found locally. If it exists on remote, run gitFetch first to update remote-tracking branches, then retry.`);
281
+ }
282
+ if (msg.includes("local changes")) {
283
+ return error(`Cannot switch branch: you have uncommitted changes. Use gitStash to save them, then switch branches and use gitStashPop to restore.\n${msg}`);
284
+ }
285
+ return error(`git checkout failed: ${msg}`);
286
+ }
287
+ const newBranch = await currentBranch(workspace, signal);
288
+ return success({
289
+ branch: newBranch,
290
+ previousBranch: prevBranch,
291
+ created: create,
292
+ });
293
+ },
294
+ };
295
+ }
296
+ export function createGitBlameTool(workspace) {
297
+ return {
298
+ schema: {
299
+ name: "gitBlame",
300
+ description: "Show who last modified each line of a file and in which commit. " +
301
+ "Use to understand why code was written a certain way, find the PR/commit that introduced a bug, " +
302
+ "or identify the author to ask for context. " +
303
+ "Optionally limit to a line range to avoid large outputs.",
304
+ annotations: { readOnlyHint: true },
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ filePath: {
309
+ type: "string",
310
+ description: "Absolute or workspace-relative path to the file",
311
+ },
312
+ startLine: {
313
+ type: "number",
314
+ description: "First line number to blame (1-based, inclusive). Omit for start of file.",
315
+ },
316
+ endLine: {
317
+ type: "number",
318
+ description: "Last line number to blame (1-based, inclusive). Omit for end of file.",
319
+ },
320
+ },
321
+ required: ["filePath"],
322
+ additionalProperties: false,
323
+ },
324
+ },
325
+ handler: async (args, signal) => {
326
+ if (!(await checkGitRepo(workspace, signal))) {
327
+ return error("Not a git repository");
328
+ }
329
+ const rawPath = requireString(args, "filePath");
330
+ const filePath = resolveFilePath(rawPath, workspace);
331
+ const startLine = typeof args.startLine === "number"
332
+ ? Math.max(1, Math.floor(args.startLine))
333
+ : undefined;
334
+ const endLine = typeof args.endLine === "number"
335
+ ? Math.max(1, Math.floor(args.endLine))
336
+ : undefined;
337
+ const blameArgs = ["blame", "--porcelain"];
338
+ if (startLine !== undefined && endLine !== undefined) {
339
+ blameArgs.push(`-L${startLine},${endLine}`);
340
+ }
341
+ else if (startLine !== undefined) {
342
+ blameArgs.push(`-L${startLine},+50`); // default 50 lines if only start given
343
+ }
344
+ blameArgs.push("--", filePath);
345
+ let blameOutput;
346
+ try {
347
+ ({ stdout: blameOutput } = await runGit(blameArgs, workspace, {
348
+ signal,
349
+ timeout: 15_000,
350
+ maxBuffer: 512 * 1024,
351
+ }));
352
+ }
353
+ catch (e) {
354
+ const msg = e instanceof Error ? e.message : "unknown error";
355
+ if (msg.includes("no such path")) {
356
+ return error(`File not tracked by git: ${filePath}`);
357
+ }
358
+ return error(`git blame failed: ${msg}`);
359
+ }
360
+ // Parse porcelain format
361
+ const lines = blameOutput.split("\n");
362
+ const commits = new Map();
363
+ const blameLines = [];
364
+ let currentHash = "";
365
+ let lineNum = 0;
366
+ for (let i = 0; i < lines.length; i++) {
367
+ const l = lines[i];
368
+ if (!l)
369
+ continue;
370
+ const headerMatch = l.match(/^([0-9a-f]{40})\s+\d+\s+(\d+)/);
371
+ if (headerMatch) {
372
+ currentHash = headerMatch[1];
373
+ lineNum = Number.parseInt(headerMatch[2], 10);
374
+ continue;
375
+ }
376
+ if (l.startsWith("author ") && !l.startsWith("author-")) {
377
+ const existing = commits.get(currentHash);
378
+ if (!existing) {
379
+ commits.set(currentHash, {
380
+ author: l.slice(7),
381
+ authorEmail: "",
382
+ summary: "",
383
+ timestamp: 0,
384
+ });
385
+ }
386
+ else {
387
+ existing.author = l.slice(7);
388
+ }
389
+ }
390
+ else if (l.startsWith("author-mail ")) {
391
+ const entry = commits.get(currentHash);
392
+ if (entry)
393
+ entry.authorEmail = l.slice(12).replace(/[<>]/g, "");
394
+ }
395
+ else if (l.startsWith("author-time ")) {
396
+ const entry = commits.get(currentHash);
397
+ if (entry)
398
+ entry.timestamp = Number.parseInt(l.slice(12), 10);
399
+ }
400
+ else if (l.startsWith("summary ")) {
401
+ const entry = commits.get(currentHash);
402
+ if (entry)
403
+ entry.summary = l.slice(8);
404
+ }
405
+ else if (l.startsWith("\t")) {
406
+ const info = commits.get(currentHash);
407
+ if (info && lineNum > 0) {
408
+ blameLines.push({
409
+ line: lineNum,
410
+ hash: currentHash.slice(0, 12),
411
+ author: info.author,
412
+ summary: info.summary,
413
+ code: l.slice(1),
414
+ });
415
+ lineNum = 0;
416
+ }
417
+ }
418
+ }
419
+ return success({ lines: blameLines, count: blameLines.length });
420
+ },
421
+ };
422
+ }
423
+ export function createGitFetchTool(workspace) {
424
+ return {
425
+ schema: {
426
+ name: "gitFetch",
427
+ description: "Fetch updates from a remote without merging — updates remote-tracking branches (e.g. origin/main) " +
428
+ "so gitListBranches shows current remote state and gitCheckout can find remote branches. " +
429
+ "Required before checking out a branch that exists on remote but not locally. " +
430
+ "Use gitPull to fetch and merge in one step.",
431
+ annotations: { readOnlyHint: true },
432
+ inputSchema: {
433
+ type: "object",
434
+ properties: {
435
+ remote: {
436
+ type: "string",
437
+ description: "Remote to fetch from (default: origin)",
438
+ },
439
+ all: {
440
+ type: "boolean",
441
+ description: "Fetch from all configured remotes. Default: false.",
442
+ },
443
+ },
444
+ additionalProperties: false,
445
+ },
446
+ },
447
+ handler: async (args, signal) => {
448
+ if (!(await checkGitRepo(workspace, signal))) {
449
+ return error("Not a git repository");
450
+ }
451
+ const all = optionalBool(args, "all") ?? false;
452
+ const remote = optionalString(args, "remote", 256) ?? "origin";
453
+ if (!all && !VALID_REF_RE.test(remote)) {
454
+ return error(`Invalid remote name: "${remote}"`);
455
+ }
456
+ const fetchArgs = all ? ["fetch", "--all"] : ["fetch", remote];
457
+ let fetchStdout;
458
+ let fetchStderr;
459
+ try {
460
+ ({ stdout: fetchStdout, stderr: fetchStderr } = await runGit(fetchArgs, workspace, { signal, timeout: 60_000 }));
461
+ }
462
+ catch (e) {
463
+ const msg = e instanceof Error ? e.message : "unknown error";
464
+ if (msg.includes("Authentication") ||
465
+ msg.includes("credential") ||
466
+ msg.includes("Permission denied") ||
467
+ msg.includes("could not read Username")) {
468
+ return error(`Authentication failed. Check your git credentials.\n${msg}`);
469
+ }
470
+ if (msg.includes("does not appear") || msg.includes("not found")) {
471
+ return error(`Remote '${remote}' not found. Check configured remotes.`);
472
+ }
473
+ return error(`git fetch failed: ${msg}`);
474
+ }
475
+ // git fetch writes to stderr even on success; empty = nothing new
476
+ const output = fetchStderr.trim() || fetchStdout.trim();
477
+ return success({ fetched: true, nothingNew: !output, output });
478
+ },
479
+ };
480
+ }
481
+ export function createGitListBranchesTool(workspace) {
482
+ return {
483
+ schema: {
484
+ name: "gitListBranches",
485
+ description: "List git branches in the workspace. " +
486
+ "Returns local branches with the current branch marked. " +
487
+ "Pass includeRemote: true to also list remote-tracking branches. " +
488
+ "Use before gitCheckout to see available branches.",
489
+ annotations: { readOnlyHint: true },
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ includeRemote: {
494
+ type: "boolean",
495
+ description: "Include remote-tracking branches (e.g. origin/main). Default: false.",
496
+ },
497
+ },
498
+ additionalProperties: false,
499
+ },
500
+ },
501
+ handler: async (args, signal) => {
502
+ if (!(await checkGitRepo(workspace, signal))) {
503
+ return error("Not a git repository");
504
+ }
505
+ const includeRemote = optionalBool(args, "includeRemote") ?? false;
506
+ let branchOutput;
507
+ try {
508
+ ({ stdout: branchOutput } = await runGit(["branch"], workspace, {
509
+ signal,
510
+ }));
511
+ }
512
+ catch (e) {
513
+ return error(`git branch failed: ${e instanceof Error ? e.message : "unknown error"}`);
514
+ }
515
+ const local = branchOutput
516
+ .split("\n")
517
+ .map((l) => l.trimEnd())
518
+ .filter(Boolean)
519
+ .map((l) => ({
520
+ name: l.startsWith("* ") ? l.slice(2) : l.trimStart(),
521
+ current: l.startsWith("* "),
522
+ }));
523
+ const current = local.find((b) => b.current)?.name ?? "";
524
+ const result = { local, current };
525
+ if (includeRemote) {
526
+ const remoteResult = await execSafe("git", ["branch", "-r"], {
527
+ cwd: workspace,
528
+ signal,
529
+ });
530
+ if (remoteResult.exitCode === 0) {
531
+ result.remote = remoteResult.stdout
532
+ .split("\n")
533
+ .map((l) => l.trim())
534
+ .filter(Boolean)
535
+ .filter((b) => !b.includes("HEAD ->"));
536
+ }
537
+ }
538
+ return success(result);
539
+ },
540
+ };
541
+ }
542
+ export function createGitPullTool(workspace) {
543
+ return {
544
+ schema: {
545
+ name: "gitPull",
546
+ description: "Pull changes from a remote repository into the current branch. " +
547
+ "Defaults to fetching from origin and merging (or rebasing if configured). " +
548
+ "Use rebase: true for a cleaner linear history. " +
549
+ "Returns output summary and whether the branch was already up to date.",
550
+ annotations: { destructiveHint: true },
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ remote: {
555
+ type: "string",
556
+ description: "Remote name (default: origin)",
557
+ },
558
+ branch: {
559
+ type: "string",
560
+ description: "Remote branch to pull from (default: tracking branch for current branch)",
561
+ },
562
+ rebase: {
563
+ type: "boolean",
564
+ description: "Rebase local commits on top of remote changes instead of merging. Default: false.",
565
+ },
566
+ },
567
+ additionalProperties: false,
568
+ },
569
+ },
570
+ handler: async (args, signal) => {
571
+ if (!(await checkGitRepo(workspace, signal))) {
572
+ return error("Not a git repository");
573
+ }
574
+ const remote = optionalString(args, "remote", 256) ?? "origin";
575
+ const branch = optionalString(args, "branch", 256);
576
+ const rebase = optionalBool(args, "rebase") ?? false;
577
+ if (!VALID_REF_RE.test(remote)) {
578
+ return error(`Invalid remote name: "${remote}"`);
579
+ }
580
+ if (branch !== undefined && !VALID_REF_RE.test(branch)) {
581
+ return error(`Invalid branch name: "${branch}"`);
582
+ }
583
+ const pullArgs = ["pull"];
584
+ if (rebase)
585
+ pullArgs.push("--rebase");
586
+ pullArgs.push(remote);
587
+ if (branch)
588
+ pullArgs.push(branch);
589
+ let pullOutput;
590
+ try {
591
+ ({ stdout: pullOutput } = await runGit(pullArgs, workspace, {
592
+ signal,
593
+ timeout: 60_000,
594
+ }));
595
+ }
596
+ catch (e) {
597
+ const msg = e instanceof Error ? e.message : "unknown error";
598
+ if (msg.includes("CONFLICT")) {
599
+ return error(`Merge conflict during pull. Resolve conflicts manually, then use gitAdd + gitCommit.\n${msg}`);
600
+ }
601
+ if (msg.includes("no tracking information") ||
602
+ msg.includes("has no upstream") ||
603
+ msg.includes("no upstream")) {
604
+ return error("No upstream branch configured for the current branch. Specify remote and branch explicitly.");
605
+ }
606
+ if (msg.includes("Authentication") ||
607
+ msg.includes("credential") ||
608
+ msg.includes("Permission denied") ||
609
+ msg.includes("could not read Username")) {
610
+ return error(`Authentication failed. Check your git credentials.\n${msg}`);
611
+ }
612
+ return error(`git pull failed: ${msg}`);
613
+ }
614
+ const alreadyUpToDate = pullOutput.includes("Already up to date") ||
615
+ pullOutput.includes("Already up-to-date");
616
+ return success({ alreadyUpToDate, output: pullOutput });
617
+ },
618
+ };
619
+ }
620
+ export function createGitPushTool(workspace) {
621
+ return {
622
+ schema: {
623
+ name: "gitPush",
624
+ description: "Push the current branch to a remote repository. " +
625
+ "Use setUpstream: true on the first push of a new branch to set its tracking remote. " +
626
+ "Force push is blocked on main/master to prevent accidental history rewrites. " +
627
+ "Uses --force-with-lease (not --force) for safe force pushes.",
628
+ annotations: { destructiveHint: true },
629
+ inputSchema: {
630
+ type: "object",
631
+ properties: {
632
+ remote: {
633
+ type: "string",
634
+ description: "Remote name (default: origin)",
635
+ },
636
+ branch: {
637
+ type: "string",
638
+ description: "Branch to push (default: current branch)",
639
+ },
640
+ setUpstream: {
641
+ type: "boolean",
642
+ description: "Set the upstream tracking branch (-u). Use on first push of a new branch. Default: false.",
643
+ },
644
+ force: {
645
+ type: "boolean",
646
+ description: "Force push with --force-with-lease. Blocked on main/master. Default: false.",
647
+ },
648
+ },
649
+ additionalProperties: false,
650
+ },
651
+ },
652
+ handler: async (args, signal) => {
653
+ if (!(await checkGitRepo(workspace, signal))) {
654
+ return error("Not a git repository");
655
+ }
656
+ const remote = optionalString(args, "remote", 256) ?? "origin";
657
+ const branchArg = optionalString(args, "branch", 256);
658
+ const setUpstream = optionalBool(args, "setUpstream") ?? false;
659
+ const force = optionalBool(args, "force") ?? false;
660
+ if (!VALID_REF_RE.test(remote)) {
661
+ return error(`Invalid remote name: "${remote}"`);
662
+ }
663
+ if (branchArg !== undefined && !VALID_REF_RE.test(branchArg)) {
664
+ return error(`Invalid branch name: "${branchArg}"`);
665
+ }
666
+ const branch = branchArg ?? (await currentBranch(workspace, signal));
667
+ if (force && (branch === "main" || branch === "master")) {
668
+ return error(`Force push to '${branch}' is blocked. This would rewrite shared history on the main branch.`);
669
+ }
670
+ const pushArgs = ["push"];
671
+ if (force)
672
+ pushArgs.push("--force-with-lease");
673
+ if (setUpstream)
674
+ pushArgs.push("-u");
675
+ pushArgs.push(remote, branch);
676
+ let pushStdout;
677
+ let pushStderr;
678
+ try {
679
+ ({ stdout: pushStdout, stderr: pushStderr } = await runGit(pushArgs, workspace, { signal, timeout: 60_000 }));
680
+ }
681
+ catch (e) {
682
+ const msg = e instanceof Error ? e.message : "unknown error";
683
+ if (msg.includes("rejected") && msg.includes("non-fast-forward")) {
684
+ return error("Push rejected: remote has commits not present locally. Run gitPull to sync, then push again.");
685
+ }
686
+ if (msg.includes("rejected") &&
687
+ (msg.includes("stale") || msg.includes("force-with-lease"))) {
688
+ return error("Force push rejected: remote branch was updated since your last fetch. Run gitPull to sync first.");
689
+ }
690
+ if (msg.includes("has no upstream") ||
691
+ msg.includes("no upstream branch")) {
692
+ return error(`Branch '${branch}' has no upstream. Use setUpstream: true to set the tracking branch on first push.`);
693
+ }
694
+ if (msg.includes("Authentication") ||
695
+ msg.includes("credential") ||
696
+ msg.includes("Permission denied") ||
697
+ msg.includes("Repository not found") ||
698
+ msg.includes("could not read Username")) {
699
+ return error(`Authentication failed. Check your git credentials.\n${msg}`);
700
+ }
701
+ return error(`git push failed: ${msg}`);
702
+ }
703
+ const hashResult = await execSafe("git", ["rev-parse", "HEAD"], {
704
+ cwd: workspace,
705
+ signal,
706
+ });
707
+ const hash = hashResult.stdout.trim().slice(0, 12);
708
+ return success({
709
+ remote,
710
+ branch,
711
+ hash,
712
+ setUpstream,
713
+ output: pushStderr.trim() || pushStdout.trim(),
714
+ });
715
+ },
716
+ };
717
+ }
718
+ export function createGitStashTool(workspace) {
719
+ return {
720
+ schema: {
721
+ name: "gitStash",
722
+ description: "Stash current changes to get a clean working tree — required before switching branches when you have uncommitted changes. " +
723
+ "Use gitStashPop to restore them after switching back. " +
724
+ "Pass includeUntracked: true to also stash new files not yet tracked by git.",
725
+ annotations: { destructiveHint: true },
726
+ inputSchema: {
727
+ type: "object",
728
+ properties: {
729
+ message: {
730
+ type: "string",
731
+ description: "Optional description for the stash entry",
732
+ },
733
+ includeUntracked: {
734
+ type: "boolean",
735
+ description: "Also stash untracked (new) files. Default: false.",
736
+ },
737
+ },
738
+ additionalProperties: false,
739
+ },
740
+ },
741
+ handler: async (args, signal) => {
742
+ if (!(await checkGitRepo(workspace, signal))) {
743
+ return error("Not a git repository");
744
+ }
745
+ const message = optionalString(args, "message", 256);
746
+ const includeUntracked = optionalBool(args, "includeUntracked") ?? false;
747
+ const stashArgs = ["stash", "push"];
748
+ if (includeUntracked)
749
+ stashArgs.push("-u");
750
+ if (message)
751
+ stashArgs.push("-m", message);
752
+ let stashStdout;
753
+ let stashStderr;
754
+ try {
755
+ ({ stdout: stashStdout, stderr: stashStderr } = await runGit(stashArgs, workspace, { signal, timeout: 15_000 }));
756
+ }
757
+ catch (e) {
758
+ return error(`git stash failed: ${e instanceof Error ? e.message : "unknown error"}`);
759
+ }
760
+ const output = stashStdout.trim() || stashStderr.trim();
761
+ if (output.includes("No local changes to save")) {
762
+ return success({ stashed: false, reason: "No local changes to save" });
763
+ }
764
+ const listResult = await execSafe("git", ["stash", "list", "--max-count=1"], { cwd: workspace, signal });
765
+ const stashRef = listResult.stdout.trim().split(":")[0] ?? "stash@{0}";
766
+ return success({ stashed: true, stashRef, output });
767
+ },
768
+ };
769
+ }
770
+ export function createGitStashPopTool(workspace) {
771
+ return {
772
+ schema: {
773
+ name: "gitStashPop",
774
+ description: "Restore stashed changes back to the working tree and remove the stash entry. " +
775
+ "Pops the most recent stash by default, or a specific entry by index (from gitStashList). " +
776
+ "Will fail with a conflict error if the stashed changes clash with the current state.",
777
+ annotations: { destructiveHint: true },
778
+ inputSchema: {
779
+ type: "object",
780
+ properties: {
781
+ index: {
782
+ type: "integer",
783
+ description: "Stash entry index to pop (0 = most recent). Default: 0.",
784
+ },
785
+ },
786
+ additionalProperties: false,
787
+ },
788
+ },
789
+ handler: async (args, signal) => {
790
+ if (!(await checkGitRepo(workspace, signal))) {
791
+ return error("Not a git repository");
792
+ }
793
+ const index = typeof args.index === "number"
794
+ ? Math.max(0, Math.floor(args.index))
795
+ : 0;
796
+ const stashRef = `stash@{${index}}`;
797
+ let popOutput;
798
+ try {
799
+ ({ stdout: popOutput } = await runGit(["stash", "pop", stashRef], workspace, { signal, timeout: 15_000 }));
800
+ }
801
+ catch (e) {
802
+ const msg = e instanceof Error ? e.message : "unknown error";
803
+ if (msg.includes("CONFLICT")) {
804
+ return error(`Merge conflict while applying stash. Resolve conflicts, then use gitAdd to mark them resolved.\n${msg}`);
805
+ }
806
+ if (msg.includes("No stash entries") ||
807
+ msg.includes("is not a valid reference")) {
808
+ return error(`No stash entry at index ${index}. Use gitStashList to see available entries.`);
809
+ }
810
+ return error(`git stash pop failed: ${msg}`);
811
+ }
812
+ return success({ restored: true, stashRef, output: popOutput.trim() });
813
+ },
814
+ };
815
+ }
816
+ export function createGitStashListTool(workspace) {
817
+ return {
818
+ schema: {
819
+ name: "gitStashList",
820
+ description: "List all stash entries in the repository. " +
821
+ "Returns each entry's index, branch it was stashed from, message, and age. " +
822
+ "Use before gitStashPop to identify the right entry to restore.",
823
+ annotations: { readOnlyHint: true },
824
+ inputSchema: {
825
+ type: "object",
826
+ properties: {},
827
+ additionalProperties: false,
828
+ },
829
+ },
830
+ handler: async (_args, signal) => {
831
+ if (!(await checkGitRepo(workspace, signal))) {
832
+ return error("Not a git repository");
833
+ }
834
+ let listOutput;
835
+ try {
836
+ ({ stdout: listOutput } = await runGit(["stash", "list", "--format=%gd|%gs|%cr"], workspace, { signal }));
837
+ }
838
+ catch (e) {
839
+ return error(`git stash list failed: ${e instanceof Error ? e.message : "unknown error"}`);
840
+ }
841
+ const entries = listOutput
842
+ .split("\n")
843
+ .map((l) => l.trim())
844
+ .filter(Boolean)
845
+ .map((l) => {
846
+ const [ref, subject, age] = l.split("|");
847
+ const index = Number.parseInt(ref?.match(/\{(\d+)\}/)?.[1] ?? "0", 10);
848
+ return {
849
+ index,
850
+ ref: ref ?? "",
851
+ subject: subject ?? "",
852
+ age: age ?? "",
853
+ };
854
+ });
855
+ return success({ entries, count: entries.length });
856
+ },
857
+ };
858
+ }
859
+ //# sourceMappingURL=gitWrite.js.map