@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- 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
|
+
}
|