@atercates/claude-deck 0.2.1

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 (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/api/claude/hidden/route.ts +66 -0
  4. package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
  5. package/app/api/claude/projects/route.ts +44 -0
  6. package/app/api/code-search/available/route.ts +12 -0
  7. package/app/api/code-search/route.ts +47 -0
  8. package/app/api/dev-servers/[id]/logs/route.ts +23 -0
  9. package/app/api/dev-servers/[id]/restart/route.ts +20 -0
  10. package/app/api/dev-servers/[id]/route.ts +51 -0
  11. package/app/api/dev-servers/[id]/stop/route.ts +20 -0
  12. package/app/api/dev-servers/detect/route.ts +39 -0
  13. package/app/api/dev-servers/route.ts +48 -0
  14. package/app/api/exec/route.ts +60 -0
  15. package/app/api/files/content/route.ts +76 -0
  16. package/app/api/files/route.ts +37 -0
  17. package/app/api/files/upload-temp/route.ts +41 -0
  18. package/app/api/git/check/route.ts +54 -0
  19. package/app/api/git/clone/route.ts +99 -0
  20. package/app/api/git/commit/route.ts +75 -0
  21. package/app/api/git/discard/route.ts +38 -0
  22. package/app/api/git/file-content/route.ts +64 -0
  23. package/app/api/git/history/[hash]/diff/route.ts +38 -0
  24. package/app/api/git/history/[hash]/route.ts +34 -0
  25. package/app/api/git/history/route.ts +27 -0
  26. package/app/api/git/multi-status/route.ts +46 -0
  27. package/app/api/git/pr/route.ts +164 -0
  28. package/app/api/git/push/route.ts +64 -0
  29. package/app/api/git/stage/route.ts +40 -0
  30. package/app/api/git/status/route.ts +51 -0
  31. package/app/api/git/unstage/route.ts +46 -0
  32. package/app/api/groups/[...path]/route.ts +136 -0
  33. package/app/api/groups/route.ts +93 -0
  34. package/app/api/orchestrate/spawn/route.ts +45 -0
  35. package/app/api/orchestrate/workers/[id]/route.ts +89 -0
  36. package/app/api/orchestrate/workers/route.ts +31 -0
  37. package/app/api/projects/[id]/detect/route.ts +27 -0
  38. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
  39. package/app/api/projects/[id]/dev-servers/route.ts +51 -0
  40. package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
  41. package/app/api/projects/[id]/repositories/route.ts +74 -0
  42. package/app/api/projects/[id]/route.ts +108 -0
  43. package/app/api/projects/detect/route.ts +33 -0
  44. package/app/api/projects/route.ts +59 -0
  45. package/app/api/sessions/[id]/claude-session/route.ts +42 -0
  46. package/app/api/sessions/[id]/fork/route.ts +74 -0
  47. package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
  48. package/app/api/sessions/[id]/messages/route.ts +60 -0
  49. package/app/api/sessions/[id]/pr/route.ts +188 -0
  50. package/app/api/sessions/[id]/preview/route.ts +42 -0
  51. package/app/api/sessions/[id]/route.ts +229 -0
  52. package/app/api/sessions/[id]/send-keys/route.ts +119 -0
  53. package/app/api/sessions/[id]/summarize/route.ts +331 -0
  54. package/app/api/sessions/init-script/route.ts +84 -0
  55. package/app/api/sessions/route.ts +209 -0
  56. package/app/api/sessions/status/route.ts +237 -0
  57. package/app/api/system/route.ts +9 -0
  58. package/app/api/tmux/kill-all/route.ts +57 -0
  59. package/app/api/tmux/rename/route.ts +30 -0
  60. package/app/globals.css +174 -0
  61. package/app/icon.svg +11 -0
  62. package/app/layout.tsx +122 -0
  63. package/app/page.tsx +629 -0
  64. package/components/ChatMessage.tsx +65 -0
  65. package/components/ChatView.tsx +276 -0
  66. package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
  67. package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
  68. package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
  69. package/components/ClaudeProjects/index.ts +1 -0
  70. package/components/CodeSearch/CodeSearchResults.tsx +177 -0
  71. package/components/ConductorPanel.tsx +256 -0
  72. package/components/DevServers/DevServerCard.tsx +311 -0
  73. package/components/DevServers/DevServersSection.tsx +91 -0
  74. package/components/DevServers/ServerLogsModal.tsx +151 -0
  75. package/components/DevServers/StartServerDialog.tsx +359 -0
  76. package/components/DevServers/index.ts +4 -0
  77. package/components/DiffViewer/DiffModal.tsx +151 -0
  78. package/components/DiffViewer/UnifiedDiff.tsx +185 -0
  79. package/components/DiffViewer/index.tsx +2 -0
  80. package/components/DirectoryPicker.tsx +355 -0
  81. package/components/FileExplorer/FileEditor.tsx +276 -0
  82. package/components/FileExplorer/FileTabs.tsx +118 -0
  83. package/components/FileExplorer/FileTree.tsx +214 -0
  84. package/components/FileExplorer/HtmlRenderer.tsx +16 -0
  85. package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
  86. package/components/FileExplorer/index.tsx +520 -0
  87. package/components/FilePicker.tsx +339 -0
  88. package/components/FolderPicker.tsx +201 -0
  89. package/components/GitDrawer/FileEditDialog.tsx +400 -0
  90. package/components/GitDrawer/index.tsx +464 -0
  91. package/components/GitPanel/CommitForm.tsx +205 -0
  92. package/components/GitPanel/CommitHistory.tsx +174 -0
  93. package/components/GitPanel/CommitItem.tsx +196 -0
  94. package/components/GitPanel/FileChanges.tsx +414 -0
  95. package/components/GitPanel/GitPanelTabs.tsx +39 -0
  96. package/components/GitPanel/index.tsx +817 -0
  97. package/components/MessageInput.tsx +82 -0
  98. package/components/NewClaudeSessionDialog.tsx +166 -0
  99. package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
  100. package/components/NewSessionDialog/AgentSelector.tsx +37 -0
  101. package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
  102. package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
  103. package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
  104. package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
  105. package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
  106. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
  107. package/components/NewSessionDialog/index.tsx +106 -0
  108. package/components/NotificationSettings.tsx +127 -0
  109. package/components/PRCreationModal.tsx +272 -0
  110. package/components/Pane/DesktopTabBar.tsx +353 -0
  111. package/components/Pane/MobileTabBar.tsx +210 -0
  112. package/components/Pane/OpenInVSCode.tsx +69 -0
  113. package/components/Pane/PaneSkeletons.tsx +57 -0
  114. package/components/Pane/index.tsx +558 -0
  115. package/components/PaneLayout.tsx +60 -0
  116. package/components/Projects/DevServersSection.tsx +140 -0
  117. package/components/Projects/DirectoryField.tsx +92 -0
  118. package/components/Projects/NewProjectDialog.tsx +188 -0
  119. package/components/Projects/NewProjectDialog.types.ts +46 -0
  120. package/components/Projects/ProjectCard.tsx +276 -0
  121. package/components/Projects/ProjectSettingsDialog.tsx +811 -0
  122. package/components/Projects/hooks/useNewProjectForm.ts +249 -0
  123. package/components/Projects/index.ts +3 -0
  124. package/components/Providers.tsx +49 -0
  125. package/components/QuickSwitcher.tsx +306 -0
  126. package/components/SessionList/KillAllConfirm.tsx +46 -0
  127. package/components/SessionList/SelectionToolbar.tsx +164 -0
  128. package/components/SessionList/SessionList.types.ts +37 -0
  129. package/components/SessionList/SessionListHeader.tsx +71 -0
  130. package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
  131. package/components/SessionList/index.tsx +189 -0
  132. package/components/ShellDrawer/index.tsx +106 -0
  133. package/components/SidebarFooter.tsx +55 -0
  134. package/components/Terminal/KeybarToggleButton.tsx +45 -0
  135. package/components/Terminal/ScrollToBottomButton.tsx +32 -0
  136. package/components/Terminal/SearchBar.tsx +71 -0
  137. package/components/Terminal/TerminalToolbar.tsx +551 -0
  138. package/components/Terminal/VirtualKeyboard.tsx +711 -0
  139. package/components/Terminal/constants.ts +20 -0
  140. package/components/Terminal/hooks/index.ts +5 -0
  141. package/components/Terminal/hooks/resize-handlers.ts +140 -0
  142. package/components/Terminal/hooks/terminal-init.ts +151 -0
  143. package/components/Terminal/hooks/touch-scroll.ts +155 -0
  144. package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
  145. package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
  146. package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
  147. package/components/Terminal/hooks/websocket-connection.ts +274 -0
  148. package/components/Terminal/index.tsx +320 -0
  149. package/components/ThemeToggle.tsx +168 -0
  150. package/components/TmuxSessions.tsx +132 -0
  151. package/components/ToolCallDisplay.tsx +71 -0
  152. package/components/WorkerCard.tsx +245 -0
  153. package/components/a/ABadge.tsx +115 -0
  154. package/components/a/AButton.tsx +163 -0
  155. package/components/a/ADialog.tsx +93 -0
  156. package/components/a/ADropdownMenu.tsx +279 -0
  157. package/components/a/AIconButton.tsx +190 -0
  158. package/components/a/ASheet.tsx +150 -0
  159. package/components/a/ATooltip.tsx +77 -0
  160. package/components/a/index.ts +64 -0
  161. package/components/mobile/SwipeSidebar.tsx +122 -0
  162. package/components/ui/badge.tsx +41 -0
  163. package/components/ui/button.tsx +60 -0
  164. package/components/ui/context-menu.tsx +197 -0
  165. package/components/ui/dialog.tsx +143 -0
  166. package/components/ui/dropdown-menu.tsx +257 -0
  167. package/components/ui/input.tsx +21 -0
  168. package/components/ui/scroll-area.tsx +52 -0
  169. package/components/ui/select.tsx +159 -0
  170. package/components/ui/skeleton.tsx +111 -0
  171. package/components/ui/switch.tsx +31 -0
  172. package/components/ui/textarea.tsx +21 -0
  173. package/components/ui/tooltip.tsx +32 -0
  174. package/components/views/DesktopView.tsx +244 -0
  175. package/components/views/MobileView.tsx +110 -0
  176. package/components/views/types.ts +75 -0
  177. package/contexts/PaneContext.tsx +336 -0
  178. package/data/claude/index.ts +9 -0
  179. package/data/claude/keys.ts +6 -0
  180. package/data/claude/queries.ts +120 -0
  181. package/data/claude/useClaudeUpdates.ts +37 -0
  182. package/data/code-search/index.ts +2 -0
  183. package/data/code-search/keys.ts +7 -0
  184. package/data/code-search/queries.ts +61 -0
  185. package/data/dev-servers/index.ts +8 -0
  186. package/data/dev-servers/keys.ts +4 -0
  187. package/data/dev-servers/queries.ts +104 -0
  188. package/data/files/index.ts +3 -0
  189. package/data/files/keys.ts +4 -0
  190. package/data/files/queries.ts +25 -0
  191. package/data/git/keys.ts +15 -0
  192. package/data/git/queries.ts +395 -0
  193. package/data/groups/index.ts +1 -0
  194. package/data/groups/mutations.ts +95 -0
  195. package/data/projects/index.ts +10 -0
  196. package/data/projects/keys.ts +4 -0
  197. package/data/projects/queries.ts +193 -0
  198. package/data/repositories/index.ts +7 -0
  199. package/data/repositories/keys.ts +5 -0
  200. package/data/repositories/queries.ts +122 -0
  201. package/data/sessions/index.ts +12 -0
  202. package/data/sessions/keys.ts +8 -0
  203. package/data/sessions/queries.ts +218 -0
  204. package/data/statuses/index.ts +1 -0
  205. package/data/statuses/queries.ts +69 -0
  206. package/hooks/useCopyToClipboard.ts +48 -0
  207. package/hooks/useDevServersManager.ts +73 -0
  208. package/hooks/useDirectoryBrowser.ts +90 -0
  209. package/hooks/useDrawerAnimation.ts +27 -0
  210. package/hooks/useFileDrop.ts +87 -0
  211. package/hooks/useFileEditor.ts +184 -0
  212. package/hooks/useGroups.ts +37 -0
  213. package/hooks/useHomePath.ts +34 -0
  214. package/hooks/useKeyRepeat.ts +55 -0
  215. package/hooks/useKeybarVisibility.ts +42 -0
  216. package/hooks/useNotifications.ts +257 -0
  217. package/hooks/useProjects.ts +53 -0
  218. package/hooks/useSessionStatuses.ts +30 -0
  219. package/hooks/useSessions.ts +86 -0
  220. package/hooks/useSpeechRecognition.ts +124 -0
  221. package/hooks/useViewport.ts +32 -0
  222. package/hooks/useViewportHeight.ts +50 -0
  223. package/lib/async-operations.ts +35 -0
  224. package/lib/banner.ts +81 -0
  225. package/lib/claude/jsonl-cache.ts +86 -0
  226. package/lib/claude/jsonl-reader.ts +271 -0
  227. package/lib/claude/process-manager.ts +278 -0
  228. package/lib/claude/stream-parser.ts +173 -0
  229. package/lib/claude/types.ts +154 -0
  230. package/lib/claude/watcher.ts +71 -0
  231. package/lib/client/session-registry.ts +111 -0
  232. package/lib/code-search.ts +121 -0
  233. package/lib/db/index.ts +48 -0
  234. package/lib/db/migrations.ts +45 -0
  235. package/lib/db/queries.ts +460 -0
  236. package/lib/db/schema.ts +114 -0
  237. package/lib/db/types.ts +92 -0
  238. package/lib/db.ts +2 -0
  239. package/lib/dev-servers.ts +509 -0
  240. package/lib/diff-parser.ts +221 -0
  241. package/lib/env-setup.ts +285 -0
  242. package/lib/file-upload.ts +34 -0
  243. package/lib/file-utils.ts +50 -0
  244. package/lib/files.ts +207 -0
  245. package/lib/git-history.ts +294 -0
  246. package/lib/git-status.ts +391 -0
  247. package/lib/git.ts +257 -0
  248. package/lib/mcp-config.ts +81 -0
  249. package/lib/multi-repo-git.ts +179 -0
  250. package/lib/notifications.ts +219 -0
  251. package/lib/orchestration.ts +448 -0
  252. package/lib/panes.ts +232 -0
  253. package/lib/ports.ts +97 -0
  254. package/lib/pr-generation.ts +307 -0
  255. package/lib/pr.ts +234 -0
  256. package/lib/projects.ts +578 -0
  257. package/lib/providers/registry.ts +70 -0
  258. package/lib/providers.ts +121 -0
  259. package/lib/query-client.ts +14 -0
  260. package/lib/rangeSelectionUtils.ts +65 -0
  261. package/lib/status-detector.ts +375 -0
  262. package/lib/terminal-themes.ts +265 -0
  263. package/lib/theme-config.ts +327 -0
  264. package/lib/utils.ts +6 -0
  265. package/lib/worktrees.ts +262 -0
  266. package/mcp/orchestration-server.ts +438 -0
  267. package/package.json +139 -0
  268. package/postcss.config.mjs +7 -0
  269. package/public/icon.svg +10 -0
  270. package/public/icons/icon-128x128.png +0 -0
  271. package/public/icons/icon-144x144.png +0 -0
  272. package/public/icons/icon-152x152.png +0 -0
  273. package/public/icons/icon-192x192.png +0 -0
  274. package/public/icons/icon-384x384.png +0 -0
  275. package/public/icons/icon-512x512.png +0 -0
  276. package/public/icons/icon-72x72.png +0 -0
  277. package/public/icons/icon-96x96.png +0 -0
  278. package/public/manifest.json +61 -0
  279. package/public/sw.js +64 -0
  280. package/scripts/agent-os +91 -0
  281. package/scripts/install.sh +48 -0
  282. package/scripts/lib/ai-clis.sh +132 -0
  283. package/scripts/lib/commands.sh +487 -0
  284. package/scripts/lib/common.sh +89 -0
  285. package/scripts/lib/prerequisites.sh +462 -0
  286. package/scripts/setup.sh +134 -0
  287. package/server.ts +155 -0
  288. package/stores/fileOpen.ts +26 -0
  289. package/stores/index.ts +1 -0
  290. package/stores/initialPrompt.ts +24 -0
  291. package/stores/sessionSelection.ts +48 -0
  292. package/styles/themes.css +603 -0
  293. package/tsconfig.json +33 -0
@@ -0,0 +1,294 @@
1
+ import { execSync } from "child_process";
2
+ import { expandPath } from "./git-status";
3
+
4
+ export interface CommitSummary {
5
+ hash: string;
6
+ shortHash: string;
7
+ subject: string;
8
+ body: string;
9
+ author: string;
10
+ authorEmail: string;
11
+ timestamp: number;
12
+ relativeTime: string;
13
+ filesChanged: number;
14
+ additions: number;
15
+ deletions: number;
16
+ }
17
+
18
+ export interface CommitFile {
19
+ path: string;
20
+ oldPath?: string;
21
+ status: "added" | "modified" | "deleted" | "renamed";
22
+ additions: number;
23
+ deletions: number;
24
+ }
25
+
26
+ export interface CommitDetail extends CommitSummary {
27
+ files: CommitFile[];
28
+ }
29
+
30
+ /**
31
+ * Get relative time string from timestamp
32
+ */
33
+ function getRelativeTime(timestamp: number): string {
34
+ const now = Date.now();
35
+ const diff = now - timestamp * 1000;
36
+ const seconds = Math.floor(diff / 1000);
37
+ const minutes = Math.floor(seconds / 60);
38
+ const hours = Math.floor(minutes / 60);
39
+ const days = Math.floor(hours / 24);
40
+ const weeks = Math.floor(days / 7);
41
+ const months = Math.floor(days / 30);
42
+
43
+ if (months > 0) return `${months}mo ago`;
44
+ if (weeks > 0) return `${weeks}w ago`;
45
+ if (days > 0) return `${days}d ago`;
46
+ if (hours > 0) return `${hours}h ago`;
47
+ if (minutes > 0) return `${minutes}m ago`;
48
+ return "just now";
49
+ }
50
+
51
+ /**
52
+ * Get commit history
53
+ */
54
+ export function getCommitHistory(
55
+ workingDir: string,
56
+ limit: number = 30
57
+ ): CommitSummary[] {
58
+ const cwd = expandPath(workingDir);
59
+
60
+ try {
61
+ // Format: hash|shortHash|subject|body|author|email|timestamp
62
+ // Using %x00 as separator to handle commit messages with |
63
+ const format = "%H%x00%h%x00%s%x00%b%x00%an%x00%ae%x00%at";
64
+ const output = execSync(
65
+ `git log --format="${format}" -n ${limit} --shortstat`,
66
+ { cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
67
+ );
68
+
69
+ const commits: CommitSummary[] = [];
70
+ const lines = output.split("\n");
71
+
72
+ let i = 0;
73
+ while (i < lines.length) {
74
+ const line = lines[i];
75
+ if (!line || !line.includes("\x00")) {
76
+ i++;
77
+ continue;
78
+ }
79
+
80
+ const parts = line.split("\x00");
81
+ if (parts.length < 7) {
82
+ i++;
83
+ continue;
84
+ }
85
+
86
+ const [
87
+ hash,
88
+ shortHash,
89
+ subject,
90
+ body,
91
+ author,
92
+ authorEmail,
93
+ timestampStr,
94
+ ] = parts;
95
+ const timestamp = parseInt(timestampStr, 10);
96
+
97
+ // Look for shortstat line (next non-empty line)
98
+ let filesChanged = 0;
99
+ let additions = 0;
100
+ let deletions = 0;
101
+
102
+ i++;
103
+ while (i < lines.length) {
104
+ const statLine = lines[i].trim();
105
+ if (!statLine) {
106
+ i++;
107
+ continue;
108
+ }
109
+ // Parse shortstat: "3 files changed, 10 insertions(+), 5 deletions(-)"
110
+ const filesMatch = statLine.match(/(\d+) files? changed/);
111
+ const addMatch = statLine.match(/(\d+) insertions?\(\+\)/);
112
+ const delMatch = statLine.match(/(\d+) deletions?\(-\)/);
113
+
114
+ if (filesMatch || addMatch || delMatch) {
115
+ filesChanged = filesMatch ? parseInt(filesMatch[1], 10) : 0;
116
+ additions = addMatch ? parseInt(addMatch[1], 10) : 0;
117
+ deletions = delMatch ? parseInt(delMatch[1], 10) : 0;
118
+ i++;
119
+ break;
120
+ }
121
+ // If line contains separator, it's a new commit
122
+ if (statLine.includes("\x00")) {
123
+ break;
124
+ }
125
+ i++;
126
+ }
127
+
128
+ commits.push({
129
+ hash,
130
+ shortHash,
131
+ subject,
132
+ body: body.trim(),
133
+ author,
134
+ authorEmail,
135
+ timestamp,
136
+ relativeTime: getRelativeTime(timestamp),
137
+ filesChanged,
138
+ additions,
139
+ deletions,
140
+ });
141
+ }
142
+
143
+ return commits;
144
+ } catch (error) {
145
+ console.error("Failed to get commit history:", error);
146
+ return [];
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get detailed commit info including files changed
152
+ */
153
+ export function getCommitDetail(
154
+ workingDir: string,
155
+ commitHash: string
156
+ ): CommitDetail | null {
157
+ const cwd = expandPath(workingDir);
158
+
159
+ try {
160
+ // Get commit info
161
+ const format = "%H%x00%h%x00%s%x00%b%x00%an%x00%ae%x00%at";
162
+ const infoOutput = execSync(
163
+ `git show --format="${format}" -s ${commitHash}`,
164
+ {
165
+ cwd,
166
+ encoding: "utf-8",
167
+ }
168
+ ).trim();
169
+
170
+ const parts = infoOutput.split("\x00");
171
+ if (parts.length < 7) return null;
172
+
173
+ const [hash, shortHash, subject, body, author, authorEmail, timestampStr] =
174
+ parts;
175
+ const timestamp = parseInt(timestampStr, 10);
176
+
177
+ // Get file stats using numstat
178
+ const statOutput = execSync(
179
+ `git show --numstat --format="" ${commitHash}`,
180
+ { cwd, encoding: "utf-8" }
181
+ );
182
+
183
+ // Get name-status for detecting renames
184
+ const nameStatusOutput = execSync(
185
+ `git show --name-status --format="" ${commitHash}`,
186
+ { cwd, encoding: "utf-8" }
187
+ );
188
+
189
+ const files: CommitFile[] = [];
190
+ const statLines = statOutput.trim().split("\n").filter(Boolean);
191
+ const nameStatusLines = nameStatusOutput.trim().split("\n").filter(Boolean);
192
+
193
+ // Build a map of path -> status from name-status
194
+ const statusMap = new Map<string, { status: string; oldPath?: string }>();
195
+ for (const line of nameStatusLines) {
196
+ const match = line.match(/^([AMDRC])\d*\t(.+?)(?:\t(.+))?$/);
197
+ if (match) {
198
+ const [, status, path1, path2] = match;
199
+ const finalPath = path2 || path1;
200
+ statusMap.set(finalPath, {
201
+ status,
202
+ oldPath: path2 ? path1 : undefined,
203
+ });
204
+ }
205
+ }
206
+
207
+ // Parse numstat for additions/deletions
208
+ for (const line of statLines) {
209
+ const match = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
210
+ if (match) {
211
+ const [, addStr, delStr, path] = match;
212
+ const additions = addStr === "-" ? 0 : parseInt(addStr, 10);
213
+ const deletions = delStr === "-" ? 0 : parseInt(delStr, 10);
214
+
215
+ const statusInfo = statusMap.get(path);
216
+ let status: CommitFile["status"] = "modified";
217
+ if (statusInfo) {
218
+ switch (statusInfo.status) {
219
+ case "A":
220
+ status = "added";
221
+ break;
222
+ case "D":
223
+ status = "deleted";
224
+ break;
225
+ case "R":
226
+ status = "renamed";
227
+ break;
228
+ default:
229
+ status = "modified";
230
+ }
231
+ }
232
+
233
+ files.push({
234
+ path,
235
+ oldPath: statusInfo?.oldPath,
236
+ status,
237
+ additions,
238
+ deletions,
239
+ });
240
+ }
241
+ }
242
+
243
+ // Get total stats
244
+ let totalFilesChanged = files.length;
245
+ let totalAdditions = 0;
246
+ let totalDeletions = 0;
247
+ for (const file of files) {
248
+ totalAdditions += file.additions;
249
+ totalDeletions += file.deletions;
250
+ }
251
+
252
+ return {
253
+ hash,
254
+ shortHash,
255
+ subject,
256
+ body: body.trim(),
257
+ author,
258
+ authorEmail,
259
+ timestamp,
260
+ relativeTime: getRelativeTime(timestamp),
261
+ filesChanged: totalFilesChanged,
262
+ additions: totalAdditions,
263
+ deletions: totalDeletions,
264
+ files,
265
+ };
266
+ } catch (error) {
267
+ console.error("Failed to get commit detail:", error);
268
+ return null;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Get diff for a specific file in a commit
274
+ */
275
+ export function getCommitFileDiff(
276
+ workingDir: string,
277
+ commitHash: string,
278
+ filePath: string
279
+ ): string {
280
+ const cwd = expandPath(workingDir);
281
+
282
+ try {
283
+ // Get diff for the specific file in this commit
284
+ // Use -m to handle merge commits (shows diff against first parent)
285
+ const diff = execSync(
286
+ `git show -m --first-parent ${commitHash} -- "${filePath}"`,
287
+ { cwd, encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 }
288
+ );
289
+ return diff;
290
+ } catch (error) {
291
+ console.error("Failed to get commit file diff:", error);
292
+ return "";
293
+ }
294
+ }
@@ -0,0 +1,391 @@
1
+ import { execSync } from "child_process";
2
+ import { unlinkSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ /**
7
+ * Expand ~ to home directory in paths
8
+ */
9
+ export function expandPath(path: string): string {
10
+ if (path.startsWith("~")) {
11
+ return path.replace(/^~/, homedir());
12
+ }
13
+ return path;
14
+ }
15
+
16
+ export type FileStatus =
17
+ | "modified"
18
+ | "added"
19
+ | "deleted"
20
+ | "renamed"
21
+ | "copied"
22
+ | "untracked"
23
+ | "unmerged";
24
+
25
+ export interface GitFile {
26
+ path: string;
27
+ status: FileStatus;
28
+ staged: boolean;
29
+ oldPath?: string; // For renamed files
30
+ }
31
+
32
+ export interface GitStatus {
33
+ branch: string;
34
+ ahead: number;
35
+ behind: number;
36
+ staged: GitFile[];
37
+ unstaged: GitFile[];
38
+ untracked: GitFile[];
39
+ }
40
+
41
+ /**
42
+ * Parse git status --porcelain=v2 output
43
+ */
44
+ export function getGitStatus(workingDir: string): GitStatus {
45
+ try {
46
+ // Get branch info
47
+ const branchOutput = execSync("git branch --show-current", {
48
+ cwd: workingDir,
49
+ encoding: "utf-8",
50
+ }).trim();
51
+
52
+ // Get ahead/behind counts
53
+ let ahead = 0;
54
+ let behind = 0;
55
+ try {
56
+ const trackingOutput = execSync(
57
+ "git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo '0 0'",
58
+ { cwd: workingDir, encoding: "utf-8" }
59
+ ).trim();
60
+ const [b, a] = trackingOutput.split(/\s+/).map(Number);
61
+ ahead = a || 0;
62
+ behind = b || 0;
63
+ } catch {
64
+ // No upstream configured
65
+ }
66
+
67
+ // Get status
68
+ const statusOutput = execSync("git status --porcelain=v1", {
69
+ cwd: workingDir,
70
+ encoding: "utf-8",
71
+ });
72
+
73
+ const staged: GitFile[] = [];
74
+ const unstaged: GitFile[] = [];
75
+ const untracked: GitFile[] = [];
76
+
77
+ for (const line of statusOutput.split("\n")) {
78
+ if (!line) continue;
79
+
80
+ const indexStatus = line[0];
81
+ const workTreeStatus = line[1];
82
+ const filePath = line.slice(3);
83
+
84
+ // Handle renames (format: "R old -> new")
85
+ let path = filePath;
86
+ let oldPath: string | undefined;
87
+ if (filePath.includes(" -> ")) {
88
+ const parts = filePath.split(" -> ");
89
+ oldPath = parts[0];
90
+ path = parts[1];
91
+ }
92
+
93
+ // Untracked files
94
+ if (indexStatus === "?" && workTreeStatus === "?") {
95
+ untracked.push({ path, status: "untracked", staged: false });
96
+ continue;
97
+ }
98
+
99
+ // Staged changes (index status)
100
+ if (indexStatus !== " " && indexStatus !== "?") {
101
+ staged.push({
102
+ path,
103
+ oldPath,
104
+ status: parseStatus(indexStatus),
105
+ staged: true,
106
+ });
107
+ }
108
+
109
+ // Unstaged changes (work tree status)
110
+ if (workTreeStatus !== " " && workTreeStatus !== "?") {
111
+ unstaged.push({
112
+ path,
113
+ oldPath,
114
+ status: parseStatus(workTreeStatus),
115
+ staged: false,
116
+ });
117
+ }
118
+ }
119
+
120
+ return {
121
+ branch: branchOutput || "HEAD",
122
+ ahead,
123
+ behind,
124
+ staged,
125
+ unstaged,
126
+ untracked,
127
+ };
128
+ } catch (error) {
129
+ throw new Error(
130
+ `Failed to get git status: ${error instanceof Error ? error.message : "Unknown error"}`
131
+ );
132
+ }
133
+ }
134
+
135
+ function parseStatus(char: string): FileStatus {
136
+ switch (char) {
137
+ case "M":
138
+ return "modified";
139
+ case "A":
140
+ return "added";
141
+ case "D":
142
+ return "deleted";
143
+ case "R":
144
+ return "renamed";
145
+ case "C":
146
+ return "copied";
147
+ case "U":
148
+ return "unmerged";
149
+ default:
150
+ return "modified";
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get diff for a specific file
156
+ */
157
+ export function getFileDiff(
158
+ workingDir: string,
159
+ filePath: string,
160
+ staged: boolean
161
+ ): string {
162
+ try {
163
+ const stagedFlag = staged ? "--staged" : "";
164
+ const output = execSync(
165
+ `git diff ${stagedFlag} -- "${filePath}" 2>/dev/null || true`,
166
+ {
167
+ cwd: workingDir,
168
+ encoding: "utf-8",
169
+ maxBuffer: 10 * 1024 * 1024, // 10MB
170
+ }
171
+ );
172
+ return output;
173
+ } catch {
174
+ return "";
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Get diff for untracked file (show full content)
180
+ */
181
+ export function getUntrackedFileDiff(
182
+ workingDir: string,
183
+ filePath: string
184
+ ): string {
185
+ try {
186
+ const output = execSync(
187
+ `git diff --no-index /dev/null "${filePath}" 2>/dev/null || true`,
188
+ {
189
+ cwd: workingDir,
190
+ encoding: "utf-8",
191
+ maxBuffer: 10 * 1024 * 1024,
192
+ }
193
+ );
194
+ return output;
195
+ } catch {
196
+ return "";
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Stage a file
202
+ */
203
+ export function stageFile(workingDir: string, filePath: string): void {
204
+ execSync(`git add -- "${filePath}"`, {
205
+ cwd: workingDir,
206
+ encoding: "utf-8",
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Stage all files
212
+ */
213
+ export function stageAll(workingDir: string): void {
214
+ execSync("git add -A", {
215
+ cwd: workingDir,
216
+ encoding: "utf-8",
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Unstage a file
222
+ */
223
+ export function unstageFile(workingDir: string, filePath: string): void {
224
+ execSync(`git reset HEAD -- "${filePath}"`, {
225
+ cwd: workingDir,
226
+ encoding: "utf-8",
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Unstage all files
232
+ */
233
+ export function unstageAll(workingDir: string): void {
234
+ execSync("git reset HEAD", {
235
+ cwd: workingDir,
236
+ encoding: "utf-8",
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Discard changes to a file (checkout for tracked, delete for untracked)
242
+ */
243
+ export function discardChanges(workingDir: string, filePath: string): void {
244
+ // Check if file is tracked by git
245
+ try {
246
+ execSync(`git ls-files --error-unmatch "${filePath}"`, {
247
+ cwd: workingDir,
248
+ encoding: "utf-8",
249
+ stdio: "pipe",
250
+ });
251
+ // File is tracked - use checkout
252
+ execSync(`git checkout -- "${filePath}"`, {
253
+ cwd: workingDir,
254
+ encoding: "utf-8",
255
+ });
256
+ } catch {
257
+ // File is untracked - delete it
258
+ unlinkSync(join(workingDir, filePath));
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Check if directory is a git repository
264
+ */
265
+ export function isGitRepo(workingDir: string): boolean {
266
+ try {
267
+ execSync("git rev-parse --git-dir", {
268
+ cwd: workingDir,
269
+ encoding: "utf-8",
270
+ stdio: ["pipe", "pipe", "pipe"],
271
+ });
272
+ return true;
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Get the root of the git repository
280
+ */
281
+ export function getGitRoot(workingDir: string): string {
282
+ try {
283
+ return execSync("git rev-parse --show-toplevel", {
284
+ cwd: workingDir,
285
+ encoding: "utf-8",
286
+ }).trim();
287
+ } catch {
288
+ return workingDir;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Check if on main/master branch
294
+ */
295
+ export function isMainBranch(workingDir: string): boolean {
296
+ try {
297
+ const branch = execSync("git branch --show-current", {
298
+ cwd: workingDir,
299
+ encoding: "utf-8",
300
+ }).trim();
301
+ return branch === "main" || branch === "master";
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Create a new branch and switch to it
309
+ */
310
+ export function createBranch(workingDir: string, branchName: string): void {
311
+ execSync(`git checkout -b "${branchName}"`, {
312
+ cwd: workingDir,
313
+ encoding: "utf-8",
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Commit staged changes
319
+ */
320
+ export function commit(workingDir: string, message: string): string {
321
+ const output = execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
322
+ cwd: workingDir,
323
+ encoding: "utf-8",
324
+ });
325
+ return output;
326
+ }
327
+
328
+ /**
329
+ * Push to remote
330
+ */
331
+ export function push(workingDir: string, setUpstream = false): string {
332
+ const branch = execSync("git branch --show-current", {
333
+ cwd: workingDir,
334
+ encoding: "utf-8",
335
+ }).trim();
336
+
337
+ const upstreamFlag = setUpstream ? `-u origin "${branch}"` : "";
338
+ const output = execSync(`git push ${upstreamFlag}`, {
339
+ cwd: workingDir,
340
+ encoding: "utf-8",
341
+ });
342
+ return output;
343
+ }
344
+
345
+ /**
346
+ * Check if branch has upstream
347
+ */
348
+ export function hasUpstream(workingDir: string): boolean {
349
+ try {
350
+ execSync("git rev-parse --abbrev-ref @{upstream}", {
351
+ cwd: workingDir,
352
+ encoding: "utf-8",
353
+ stdio: ["pipe", "pipe", "pipe"],
354
+ });
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Get remote URL
363
+ */
364
+ export function getRemoteUrl(workingDir: string): string | null {
365
+ try {
366
+ return execSync("git remote get-url origin", {
367
+ cwd: workingDir,
368
+ encoding: "utf-8",
369
+ }).trim();
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Get the default branch name (main or master)
377
+ */
378
+ export function getDefaultBranch(workingDir: string): string {
379
+ try {
380
+ // Try to get from remote
381
+ const output = execSync(
382
+ "git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo 'refs/heads/main'",
383
+ { cwd: workingDir, encoding: "utf-8" }
384
+ ).trim();
385
+ return output
386
+ .replace("refs/remotes/origin/", "")
387
+ .replace("refs/heads/", "");
388
+ } catch {
389
+ return "main";
390
+ }
391
+ }