@atercates/claude-deck 0.2.1 → 0.2.2
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/README.md +4 -18
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +1 -1
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +1 -1
- package/app/api/sessions/[id]/fork/route.ts +1 -1
- package/app/api/sessions/[id]/pr/route.ts +1 -1
- package/app/api/sessions/[id]/preview/route.ts +1 -1
- package/app/api/sessions/[id]/route.ts +13 -4
- package/app/api/sessions/[id]/send-keys/route.ts +1 -1
- package/app/api/sessions/route.ts +2 -2
- package/components/ConductorPanel.tsx +1 -1
- package/components/DevServers/ServerLogsModal.tsx +24 -21
- package/components/DiffViewer/DiffModal.tsx +0 -1
- package/components/FileExplorer/index.tsx +1 -1
- package/components/GitDrawer/FileEditDialog.tsx +1 -1
- package/components/GitPanel/FileChanges.tsx +6 -2
- package/components/GitPanel/index.tsx +1 -1
- package/components/Pane/index.tsx +16 -15
- package/components/Projects/ProjectCard.tsx +1 -1
- package/components/QuickSwitcher.tsx +1 -0
- package/components/SessionList/SessionList.types.ts +1 -1
- package/components/SessionList/index.tsx +8 -8
- package/components/Terminal/hooks/useTerminalConnection.ts +3 -2
- package/components/Terminal/hooks/websocket-connection.ts +1 -0
- package/data/git/queries.ts +0 -1
- package/lib/claude/process-manager.ts +1 -1
- package/lib/code-search.ts +5 -5
- package/lib/db/index.ts +1 -1
- package/lib/git-history.ts +1 -1
- package/lib/git.ts +0 -1
- package/lib/multi-repo-git.ts +0 -1
- package/lib/projects.ts +29 -8
- package/package.json +4 -4
- package/scripts/agent-os +1 -1
- package/scripts/install.sh +2 -2
- package/server.ts +1 -1
package/README.md
CHANGED
|
@@ -30,13 +30,13 @@ claude-deck start
|
|
|
30
30
|
For fresh installs without Node.js:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
curl -fsSL https://raw.githubusercontent.com/
|
|
33
|
+
curl -fsSL https://raw.githubusercontent.com/ATERCATES/claude-deck/main/scripts/install.sh | bash
|
|
34
34
|
claude-deck start
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
### Desktop App
|
|
38
38
|
|
|
39
|
-
Download native desktop apps from [Releases](https://github.com/
|
|
39
|
+
Download native desktop apps from [Releases](https://github.com/ATERCATES/claude-deck/releases):
|
|
40
40
|
|
|
41
41
|
- macOS (Apple Silicon): `.dmg`
|
|
42
42
|
- Linux: `.deb` or `.AppImage`
|
|
@@ -48,7 +48,7 @@ Download native desktop apps from [Releases](https://github.com/atercates/claude
|
|
|
48
48
|
### Manual Install
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
git clone https://github.com/
|
|
51
|
+
git clone https://github.com/ATERCATES/claude-deck
|
|
52
52
|
cd claude-deck
|
|
53
53
|
npm install
|
|
54
54
|
npm run dev # http://localhost:3011
|
|
@@ -61,20 +61,6 @@ npm run dev # http://localhost:3011
|
|
|
61
61
|
- [ripgrep](https://github.com/BurntSushi/ripgrep) (for code search - auto-installed by installer script, or run `claude-deck update`)
|
|
62
62
|
- At least one AI CLI: [Claude Code](https://github.com/anthropics/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://github.com/anomalyco/opencode), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Aider](https://aider.chat/), or [Cursor CLI](https://cursor.com/cli)
|
|
63
63
|
|
|
64
|
-
## Supported Agents
|
|
65
|
-
|
|
66
|
-
| Agent | Resume | Fork | Auto-Approve |
|
|
67
|
-
| ----------- | ------ | ---- | -------------------------------- |
|
|
68
|
-
| Claude Code | ✅ | ✅ | `--dangerously-skip-permissions` |
|
|
69
|
-
| Codex | ❌ | ❌ | `--approval-mode full-auto` |
|
|
70
|
-
| OpenCode | ❌ | ❌ | Config file |
|
|
71
|
-
| Gemini CLI | ❌ | ❌ | `--yolomode` |
|
|
72
|
-
| Aider | ❌ | ❌ | `--yes` |
|
|
73
|
-
| Cursor CLI | ❌ | ❌ | N/A |
|
|
74
|
-
| Amp | ❌ | ❌ | `--dangerously-allow-all` |
|
|
75
|
-
| Pi | ❌ | ❌ | N/A |
|
|
76
|
-
| Oh My Pi | ❌ | ❌ | N/A |
|
|
77
|
-
|
|
78
64
|
## Features
|
|
79
65
|
|
|
80
66
|
- **Mobile-first** - Full functionality from your phone, not a dumbed-down responsive view
|
|
@@ -113,7 +99,7 @@ For configuration and advanced usage, see the [docs](https://www.runagentos.com/
|
|
|
113
99
|
|
|
114
100
|
## Related Projects
|
|
115
101
|
|
|
116
|
-
- **[aTerm](https://github.com/
|
|
102
|
+
- **[aTerm](https://github.com/ATERCATES/aTerm)** - A Tauri-based desktop terminal workspace for AI-assisted coding. While ClaudeDeck is a mobile-first web UI, aTerm is a native desktop app with multi-pane layouts optimized for running AI coding agents (Claude Code, Aider, OpenCode) alongside shells, dev servers, and a built-in git panel. Choose ClaudeDeck for mobile access and browser-based workflows, or aTerm for a native desktop terminal experience.
|
|
117
103
|
- **[LumifyHub](https://lumifyhub.io)** - Team collaboration platform with real-time chat and structured documentation. Useful alongside ClaudeDeck for coordinating multi-agent work across a team — share session context, document architectural decisions from coding sessions, and track progress across parallel agent workflows.
|
|
118
104
|
|
|
119
105
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { updateProjectDevServer, deleteProjectDevServer } from "@/lib/projects";
|
|
3
|
-
import { queries
|
|
3
|
+
import { queries } from "@/lib/db";
|
|
4
4
|
|
|
5
5
|
interface RouteParams {
|
|
6
6
|
params: Promise<{ id: string; dsId: string }>;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
updateProjectRepository,
|
|
4
4
|
deleteProjectRepository,
|
|
5
5
|
} from "@/lib/projects";
|
|
6
|
-
import { queries
|
|
6
|
+
import { queries } from "@/lib/db";
|
|
7
7
|
|
|
8
8
|
interface RouteParams {
|
|
9
9
|
params: Promise<{ id: string; repoId: string }>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { exec } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
-
import { queries
|
|
4
|
+
import { queries } from "@/lib/db";
|
|
5
5
|
import { deleteWorktree, isClaudeDeckWorktree } from "@/lib/worktrees";
|
|
6
6
|
import { releasePort } from "@/lib/ports";
|
|
7
7
|
import { killWorker } from "@/lib/orchestration";
|
|
@@ -81,7 +81,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// If this is a worktree session, also rename the git branch
|
|
84
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
existing.worktree_path &&
|
|
86
|
+
isClaudeDeckWorktree(existing.worktree_path)
|
|
87
|
+
) {
|
|
85
88
|
try {
|
|
86
89
|
const currentBranch = await getCurrentBranch(existing.worktree_path);
|
|
87
90
|
const newBranchName = generateBranchName(body.name);
|
|
@@ -174,7 +177,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
174
177
|
await queries.deleteSession(id);
|
|
175
178
|
|
|
176
179
|
// Clean up worktree in background (non-blocking)
|
|
177
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
existing.worktree_path &&
|
|
182
|
+
isClaudeDeckWorktree(existing.worktree_path)
|
|
183
|
+
) {
|
|
178
184
|
const worktreePath = existing.worktree_path; // Capture for closure
|
|
179
185
|
runInBackground(async () => {
|
|
180
186
|
const { exec } = await import("child_process");
|
|
@@ -196,7 +202,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
196
202
|
// Also cleanup worker worktrees in background
|
|
197
203
|
if (workers.length > 0) {
|
|
198
204
|
for (const worker of workers) {
|
|
199
|
-
if (
|
|
205
|
+
if (
|
|
206
|
+
worker.worktree_path &&
|
|
207
|
+
isClaudeDeckWorktree(worker.worktree_path)
|
|
208
|
+
) {
|
|
200
209
|
const worktreePath = worker.worktree_path; // Capture for closure
|
|
201
210
|
const workerId = worker.id; // Capture ID for task name
|
|
202
211
|
runInBackground(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { exec } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
-
import { queries
|
|
4
|
+
import { queries } from "@/lib/db";
|
|
5
5
|
import { appendFileSync } from "fs";
|
|
6
6
|
|
|
7
7
|
const execAsync = promisify(exec);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
-
import { queries, type Session
|
|
3
|
+
import { queries, type Session } from "@/lib/db";
|
|
4
4
|
import { isValidAgentType, type AgentType } from "@/lib/providers";
|
|
5
5
|
import { createWorktree } from "@/lib/worktrees";
|
|
6
6
|
import { setupWorktree, type SetupResult } from "@/lib/env-setup";
|
|
@@ -88,7 +88,7 @@ export async function POST(request: NextRequest) {
|
|
|
88
88
|
let branchName: string | null = null;
|
|
89
89
|
let actualWorkingDirectory = workingDirectory;
|
|
90
90
|
let port: number | null = null;
|
|
91
|
-
|
|
91
|
+
const setupResult: SetupResult | null = null;
|
|
92
92
|
|
|
93
93
|
if (useWorktree && featureName) {
|
|
94
94
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { WorkerCard, type WorkerInfo
|
|
4
|
+
import { WorkerCard, type WorkerInfo } from "./WorkerCard";
|
|
5
5
|
import { Button } from "./ui/button";
|
|
6
6
|
import {
|
|
7
7
|
RefreshCw,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import { X, RefreshCw, Loader2 } from "lucide-react";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
@@ -20,31 +20,34 @@ export function ServerLogsModal({
|
|
|
20
20
|
const [refreshing, setRefreshing] = useState(false);
|
|
21
21
|
const logsRef = useRef<HTMLDivElement>(null);
|
|
22
22
|
|
|
23
|
-
const fetchLogs =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
const fetchLogs = useCallback(
|
|
24
|
+
async (isRefresh = false) => {
|
|
25
|
+
if (isRefresh) {
|
|
26
|
+
setRefreshing(true);
|
|
27
|
+
} else {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`/api/dev-servers/${serverId}/logs?lines=200`);
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
setLogs(data.logs || []);
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error("Failed to fetch logs:", err);
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
setRefreshing(false);
|
|
35
42
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
setLoading(false);
|
|
40
|
-
setRefreshing(false);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
+
},
|
|
44
|
+
[serverId]
|
|
45
|
+
);
|
|
43
46
|
|
|
44
47
|
// Initial fetch
|
|
45
48
|
useEffect(() => {
|
|
46
49
|
fetchLogs();
|
|
47
|
-
}, [
|
|
50
|
+
}, [fetchLogs]);
|
|
48
51
|
|
|
49
52
|
// Auto-scroll to bottom when logs update
|
|
50
53
|
useEffect(() => {
|
|
@@ -59,7 +62,7 @@ export function ServerLogsModal({
|
|
|
59
62
|
fetchLogs(true);
|
|
60
63
|
}, 3000);
|
|
61
64
|
return () => clearInterval(interval);
|
|
62
|
-
}, [
|
|
65
|
+
}, [fetchLogs]);
|
|
63
66
|
|
|
64
67
|
return (
|
|
65
68
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
@@ -4,7 +4,6 @@ import { X, Plus, Minus, ChevronLeft } from "lucide-react";
|
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
5
|
import { UnifiedDiff } from "./UnifiedDiff";
|
|
6
6
|
import { parseDiff, getDiffFileName, getDiffSummary } from "@/lib/diff-parser";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
7
|
|
|
9
8
|
interface DiffModalProps {
|
|
10
9
|
diff: string;
|
|
@@ -108,7 +108,7 @@ export function FileEditDialog({
|
|
|
108
108
|
: baseDir;
|
|
109
109
|
const filePath = `${expandedBaseDir}/${file.path}`;
|
|
110
110
|
const fileName = file.path.split("/").pop() || file.path;
|
|
111
|
-
const
|
|
111
|
+
const _repoName = "repoName" in file ? file.repoName : null;
|
|
112
112
|
const hasChanges = modifiedContent !== initialModified;
|
|
113
113
|
|
|
114
114
|
useEffect(() => {
|
|
@@ -44,7 +44,7 @@ const SWIPE_THRESHOLD = 80;
|
|
|
44
44
|
export function FileChanges({
|
|
45
45
|
files,
|
|
46
46
|
title,
|
|
47
|
-
emptyMessage,
|
|
47
|
+
emptyMessage: _emptyMessage,
|
|
48
48
|
selectedPath,
|
|
49
49
|
onFileClick,
|
|
50
50
|
onStage,
|
|
@@ -99,7 +99,11 @@ export function FileChanges({
|
|
|
99
99
|
<button
|
|
100
100
|
onClick={(e) => {
|
|
101
101
|
e.stopPropagation();
|
|
102
|
-
|
|
102
|
+
if (isStaged) {
|
|
103
|
+
onUnstageAll?.();
|
|
104
|
+
} else {
|
|
105
|
+
onStageAll?.();
|
|
106
|
+
}
|
|
103
107
|
}}
|
|
104
108
|
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs transition-colors"
|
|
105
109
|
>
|
|
@@ -4,10 +4,7 @@ import { useRef, useCallback, useEffect, memo, useState, useMemo } from "react";
|
|
|
4
4
|
import dynamic from "next/dynamic";
|
|
5
5
|
import { usePanes } from "@/contexts/PaneContext";
|
|
6
6
|
import { useViewport } from "@/hooks/useViewport";
|
|
7
|
-
import type {
|
|
8
|
-
TerminalHandle,
|
|
9
|
-
TerminalScrollState,
|
|
10
|
-
} from "@/components/Terminal";
|
|
7
|
+
import type { TerminalHandle } from "@/components/Terminal";
|
|
11
8
|
import type { Session, Project } from "@/lib/db";
|
|
12
9
|
import { sessionRegistry } from "@/lib/client/session-registry";
|
|
13
10
|
import { cn } from "@/lib/utils";
|
|
@@ -114,16 +111,20 @@ export const Pane = memo(function Pane({
|
|
|
114
111
|
? (terminalRefs.current.get(activeTab.id) ?? null)
|
|
115
112
|
: null;
|
|
116
113
|
const isFocused = focusedPaneId === paneId;
|
|
117
|
-
const session =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
? (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
const session = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
activeTab
|
|
117
|
+
? (sessions.find((s) => s.id === activeTab.sessionId) ??
|
|
118
|
+
(activeTab.sessionId
|
|
119
|
+
? ({
|
|
120
|
+
id: activeTab.sessionId,
|
|
121
|
+
name: activeTab.sessionName || activeTab.sessionId.slice(0, 8),
|
|
122
|
+
working_directory: activeTab.workingDirectory || "~",
|
|
123
|
+
} as Session)
|
|
124
|
+
: null))
|
|
125
|
+
: null,
|
|
126
|
+
[activeTab, sessions]
|
|
127
|
+
);
|
|
127
128
|
|
|
128
129
|
// File editor state - lifted here so it persists across view switches
|
|
129
130
|
const fileEditor = useFileEditor();
|
|
@@ -236,7 +237,7 @@ export const Pane = memo(function Pane({
|
|
|
236
237
|
setTimeout(() => handle.sendCommand(`tmux attach -t ${tmuxName}`), 100);
|
|
237
238
|
}
|
|
238
239
|
},
|
|
239
|
-
[paneId, sessions, onRegisterTerminal]
|
|
240
|
+
[paneId, paneData, sessions, onRegisterTerminal]
|
|
240
241
|
);
|
|
241
242
|
|
|
242
243
|
// Track current tab ID for cleanup
|
|
@@ -108,7 +108,7 @@ export function ProjectCard({
|
|
|
108
108
|
setIsEditing(false);
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const handleClick = (
|
|
111
|
+
const handleClick = (_e: React.MouseEvent) => {
|
|
112
112
|
if (isEditing) return;
|
|
113
113
|
onClick?.();
|
|
114
114
|
onToggleExpanded?.(!project.expanded);
|
|
@@ -100,6 +100,7 @@ export function QuickSwitcher({
|
|
|
100
100
|
(a, b) =>
|
|
101
101
|
new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
102
102
|
);
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when .data changes, not entire query objects
|
|
103
104
|
}, [s0.data, s1.data, s2.data, s3.data, topProjects]);
|
|
104
105
|
|
|
105
106
|
const filteredSessions = useMemo(() => {
|
|
@@ -23,14 +23,14 @@ import type { SessionListProps } from "./SessionList.types";
|
|
|
23
23
|
export type { SessionListProps } from "./SessionList.types";
|
|
24
24
|
|
|
25
25
|
export function SessionList({
|
|
26
|
-
activeSessionId,
|
|
27
|
-
sessionStatuses,
|
|
26
|
+
activeSessionId: _activeSessionId,
|
|
27
|
+
sessionStatuses: _sessionStatuses,
|
|
28
28
|
onSelect,
|
|
29
|
-
onOpenInTab,
|
|
30
|
-
onNewSessionInProject,
|
|
31
|
-
onOpenTerminal,
|
|
32
|
-
onStartDevServer,
|
|
33
|
-
onCreateDevServer,
|
|
29
|
+
onOpenInTab: _onOpenInTab,
|
|
30
|
+
onNewSessionInProject: _onNewSessionInProject,
|
|
31
|
+
onOpenTerminal: _onOpenTerminal,
|
|
32
|
+
onStartDevServer: _onStartDevServer,
|
|
33
|
+
onCreateDevServer: _onCreateDevServer,
|
|
34
34
|
onResumeClaudeSession,
|
|
35
35
|
onNewSession,
|
|
36
36
|
}: SessionListProps) {
|
|
@@ -66,7 +66,7 @@ export function SessionList({
|
|
|
66
66
|
rect: DOMRect;
|
|
67
67
|
} | null>(null);
|
|
68
68
|
|
|
69
|
-
const
|
|
69
|
+
const _hoverHandlers = {
|
|
70
70
|
onHoverStart: useCallback(
|
|
71
71
|
(_session: Session, _rect: DOMRect) => {
|
|
72
72
|
if (isMobile) return;
|
|
@@ -131,6 +131,7 @@ export function useTerminalConnection({
|
|
|
131
131
|
useEffect(() => {
|
|
132
132
|
if (!terminalRef.current) return;
|
|
133
133
|
|
|
134
|
+
const terminalElement = terminalRef.current;
|
|
134
135
|
let cancelled = false;
|
|
135
136
|
// Reset intentional close flag (may be true from previous cleanup)
|
|
136
137
|
intentionalCloseRef.current = false;
|
|
@@ -208,9 +209,9 @@ export function useTerminalConnection({
|
|
|
208
209
|
|
|
209
210
|
// Save scroll state before unmount
|
|
210
211
|
const term = xtermRef.current;
|
|
211
|
-
if (term && callbacksRef.current.onBeforeUnmount &&
|
|
212
|
+
if (term && callbacksRef.current.onBeforeUnmount && terminalElement) {
|
|
212
213
|
const buffer = term.buffer.active;
|
|
213
|
-
const viewport =
|
|
214
|
+
const viewport = terminalElement.querySelector(
|
|
214
215
|
".xterm-viewport"
|
|
215
216
|
) as HTMLElement;
|
|
216
217
|
callbacksRef.current.onBeforeUnmount({
|
|
@@ -57,6 +57,7 @@ export function createWebSocketConnection(
|
|
|
57
57
|
|
|
58
58
|
// Force reconnect - kills any existing connection and creates fresh one
|
|
59
59
|
// Note: savedHandlers is populated after handlers are defined below
|
|
60
|
+
// eslint-disable-next-line prefer-const -- assigned after handler definitions below
|
|
60
61
|
let savedHandlers: {
|
|
61
62
|
onopen: typeof ws.onopen;
|
|
62
63
|
onmessage: typeof ws.onmessage;
|
package/data/git/queries.ts
CHANGED
|
@@ -6,7 +6,6 @@ export { gitKeys };
|
|
|
6
6
|
import type { CommitSummary, CommitDetail } from "@/lib/git-history";
|
|
7
7
|
import type { GitStatus } from "@/lib/git-status";
|
|
8
8
|
import type { MultiRepoGitStatus } from "@/lib/multi-repo-git";
|
|
9
|
-
import type { ProjectRepository } from "@/lib/db";
|
|
10
9
|
|
|
11
10
|
export interface PRInfo {
|
|
12
11
|
number: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, ChildProcess } from "child_process";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
import { StreamParser } from "./stream-parser";
|
|
4
|
-
import { queries
|
|
4
|
+
import { queries } from "../db";
|
|
5
5
|
import type { ClaudeSessionOptions, ClientEvent } from "./types";
|
|
6
6
|
|
|
7
7
|
interface ManagedSession {
|
package/lib/code-search.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, spawnSync } from "child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Check if ripgrep is available on the system
|
|
@@ -46,13 +46,13 @@ export function searchCode(
|
|
|
46
46
|
const {
|
|
47
47
|
maxResults = 100,
|
|
48
48
|
contextLines = 2,
|
|
49
|
-
filePattern = "*",
|
|
50
|
-
caseSensitive = false,
|
|
49
|
+
filePattern: _filePattern = "*",
|
|
50
|
+
caseSensitive: _caseSensitive = false,
|
|
51
51
|
} = options;
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
// Use spawn instead of execSync for better control
|
|
55
|
-
|
|
55
|
+
// spawnSync imported at top level
|
|
56
56
|
|
|
57
57
|
const args = [
|
|
58
58
|
"--json",
|
|
@@ -100,7 +100,7 @@ export function searchCode(
|
|
|
100
100
|
} catch (error) {
|
|
101
101
|
console.error("Error in searchCode:", error);
|
|
102
102
|
// ENOENT = command not found
|
|
103
|
-
if ((error as
|
|
103
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
104
104
|
throw new Error(
|
|
105
105
|
"ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
106
106
|
);
|
package/lib/db/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import os from "os";
|
|
4
5
|
import { createSchema } from "./schema";
|
|
@@ -14,7 +15,6 @@ let _db: Database.Database | null = null;
|
|
|
14
15
|
|
|
15
16
|
export function getDb(): Database.Database {
|
|
16
17
|
if (!_db) {
|
|
17
|
-
const fs = require("fs");
|
|
18
18
|
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
19
19
|
|
|
20
20
|
_db = new Database(DB_PATH);
|
package/lib/git-history.ts
CHANGED
package/lib/git.ts
CHANGED
package/lib/multi-repo-git.ts
CHANGED
package/lib/projects.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Sessions inherit settings from their parent project.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
9
8
|
import fs from "fs";
|
|
10
9
|
import path from "path";
|
|
11
10
|
import { exec } from "child_process";
|
|
@@ -187,7 +186,9 @@ export async function getAllProjects(): Promise<Project[]> {
|
|
|
187
186
|
/**
|
|
188
187
|
* Get all projects with their dev server configurations
|
|
189
188
|
*/
|
|
190
|
-
export async function getAllProjectsWithDevServers(): Promise<
|
|
189
|
+
export async function getAllProjectsWithDevServers(): Promise<
|
|
190
|
+
ProjectWithRepositories[]
|
|
191
|
+
> {
|
|
191
192
|
const projects = await getAllProjects();
|
|
192
193
|
const result: ProjectWithRepositories[] = [];
|
|
193
194
|
for (const p of projects) {
|
|
@@ -242,7 +243,10 @@ export async function updateProject(
|
|
|
242
243
|
/**
|
|
243
244
|
* Toggle project expanded state
|
|
244
245
|
*/
|
|
245
|
-
export async function toggleProjectExpanded(
|
|
246
|
+
export async function toggleProjectExpanded(
|
|
247
|
+
id: string,
|
|
248
|
+
expanded: boolean
|
|
249
|
+
): Promise<void> {
|
|
246
250
|
await queries.updateProjectExpanded(expanded, id);
|
|
247
251
|
}
|
|
248
252
|
|
|
@@ -273,7 +277,9 @@ export async function deleteProject(id: string): Promise<boolean> {
|
|
|
273
277
|
/**
|
|
274
278
|
* Get sessions for a project
|
|
275
279
|
*/
|
|
276
|
-
export async function getProjectSessions(
|
|
280
|
+
export async function getProjectSessions(
|
|
281
|
+
projectId: string
|
|
282
|
+
): Promise<Session[]> {
|
|
277
283
|
return queries.getSessionsByProject(projectId);
|
|
278
284
|
}
|
|
279
285
|
|
|
@@ -478,7 +484,9 @@ export function validateWorkingDirectory(dir: string): boolean {
|
|
|
478
484
|
/**
|
|
479
485
|
* Get repositories for a project
|
|
480
486
|
*/
|
|
481
|
-
export async function getProjectRepositories(
|
|
487
|
+
export async function getProjectRepositories(
|
|
488
|
+
projectId: string
|
|
489
|
+
): Promise<ProjectRepository[]> {
|
|
482
490
|
const rawRepos = await queries.getProjectRepositories(projectId);
|
|
483
491
|
return rawRepos.map((r) => ({
|
|
484
492
|
...r,
|
|
@@ -509,14 +517,23 @@ export async function addProjectRepository(
|
|
|
509
517
|
for (const repo of existing) {
|
|
510
518
|
if (repo.is_primary) {
|
|
511
519
|
await queries.updateProjectRepository(
|
|
512
|
-
repo.name,
|
|
520
|
+
repo.name,
|
|
521
|
+
repo.path,
|
|
522
|
+
false,
|
|
523
|
+
repo.sort_order,
|
|
524
|
+
repo.id
|
|
513
525
|
);
|
|
514
526
|
}
|
|
515
527
|
}
|
|
516
528
|
}
|
|
517
529
|
|
|
518
530
|
await queries.createProjectRepository(
|
|
519
|
-
id,
|
|
531
|
+
id,
|
|
532
|
+
projectId,
|
|
533
|
+
opts.name,
|
|
534
|
+
opts.path,
|
|
535
|
+
isPrimary,
|
|
536
|
+
maxOrder + 1
|
|
520
537
|
);
|
|
521
538
|
|
|
522
539
|
const raw = (await queries.getProjectRepository(id))!;
|
|
@@ -549,7 +566,11 @@ export async function updateProjectRepository(
|
|
|
549
566
|
for (const repo of allRepos) {
|
|
550
567
|
if (repo.is_primary && repo.id !== id) {
|
|
551
568
|
await queries.updateProjectRepository(
|
|
552
|
-
repo.name,
|
|
569
|
+
repo.name,
|
|
570
|
+
repo.path,
|
|
571
|
+
false,
|
|
572
|
+
repo.sort_order,
|
|
573
|
+
repo.id
|
|
553
574
|
);
|
|
554
575
|
}
|
|
555
576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atercates/claude-deck",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Self-hosted web UI for managing Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-deck": "./scripts/claude-deck"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/
|
|
31
|
+
"url": "git+https://github.com/ATERCATES/claude-deck.git"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"claude",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"node": ">=24"
|
|
44
44
|
},
|
|
45
45
|
"bugs": {
|
|
46
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/ATERCATES/claude-deck/issues"
|
|
47
47
|
},
|
|
48
|
-
"homepage": "https://github.com/
|
|
48
|
+
"homepage": "https://github.com/ATERCATES/claude-deck#readme",
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@anthropic-ai/claude-agent-sdk": "^0.2.104",
|
|
51
51
|
"@codemirror/lang-css": "^6.3.1",
|
package/scripts/agent-os
CHANGED
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# Configuration
|
|
10
10
|
AGENT_OS_HOME="${AGENT_OS_HOME:-$HOME/.agent-os}"
|
|
11
11
|
PORT="${AGENT_OS_PORT:-3011}"
|
|
12
|
-
REPO_URL="https://github.com/
|
|
12
|
+
REPO_URL="https://github.com/ATERCATES/agent-os.git"
|
|
13
13
|
|
|
14
14
|
# Derived paths
|
|
15
15
|
REPO_DIR="$AGENT_OS_HOME/repo"
|
package/scripts/install.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ClaudeDeck Installer
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# curl -fsSL https://raw.githubusercontent.com/
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/ATERCATES/claude-deck/main/scripts/install.sh | bash
|
|
7
7
|
#
|
|
8
8
|
|
|
9
9
|
set -e
|
|
@@ -19,7 +19,7 @@ log_info() { echo -e "${BLUE}==>${NC} $1"; }
|
|
|
19
19
|
log_success() { echo -e "${GREEN}==>${NC} $1"; }
|
|
20
20
|
log_error() { echo -e "${RED}==>${NC} $1"; }
|
|
21
21
|
|
|
22
|
-
REPO_URL="https://github.com/
|
|
22
|
+
REPO_URL="https://github.com/ATERCATES/claude-deck.git"
|
|
23
23
|
INSTALL_DIR="$HOME/.claude-deck/repo"
|
|
24
24
|
|
|
25
25
|
echo ""
|
package/server.ts
CHANGED
|
@@ -49,7 +49,7 @@ app.prepare().then(async () => {
|
|
|
49
49
|
|
|
50
50
|
// Heartbeat: ping every 30s, kill if no pong in 10s
|
|
51
51
|
const HEARTBEAT_INTERVAL = 30000;
|
|
52
|
-
const
|
|
52
|
+
const _HEARTBEAT_TIMEOUT = 10000;
|
|
53
53
|
|
|
54
54
|
function setupHeartbeat(ws: WebSocket) {
|
|
55
55
|
let alive = true;
|