@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,509 @@
|
|
|
1
|
+
import { spawn, exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { queries, type DevServer, type DevServerType, type DevServerStatus } from "./db";
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
const LOGS_DIR = path.join(process.env.HOME || "~", ".claude-deck", "logs");
|
|
10
|
+
|
|
11
|
+
// Ensure logs directory exists
|
|
12
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
13
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StartServerOptions {
|
|
17
|
+
projectId: string;
|
|
18
|
+
type: DevServerType;
|
|
19
|
+
name: string;
|
|
20
|
+
command: string;
|
|
21
|
+
workingDirectory: string;
|
|
22
|
+
ports?: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DetectedServer {
|
|
26
|
+
type: DevServerType;
|
|
27
|
+
name: string;
|
|
28
|
+
command: string;
|
|
29
|
+
ports: number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Generate unique ID
|
|
33
|
+
function generateId(): string {
|
|
34
|
+
return `ds_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get log file path for a server
|
|
38
|
+
export function getLogPath(serverId: string): string {
|
|
39
|
+
return path.join(LOGS_DIR, `${serverId}.log`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if a process is running by PID
|
|
43
|
+
async function isPidRunning(pid: number): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if a port is in use
|
|
53
|
+
async function isPortInUse(port: number): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
const { stdout } = await execAsync(
|
|
56
|
+
`lsof -i :${port} -t 2>/dev/null || true`
|
|
57
|
+
);
|
|
58
|
+
return stdout.trim().length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get PID using a port
|
|
65
|
+
async function getPidOnPort(port: number): Promise<number | null> {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await execAsync(
|
|
68
|
+
`lsof -i :${port} -t 2>/dev/null | head -1`
|
|
69
|
+
);
|
|
70
|
+
const pid = parseInt(stdout.trim(), 10);
|
|
71
|
+
return isNaN(pid) ? null : pid;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check Node.js server status
|
|
78
|
+
async function checkNodeStatus(server: DevServer): Promise<DevServerStatus> {
|
|
79
|
+
if (server.pid) {
|
|
80
|
+
const running = await isPidRunning(server.pid);
|
|
81
|
+
if (running) return "running";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if any of its ports are in use
|
|
85
|
+
const ports: number[] = JSON.parse(server.ports || "[]");
|
|
86
|
+
for (const port of ports) {
|
|
87
|
+
if (await isPortInUse(port)) {
|
|
88
|
+
// Port is in use, try to get the PID
|
|
89
|
+
const pid = await getPidOnPort(port);
|
|
90
|
+
if (pid) {
|
|
91
|
+
// Update PID in database
|
|
92
|
+
await queries.updateDevServerPid(pid, "running", server.id);
|
|
93
|
+
return "running";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return "stopped";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check Docker service status
|
|
102
|
+
async function checkDockerStatus(server: DevServer): Promise<DevServerStatus> {
|
|
103
|
+
if (!server.container_id) return "stopped";
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await execAsync(
|
|
107
|
+
`docker inspect -f '{{.State.Status}}' ${server.container_id} 2>/dev/null || echo ""`
|
|
108
|
+
);
|
|
109
|
+
const status = stdout.trim();
|
|
110
|
+
if (status === "running") return "running";
|
|
111
|
+
if (status === "starting" || status === "restarting") return "starting";
|
|
112
|
+
return "stopped";
|
|
113
|
+
} catch {
|
|
114
|
+
return "stopped";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get live status for a server
|
|
119
|
+
export async function getServerStatus(
|
|
120
|
+
server: DevServer
|
|
121
|
+
): Promise<DevServerStatus> {
|
|
122
|
+
if (server.type === "docker") {
|
|
123
|
+
return checkDockerStatus(server);
|
|
124
|
+
}
|
|
125
|
+
return checkNodeStatus(server);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get all servers with live status
|
|
129
|
+
export async function getAllServers(): Promise<DevServer[]> {
|
|
130
|
+
const servers = await queries.getAllDevServers();
|
|
131
|
+
|
|
132
|
+
// Update status for each server
|
|
133
|
+
for (const server of servers) {
|
|
134
|
+
const liveStatus = await getServerStatus(server);
|
|
135
|
+
if (liveStatus !== server.status) {
|
|
136
|
+
await queries.updateDevServerStatus(liveStatus, server.id);
|
|
137
|
+
server.status = liveStatus;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return servers;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get servers for a project
|
|
145
|
+
export async function getServersByProject(
|
|
146
|
+
projectId: string
|
|
147
|
+
): Promise<DevServer[]> {
|
|
148
|
+
const servers = await queries.getDevServersByProject(projectId);
|
|
149
|
+
|
|
150
|
+
for (const server of servers) {
|
|
151
|
+
const liveStatus = await getServerStatus(server);
|
|
152
|
+
if (liveStatus !== server.status) {
|
|
153
|
+
await queries.updateDevServerStatus(liveStatus, server.id);
|
|
154
|
+
server.status = liveStatus;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return servers;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Expand ~ to home directory
|
|
162
|
+
function expandHome(filePath: string): string {
|
|
163
|
+
if (filePath.startsWith("~/")) {
|
|
164
|
+
return path.join(process.env.HOME || "", filePath.slice(2));
|
|
165
|
+
}
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Start a Node.js server
|
|
170
|
+
async function spawnNodeServer(
|
|
171
|
+
id: string,
|
|
172
|
+
command: string,
|
|
173
|
+
workingDirectory: string,
|
|
174
|
+
ports: number[]
|
|
175
|
+
): Promise<{ pid: number }> {
|
|
176
|
+
const logPath = getLogPath(id);
|
|
177
|
+
// Open log file for appending (get file descriptor for spawn)
|
|
178
|
+
const logFd = fs.openSync(logPath, "a");
|
|
179
|
+
|
|
180
|
+
// Expand ~ to absolute path - Node.js spawn doesn't handle tilde
|
|
181
|
+
const cwd = expandHome(workingDirectory);
|
|
182
|
+
|
|
183
|
+
// Build minimal env - only essentials for shell to work
|
|
184
|
+
// This lets Next.js/Vite/etc load .env.local without interference from parent process env
|
|
185
|
+
const env: Record<string, string | undefined> = {
|
|
186
|
+
PATH: process.env.PATH,
|
|
187
|
+
HOME: process.env.HOME,
|
|
188
|
+
USER: process.env.USER,
|
|
189
|
+
SHELL: process.env.SHELL,
|
|
190
|
+
TERM: process.env.TERM || "xterm-256color",
|
|
191
|
+
LANG: process.env.LANG || "en_US.UTF-8",
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Add port if specified
|
|
195
|
+
if (ports.length > 0) {
|
|
196
|
+
env.PORT = String(ports[0]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const fullCommand = `cd "${cwd}" && ${command}`;
|
|
200
|
+
|
|
201
|
+
const child = spawn(fullCommand, [], {
|
|
202
|
+
cwd,
|
|
203
|
+
env: env as NodeJS.ProcessEnv,
|
|
204
|
+
shell: true,
|
|
205
|
+
detached: true,
|
|
206
|
+
stdio: ["ignore", logFd, logFd],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (child.pid) child.unref();
|
|
210
|
+
|
|
211
|
+
// Close our reference to the fd - the child process has its own
|
|
212
|
+
fs.closeSync(logFd);
|
|
213
|
+
|
|
214
|
+
// Give it a moment to start
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
216
|
+
|
|
217
|
+
return { pid: child.pid || 0 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Start a Docker Compose service
|
|
221
|
+
async function spawnDockerService(
|
|
222
|
+
command: string,
|
|
223
|
+
workingDirectory: string
|
|
224
|
+
): Promise<{ containerId: string | null }> {
|
|
225
|
+
try {
|
|
226
|
+
// command is expected to be the service name
|
|
227
|
+
await execAsync(`docker compose up -d ${command}`, {
|
|
228
|
+
cwd: workingDirectory,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Get container ID
|
|
232
|
+
const { stdout } = await execAsync(
|
|
233
|
+
`docker compose ps -q ${command} 2>/dev/null || echo ""`,
|
|
234
|
+
{ cwd: workingDirectory }
|
|
235
|
+
);
|
|
236
|
+
const containerId = stdout.trim() || null;
|
|
237
|
+
|
|
238
|
+
return { containerId };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error("Failed to start Docker service:", error);
|
|
241
|
+
return { containerId: null };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Start a server
|
|
246
|
+
export async function startServer(
|
|
247
|
+
opts: StartServerOptions
|
|
248
|
+
): Promise<DevServer> {
|
|
249
|
+
const id = generateId();
|
|
250
|
+
const ports = opts.ports || [];
|
|
251
|
+
|
|
252
|
+
// Create database record first
|
|
253
|
+
await queries.createDevServer(
|
|
254
|
+
id,
|
|
255
|
+
opts.projectId,
|
|
256
|
+
opts.type,
|
|
257
|
+
opts.name,
|
|
258
|
+
opts.command,
|
|
259
|
+
"starting",
|
|
260
|
+
null, // pid
|
|
261
|
+
null, // container_id
|
|
262
|
+
JSON.stringify(ports),
|
|
263
|
+
opts.workingDirectory
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (opts.type === "docker") {
|
|
268
|
+
const { containerId } = await spawnDockerService(
|
|
269
|
+
opts.command,
|
|
270
|
+
opts.workingDirectory
|
|
271
|
+
);
|
|
272
|
+
await queries.updateDevServer("running", null, containerId, JSON.stringify(ports), id);
|
|
273
|
+
} else {
|
|
274
|
+
const { pid } = await spawnNodeServer(
|
|
275
|
+
id,
|
|
276
|
+
opts.command,
|
|
277
|
+
opts.workingDirectory,
|
|
278
|
+
ports
|
|
279
|
+
);
|
|
280
|
+
await queries.updateDevServer("running", pid, null, JSON.stringify(ports), id);
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
await queries.updateDevServerStatus("failed", id);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return (await queries.getDevServer(id))!;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Stop a server
|
|
291
|
+
export async function stopServer(id: string): Promise<void> {
|
|
292
|
+
const server = await queries.getDevServer(id);
|
|
293
|
+
if (!server) return;
|
|
294
|
+
|
|
295
|
+
if (server.type === "docker") {
|
|
296
|
+
if (server.container_id) {
|
|
297
|
+
try {
|
|
298
|
+
await execAsync(`docker stop ${server.container_id}`);
|
|
299
|
+
} catch {
|
|
300
|
+
// Container may already be stopped
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
if (server.pid) {
|
|
305
|
+
try {
|
|
306
|
+
process.kill(server.pid, "SIGTERM");
|
|
307
|
+
// Give it time to gracefully shut down
|
|
308
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
309
|
+
// Force kill if still running
|
|
310
|
+
if (await isPidRunning(server.pid)) {
|
|
311
|
+
process.kill(server.pid, "SIGKILL");
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Process may already be dead
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Also check ports and kill anything on them
|
|
319
|
+
const ports: number[] = JSON.parse(server.ports || "[]");
|
|
320
|
+
for (const port of ports) {
|
|
321
|
+
const pid = await getPidOnPort(port);
|
|
322
|
+
if (pid) {
|
|
323
|
+
try {
|
|
324
|
+
process.kill(pid, "SIGTERM");
|
|
325
|
+
} catch {
|
|
326
|
+
// Ignore
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await queries.updateDevServerStatus("stopped", id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Restart a server
|
|
336
|
+
export async function restartServer(id: string): Promise<DevServer> {
|
|
337
|
+
const server = await queries.getDevServer(id);
|
|
338
|
+
if (!server) throw new Error("Server not found");
|
|
339
|
+
|
|
340
|
+
await stopServer(id);
|
|
341
|
+
|
|
342
|
+
// Re-start with same config
|
|
343
|
+
if (server.type === "docker") {
|
|
344
|
+
const { containerId } = await spawnDockerService(
|
|
345
|
+
server.command,
|
|
346
|
+
server.working_directory
|
|
347
|
+
);
|
|
348
|
+
await queries.updateDevServer("running", null, containerId, server.ports, id);
|
|
349
|
+
} else {
|
|
350
|
+
const ports: number[] = JSON.parse(server.ports || "[]");
|
|
351
|
+
const { pid } = await spawnNodeServer(
|
|
352
|
+
id,
|
|
353
|
+
server.command,
|
|
354
|
+
server.working_directory,
|
|
355
|
+
ports
|
|
356
|
+
);
|
|
357
|
+
await queries.updateDevServer("running", pid, null, server.ports, id);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (await queries.getDevServer(id))!;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Remove a server (stop and delete)
|
|
364
|
+
export async function removeServer(id: string): Promise<void> {
|
|
365
|
+
await stopServer(id);
|
|
366
|
+
await queries.deleteDevServer(id);
|
|
367
|
+
|
|
368
|
+
// Clean up log file
|
|
369
|
+
const logPath = getLogPath(id);
|
|
370
|
+
if (fs.existsSync(logPath)) {
|
|
371
|
+
fs.unlinkSync(logPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get logs for a server
|
|
376
|
+
export async function getServerLogs(
|
|
377
|
+
id: string,
|
|
378
|
+
lines = 100
|
|
379
|
+
): Promise<string[]> {
|
|
380
|
+
const server = await queries.getDevServer(id);
|
|
381
|
+
if (!server) return [];
|
|
382
|
+
|
|
383
|
+
if (server.type === "docker" && server.container_id) {
|
|
384
|
+
try {
|
|
385
|
+
const { stdout } = await execAsync(
|
|
386
|
+
`docker logs --tail ${lines} ${server.container_id} 2>&1`
|
|
387
|
+
);
|
|
388
|
+
return stdout.split("\n");
|
|
389
|
+
} catch {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Node.js server - read log file
|
|
395
|
+
const logPath = getLogPath(id);
|
|
396
|
+
if (!fs.existsSync(logPath)) return [];
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
400
|
+
const allLines = content.split("\n");
|
|
401
|
+
return allLines.slice(-lines);
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Detect available Node.js dev server in a directory
|
|
408
|
+
export async function detectNodeServer(
|
|
409
|
+
workingDir: string
|
|
410
|
+
): Promise<DetectedServer | null> {
|
|
411
|
+
const packageJsonPath = path.join(workingDir, "package.json");
|
|
412
|
+
if (!fs.existsSync(packageJsonPath)) return null;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
416
|
+
const scripts = packageJson.scripts || {};
|
|
417
|
+
|
|
418
|
+
// Look for common dev server scripts
|
|
419
|
+
const devScripts = ["dev", "start", "serve", "develop"];
|
|
420
|
+
for (const script of devScripts) {
|
|
421
|
+
if (scripts[script]) {
|
|
422
|
+
// Try to detect port from script
|
|
423
|
+
const scriptContent = scripts[script];
|
|
424
|
+
let port = 3000; // default
|
|
425
|
+
const portMatch = scriptContent.match(/(?:port|PORT)[=\s]+(\d+)/i);
|
|
426
|
+
if (portMatch) {
|
|
427
|
+
port = parseInt(portMatch[1], 10);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
type: "node",
|
|
432
|
+
name: packageJson.name || path.basename(workingDir),
|
|
433
|
+
command: `npm run ${script}`,
|
|
434
|
+
ports: [port],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore parse errors
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Detect Docker Compose services in a directory
|
|
446
|
+
export async function detectDockerServices(
|
|
447
|
+
workingDir: string
|
|
448
|
+
): Promise<DetectedServer[]> {
|
|
449
|
+
const composeFiles = [
|
|
450
|
+
"docker-compose.yml",
|
|
451
|
+
"docker-compose.yaml",
|
|
452
|
+
"compose.yml",
|
|
453
|
+
"compose.yaml",
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
for (const file of composeFiles) {
|
|
457
|
+
const composePath = path.join(workingDir, file);
|
|
458
|
+
if (fs.existsSync(composePath)) {
|
|
459
|
+
try {
|
|
460
|
+
const { stdout } = await execAsync(
|
|
461
|
+
`docker compose -f ${file} config --services 2>/dev/null || echo ""`,
|
|
462
|
+
{ cwd: workingDir }
|
|
463
|
+
);
|
|
464
|
+
const services = stdout.trim().split("\n").filter(Boolean);
|
|
465
|
+
|
|
466
|
+
return services.map((service) => ({
|
|
467
|
+
type: "docker" as const,
|
|
468
|
+
name: service,
|
|
469
|
+
command: service,
|
|
470
|
+
ports: [], // Would need to parse compose file for ports
|
|
471
|
+
}));
|
|
472
|
+
} catch {
|
|
473
|
+
// Docker not available or compose file invalid
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Detect all available servers in a directory
|
|
482
|
+
export async function detectServers(
|
|
483
|
+
workingDir: string
|
|
484
|
+
): Promise<DetectedServer[]> {
|
|
485
|
+
const servers: DetectedServer[] = [];
|
|
486
|
+
|
|
487
|
+
const nodeServer = await detectNodeServer(workingDir);
|
|
488
|
+
if (nodeServer) {
|
|
489
|
+
servers.push(nodeServer);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const dockerServices = await detectDockerServices(workingDir);
|
|
493
|
+
servers.push(...dockerServices);
|
|
494
|
+
|
|
495
|
+
return servers;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Clean up orphaned servers on startup
|
|
499
|
+
export async function cleanupOrphanedServers(): Promise<void> {
|
|
500
|
+
const servers = await queries.getAllDevServers();
|
|
501
|
+
|
|
502
|
+
for (const server of servers) {
|
|
503
|
+
const liveStatus = await getServerStatus(server);
|
|
504
|
+
if (server.status === "running" && liveStatus === "stopped") {
|
|
505
|
+
// Server was running but is now dead
|
|
506
|
+
await queries.updateDevServerStatus("stopped", server.id);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff parser
|
|
3
|
+
* Parses git diff output into structured data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DiffLine {
|
|
7
|
+
type: "context" | "addition" | "deletion" | "header";
|
|
8
|
+
content: string;
|
|
9
|
+
oldLineNumber: number | null;
|
|
10
|
+
newLineNumber: number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DiffHunk {
|
|
14
|
+
header: string;
|
|
15
|
+
oldStart: number;
|
|
16
|
+
oldCount: number;
|
|
17
|
+
newStart: number;
|
|
18
|
+
newCount: number;
|
|
19
|
+
lines: DiffLine[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ParsedDiff {
|
|
23
|
+
oldFile: string;
|
|
24
|
+
newFile: string;
|
|
25
|
+
hunks: DiffHunk[];
|
|
26
|
+
additions: number;
|
|
27
|
+
deletions: number;
|
|
28
|
+
isBinary: boolean;
|
|
29
|
+
isNew: boolean;
|
|
30
|
+
isDeleted: boolean;
|
|
31
|
+
isRenamed: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a unified diff string into structured data
|
|
36
|
+
*/
|
|
37
|
+
export function parseDiff(diffText: string): ParsedDiff {
|
|
38
|
+
const lines = diffText.split("\n");
|
|
39
|
+
|
|
40
|
+
let oldFile = "";
|
|
41
|
+
let newFile = "";
|
|
42
|
+
const hunks: DiffHunk[] = [];
|
|
43
|
+
let currentHunk: DiffHunk | null = null;
|
|
44
|
+
let additions = 0;
|
|
45
|
+
let deletions = 0;
|
|
46
|
+
let isBinary = false;
|
|
47
|
+
let isNew = false;
|
|
48
|
+
let isDeleted = false;
|
|
49
|
+
let isRenamed = false;
|
|
50
|
+
|
|
51
|
+
let oldLineNum = 0;
|
|
52
|
+
let newLineNum = 0;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i];
|
|
56
|
+
|
|
57
|
+
// Binary file detection
|
|
58
|
+
if (line.startsWith("Binary files")) {
|
|
59
|
+
isBinary = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Old file header
|
|
64
|
+
if (line.startsWith("--- ")) {
|
|
65
|
+
oldFile = line.slice(4);
|
|
66
|
+
if (oldFile === "/dev/null") {
|
|
67
|
+
isNew = true;
|
|
68
|
+
}
|
|
69
|
+
// Strip a/ prefix
|
|
70
|
+
if (oldFile.startsWith("a/")) {
|
|
71
|
+
oldFile = oldFile.slice(2);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// New file header
|
|
77
|
+
if (line.startsWith("+++ ")) {
|
|
78
|
+
newFile = line.slice(4);
|
|
79
|
+
if (newFile === "/dev/null") {
|
|
80
|
+
isDeleted = true;
|
|
81
|
+
}
|
|
82
|
+
// Strip b/ prefix
|
|
83
|
+
if (newFile.startsWith("b/")) {
|
|
84
|
+
newFile = newFile.slice(2);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rename detection
|
|
90
|
+
if (line.startsWith("rename from ") || line.startsWith("rename to ")) {
|
|
91
|
+
isRenamed = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
96
|
+
const hunkMatch = line.match(
|
|
97
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/
|
|
98
|
+
);
|
|
99
|
+
if (hunkMatch) {
|
|
100
|
+
if (currentHunk) {
|
|
101
|
+
hunks.push(currentHunk);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const oldStart = parseInt(hunkMatch[1], 10);
|
|
105
|
+
const oldCount = hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1;
|
|
106
|
+
const newStart = parseInt(hunkMatch[3], 10);
|
|
107
|
+
const newCount = hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1;
|
|
108
|
+
const context = hunkMatch[5] || "";
|
|
109
|
+
|
|
110
|
+
currentHunk = {
|
|
111
|
+
header: line,
|
|
112
|
+
oldStart,
|
|
113
|
+
oldCount,
|
|
114
|
+
newStart,
|
|
115
|
+
newCount,
|
|
116
|
+
lines: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add header line with context (function name, etc.)
|
|
120
|
+
if (context.trim()) {
|
|
121
|
+
currentHunk.lines.push({
|
|
122
|
+
type: "header",
|
|
123
|
+
content: context.trim(),
|
|
124
|
+
oldLineNumber: null,
|
|
125
|
+
newLineNumber: null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
oldLineNum = oldStart;
|
|
130
|
+
newLineNum = newStart;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Inside a hunk
|
|
135
|
+
if (currentHunk) {
|
|
136
|
+
if (line.startsWith("+")) {
|
|
137
|
+
currentHunk.lines.push({
|
|
138
|
+
type: "addition",
|
|
139
|
+
content: line.slice(1),
|
|
140
|
+
oldLineNumber: null,
|
|
141
|
+
newLineNumber: newLineNum,
|
|
142
|
+
});
|
|
143
|
+
newLineNum++;
|
|
144
|
+
additions++;
|
|
145
|
+
} else if (line.startsWith("-")) {
|
|
146
|
+
currentHunk.lines.push({
|
|
147
|
+
type: "deletion",
|
|
148
|
+
content: line.slice(1),
|
|
149
|
+
oldLineNumber: oldLineNum,
|
|
150
|
+
newLineNumber: null,
|
|
151
|
+
});
|
|
152
|
+
oldLineNum++;
|
|
153
|
+
deletions++;
|
|
154
|
+
} else if (line.startsWith(" ") || line === "") {
|
|
155
|
+
// Context line or empty
|
|
156
|
+
const content = line.startsWith(" ") ? line.slice(1) : line;
|
|
157
|
+
currentHunk.lines.push({
|
|
158
|
+
type: "context",
|
|
159
|
+
content,
|
|
160
|
+
oldLineNumber: oldLineNum,
|
|
161
|
+
newLineNumber: newLineNum,
|
|
162
|
+
});
|
|
163
|
+
oldLineNum++;
|
|
164
|
+
newLineNum++;
|
|
165
|
+
}
|
|
166
|
+
// Skip other lines like ""
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Don't forget the last hunk
|
|
171
|
+
if (currentHunk) {
|
|
172
|
+
hunks.push(currentHunk);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
oldFile,
|
|
177
|
+
newFile,
|
|
178
|
+
hunks,
|
|
179
|
+
additions,
|
|
180
|
+
deletions,
|
|
181
|
+
isBinary,
|
|
182
|
+
isNew,
|
|
183
|
+
isDeleted,
|
|
184
|
+
isRenamed,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get a summary string for a diff
|
|
190
|
+
*/
|
|
191
|
+
export function getDiffSummary(diff: ParsedDiff): string {
|
|
192
|
+
if (diff.isBinary) {
|
|
193
|
+
return "Binary file";
|
|
194
|
+
}
|
|
195
|
+
if (diff.isNew) {
|
|
196
|
+
return `New file (+${diff.additions})`;
|
|
197
|
+
}
|
|
198
|
+
if (diff.isDeleted) {
|
|
199
|
+
return `Deleted (-${diff.deletions})`;
|
|
200
|
+
}
|
|
201
|
+
if (diff.isRenamed) {
|
|
202
|
+
return `Renamed (+${diff.additions}, -${diff.deletions})`;
|
|
203
|
+
}
|
|
204
|
+
return `+${diff.additions}, -${diff.deletions}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get file name from diff (handles renames)
|
|
209
|
+
*/
|
|
210
|
+
export function getDiffFileName(diff: ParsedDiff): string {
|
|
211
|
+
if (diff.isNew) {
|
|
212
|
+
return diff.newFile;
|
|
213
|
+
}
|
|
214
|
+
if (diff.isDeleted) {
|
|
215
|
+
return diff.oldFile;
|
|
216
|
+
}
|
|
217
|
+
if (diff.isRenamed) {
|
|
218
|
+
return `${diff.oldFile} → ${diff.newFile}`;
|
|
219
|
+
}
|
|
220
|
+
return diff.newFile || diff.oldFile;
|
|
221
|
+
}
|