@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
package/lib/projects.ts
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects Module
|
|
3
|
+
*
|
|
4
|
+
* Projects are workspaces that contain sessions and dev server configurations.
|
|
5
|
+
* Sessions inherit settings from their parent project.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { exec } from "child_process";
|
|
12
|
+
import { promisify } from "util";
|
|
13
|
+
import {
|
|
14
|
+
queries,
|
|
15
|
+
type Project,
|
|
16
|
+
type ProjectDevServer,
|
|
17
|
+
type ProjectRepository,
|
|
18
|
+
type Session,
|
|
19
|
+
type DevServerType,
|
|
20
|
+
} from "./db";
|
|
21
|
+
import type { AgentType } from "./providers";
|
|
22
|
+
|
|
23
|
+
const execAsync = promisify(exec);
|
|
24
|
+
|
|
25
|
+
export interface CreateProjectOptions {
|
|
26
|
+
name: string;
|
|
27
|
+
workingDirectory: string;
|
|
28
|
+
agentType?: AgentType;
|
|
29
|
+
defaultModel?: string;
|
|
30
|
+
initialPrompt?: string;
|
|
31
|
+
devServers?: CreateDevServerOptions[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CreateDevServerOptions {
|
|
35
|
+
name: string;
|
|
36
|
+
type: DevServerType;
|
|
37
|
+
command: string;
|
|
38
|
+
port?: number;
|
|
39
|
+
portEnvVar?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DetectedDevServer {
|
|
43
|
+
name: string;
|
|
44
|
+
type: DevServerType;
|
|
45
|
+
command: string;
|
|
46
|
+
port?: number;
|
|
47
|
+
portEnvVar?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CreateRepositoryOptions {
|
|
51
|
+
name: string;
|
|
52
|
+
path: string;
|
|
53
|
+
isPrimary?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ProjectWithDevServers extends Project {
|
|
57
|
+
devServers: ProjectDevServer[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ProjectWithRepositories extends ProjectWithDevServers {
|
|
61
|
+
repositories: ProjectRepository[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Generate project ID
|
|
65
|
+
function generateProjectId(): string {
|
|
66
|
+
return `proj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate dev server config ID
|
|
70
|
+
function generateDevServerId(): string {
|
|
71
|
+
return `pds_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate repository config ID
|
|
75
|
+
function generateRepositoryId(): string {
|
|
76
|
+
return `repo_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new project
|
|
81
|
+
*/
|
|
82
|
+
export async function createProject(
|
|
83
|
+
opts: CreateProjectOptions
|
|
84
|
+
): Promise<ProjectWithRepositories> {
|
|
85
|
+
const id = generateProjectId();
|
|
86
|
+
|
|
87
|
+
// Get next sort order
|
|
88
|
+
const projects = await queries.getAllProjects();
|
|
89
|
+
const maxOrder = projects.reduce((max, p) => Math.max(max, p.sort_order), 0);
|
|
90
|
+
|
|
91
|
+
await queries.createProject(
|
|
92
|
+
id,
|
|
93
|
+
opts.name,
|
|
94
|
+
opts.workingDirectory,
|
|
95
|
+
opts.agentType || "claude",
|
|
96
|
+
opts.defaultModel || "sonnet",
|
|
97
|
+
opts.initialPrompt || null,
|
|
98
|
+
maxOrder + 1
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Create dev server configs if provided
|
|
102
|
+
const devServers: ProjectDevServer[] = [];
|
|
103
|
+
if (opts.devServers) {
|
|
104
|
+
for (let i = 0; i < opts.devServers.length; i++) {
|
|
105
|
+
const ds = opts.devServers[i];
|
|
106
|
+
const dsId = generateDevServerId();
|
|
107
|
+
await queries.createProjectDevServer(
|
|
108
|
+
dsId,
|
|
109
|
+
id,
|
|
110
|
+
ds.name,
|
|
111
|
+
ds.type,
|
|
112
|
+
ds.command,
|
|
113
|
+
ds.port || null,
|
|
114
|
+
ds.portEnvVar || null,
|
|
115
|
+
i
|
|
116
|
+
);
|
|
117
|
+
devServers.push({
|
|
118
|
+
id: dsId,
|
|
119
|
+
project_id: id,
|
|
120
|
+
name: ds.name,
|
|
121
|
+
type: ds.type,
|
|
122
|
+
command: ds.command,
|
|
123
|
+
port: ds.port || null,
|
|
124
|
+
port_env_var: ds.portEnvVar || null,
|
|
125
|
+
sort_order: i,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const project = await queries.getProject(id);
|
|
131
|
+
return {
|
|
132
|
+
...project!,
|
|
133
|
+
expanded: Boolean(project!.expanded),
|
|
134
|
+
is_uncategorized: Boolean(project!.is_uncategorized),
|
|
135
|
+
devServers,
|
|
136
|
+
repositories: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get a project by ID
|
|
142
|
+
*/
|
|
143
|
+
export async function getProject(id: string): Promise<Project | undefined> {
|
|
144
|
+
const project = await queries.getProject(id);
|
|
145
|
+
if (!project) return undefined;
|
|
146
|
+
return {
|
|
147
|
+
...project,
|
|
148
|
+
expanded: Boolean(project.expanded),
|
|
149
|
+
is_uncategorized: Boolean(project.is_uncategorized),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get a project with its dev server configurations
|
|
155
|
+
*/
|
|
156
|
+
export async function getProjectWithDevServers(
|
|
157
|
+
id: string
|
|
158
|
+
): Promise<ProjectWithRepositories | undefined> {
|
|
159
|
+
const project = await getProject(id);
|
|
160
|
+
if (!project) return undefined;
|
|
161
|
+
|
|
162
|
+
const devServers = await queries.getProjectDevServers(id);
|
|
163
|
+
const rawRepos = await queries.getProjectRepositories(id);
|
|
164
|
+
const repositories = rawRepos.map((r) => ({
|
|
165
|
+
...r,
|
|
166
|
+
is_primary: Boolean(r.is_primary),
|
|
167
|
+
}));
|
|
168
|
+
return {
|
|
169
|
+
...project,
|
|
170
|
+
devServers,
|
|
171
|
+
repositories,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all projects (sorted by sort_order, with uncategorized last)
|
|
177
|
+
*/
|
|
178
|
+
export async function getAllProjects(): Promise<Project[]> {
|
|
179
|
+
const projects = await queries.getAllProjects();
|
|
180
|
+
return projects.map((p) => ({
|
|
181
|
+
...p,
|
|
182
|
+
expanded: Boolean(p.expanded),
|
|
183
|
+
is_uncategorized: Boolean(p.is_uncategorized),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get all projects with their dev server configurations
|
|
189
|
+
*/
|
|
190
|
+
export async function getAllProjectsWithDevServers(): Promise<ProjectWithRepositories[]> {
|
|
191
|
+
const projects = await getAllProjects();
|
|
192
|
+
const result: ProjectWithRepositories[] = [];
|
|
193
|
+
for (const p of projects) {
|
|
194
|
+
const devServers = await queries.getProjectDevServers(p.id);
|
|
195
|
+
const rawRepos = await queries.getProjectRepositories(p.id);
|
|
196
|
+
const repositories = rawRepos.map((r) => ({
|
|
197
|
+
...r,
|
|
198
|
+
is_primary: Boolean(r.is_primary),
|
|
199
|
+
}));
|
|
200
|
+
result.push({
|
|
201
|
+
...p,
|
|
202
|
+
devServers,
|
|
203
|
+
repositories,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Update a project's settings
|
|
211
|
+
*/
|
|
212
|
+
export async function updateProject(
|
|
213
|
+
id: string,
|
|
214
|
+
updates: Partial<
|
|
215
|
+
Pick<
|
|
216
|
+
Project,
|
|
217
|
+
| "name"
|
|
218
|
+
| "working_directory"
|
|
219
|
+
| "agent_type"
|
|
220
|
+
| "default_model"
|
|
221
|
+
| "initial_prompt"
|
|
222
|
+
>
|
|
223
|
+
>
|
|
224
|
+
): Promise<Project | undefined> {
|
|
225
|
+
const project = await getProject(id);
|
|
226
|
+
if (!project || project.is_uncategorized) return undefined;
|
|
227
|
+
|
|
228
|
+
await queries.updateProject(
|
|
229
|
+
updates.name ?? project.name,
|
|
230
|
+
updates.working_directory ?? project.working_directory,
|
|
231
|
+
updates.agent_type ?? project.agent_type,
|
|
232
|
+
updates.default_model ?? project.default_model,
|
|
233
|
+
updates.initial_prompt !== undefined
|
|
234
|
+
? updates.initial_prompt
|
|
235
|
+
: project.initial_prompt,
|
|
236
|
+
id
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return getProject(id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Toggle project expanded state
|
|
244
|
+
*/
|
|
245
|
+
export async function toggleProjectExpanded(id: string, expanded: boolean): Promise<void> {
|
|
246
|
+
await queries.updateProjectExpanded(expanded, id);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Delete a project (moves sessions to Uncategorized)
|
|
251
|
+
*/
|
|
252
|
+
export async function deleteProject(id: string): Promise<boolean> {
|
|
253
|
+
const project = await getProject(id);
|
|
254
|
+
if (!project || project.is_uncategorized) return false;
|
|
255
|
+
|
|
256
|
+
// Move all sessions to Uncategorized
|
|
257
|
+
const sessions = await queries.getSessionsByProject(id);
|
|
258
|
+
for (const session of sessions) {
|
|
259
|
+
await queries.updateSessionProject("uncategorized", session.id);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Delete dev server instances
|
|
263
|
+
await queries.deleteDevServersByProject(id);
|
|
264
|
+
|
|
265
|
+
// Delete dev server configs (templates)
|
|
266
|
+
await queries.deleteProjectDevServers(id);
|
|
267
|
+
|
|
268
|
+
// Delete project
|
|
269
|
+
await queries.deleteProject(id);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get sessions for a project
|
|
275
|
+
*/
|
|
276
|
+
export async function getProjectSessions(projectId: string): Promise<Session[]> {
|
|
277
|
+
return queries.getSessionsByProject(projectId);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Move a session to a project
|
|
282
|
+
*/
|
|
283
|
+
export async function moveSessionToProject(
|
|
284
|
+
sessionId: string,
|
|
285
|
+
projectId: string
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
await queries.updateSessionProject(projectId, sessionId);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Add a dev server configuration to a project
|
|
292
|
+
*/
|
|
293
|
+
export async function addProjectDevServer(
|
|
294
|
+
projectId: string,
|
|
295
|
+
opts: CreateDevServerOptions
|
|
296
|
+
): Promise<ProjectDevServer> {
|
|
297
|
+
const id = generateDevServerId();
|
|
298
|
+
|
|
299
|
+
// Get next sort order
|
|
300
|
+
const existing = await queries.getProjectDevServers(projectId);
|
|
301
|
+
const maxOrder = existing.reduce(
|
|
302
|
+
(max, ds) => Math.max(max, ds.sort_order),
|
|
303
|
+
-1
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
await queries.createProjectDevServer(
|
|
307
|
+
id,
|
|
308
|
+
projectId,
|
|
309
|
+
opts.name,
|
|
310
|
+
opts.type,
|
|
311
|
+
opts.command,
|
|
312
|
+
opts.port || null,
|
|
313
|
+
opts.portEnvVar || null,
|
|
314
|
+
maxOrder + 1
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return (await queries.getProjectDevServer(id))!;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Update a dev server configuration
|
|
322
|
+
*/
|
|
323
|
+
export async function updateProjectDevServer(
|
|
324
|
+
id: string,
|
|
325
|
+
updates: Partial<CreateDevServerOptions & { sortOrder?: number }>
|
|
326
|
+
): Promise<ProjectDevServer | undefined> {
|
|
327
|
+
const existing = await queries.getProjectDevServer(id);
|
|
328
|
+
if (!existing) return undefined;
|
|
329
|
+
|
|
330
|
+
await queries.updateProjectDevServer(
|
|
331
|
+
updates.name ?? existing.name,
|
|
332
|
+
updates.type ?? existing.type,
|
|
333
|
+
updates.command ?? existing.command,
|
|
334
|
+
updates.port ?? existing.port,
|
|
335
|
+
updates.portEnvVar ?? existing.port_env_var,
|
|
336
|
+
updates.sortOrder ?? existing.sort_order,
|
|
337
|
+
id
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return (await queries.getProjectDevServer(id))!;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Delete a dev server configuration
|
|
345
|
+
*/
|
|
346
|
+
export async function deleteProjectDevServer(id: string): Promise<void> {
|
|
347
|
+
await queries.deleteProjectDevServer(id);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Detect available npm scripts from package.json
|
|
352
|
+
*/
|
|
353
|
+
export async function detectNpmScripts(
|
|
354
|
+
workingDir: string
|
|
355
|
+
): Promise<DetectedDevServer[]> {
|
|
356
|
+
const expandedDir = workingDir.replace(/^~/, process.env.HOME || "~");
|
|
357
|
+
const packageJsonPath = path.join(expandedDir, "package.json");
|
|
358
|
+
|
|
359
|
+
if (!fs.existsSync(packageJsonPath)) return [];
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
363
|
+
const scripts = packageJson.scripts || {};
|
|
364
|
+
const detected: DetectedDevServer[] = [];
|
|
365
|
+
|
|
366
|
+
// Common dev server scripts to look for
|
|
367
|
+
const devScripts = [
|
|
368
|
+
"dev",
|
|
369
|
+
"start",
|
|
370
|
+
"serve",
|
|
371
|
+
"develop",
|
|
372
|
+
"preview",
|
|
373
|
+
"start:dev",
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
for (const script of devScripts) {
|
|
377
|
+
if (scripts[script]) {
|
|
378
|
+
const scriptContent: string = scripts[script];
|
|
379
|
+
|
|
380
|
+
// Try to detect port from script
|
|
381
|
+
let port: number | undefined;
|
|
382
|
+
const portMatch = scriptContent.match(/(?:port|PORT)[=\s]+(\d+)/i);
|
|
383
|
+
if (portMatch) {
|
|
384
|
+
port = parseInt(portMatch[1], 10);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Detect port env var from common patterns
|
|
388
|
+
let portEnvVar: string | undefined;
|
|
389
|
+
if (
|
|
390
|
+
scriptContent.includes("$PORT") ||
|
|
391
|
+
scriptContent.includes("${PORT}")
|
|
392
|
+
) {
|
|
393
|
+
portEnvVar = "PORT";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
detected.push({
|
|
397
|
+
name: `npm run ${script}`,
|
|
398
|
+
type: "node",
|
|
399
|
+
command: `npm run ${script}`,
|
|
400
|
+
port,
|
|
401
|
+
portEnvVar,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return detected;
|
|
407
|
+
} catch {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Detect Docker Compose services
|
|
414
|
+
*/
|
|
415
|
+
export async function detectDockerServices(
|
|
416
|
+
workingDir: string
|
|
417
|
+
): Promise<DetectedDevServer[]> {
|
|
418
|
+
const expandedDir = workingDir.replace(/^~/, process.env.HOME || "~");
|
|
419
|
+
const composeFiles = [
|
|
420
|
+
"docker-compose.yml",
|
|
421
|
+
"docker-compose.yaml",
|
|
422
|
+
"compose.yml",
|
|
423
|
+
"compose.yaml",
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
for (const file of composeFiles) {
|
|
427
|
+
const composePath = path.join(expandedDir, file);
|
|
428
|
+
if (fs.existsSync(composePath)) {
|
|
429
|
+
try {
|
|
430
|
+
const { stdout } = await execAsync(
|
|
431
|
+
`docker compose -f "${file}" config --services 2>/dev/null || echo ""`,
|
|
432
|
+
{ cwd: expandedDir }
|
|
433
|
+
);
|
|
434
|
+
const services = stdout.trim().split("\n").filter(Boolean);
|
|
435
|
+
|
|
436
|
+
return services.map((service) => ({
|
|
437
|
+
name: service,
|
|
438
|
+
type: "docker" as const,
|
|
439
|
+
command: service,
|
|
440
|
+
}));
|
|
441
|
+
} catch {
|
|
442
|
+
// Docker not available or compose file invalid
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Detect all available dev servers in a directory
|
|
452
|
+
*/
|
|
453
|
+
export async function detectDevServers(
|
|
454
|
+
workingDir: string
|
|
455
|
+
): Promise<DetectedDevServer[]> {
|
|
456
|
+
const [npmScripts, dockerServices] = await Promise.all([
|
|
457
|
+
detectNpmScripts(workingDir),
|
|
458
|
+
detectDockerServices(workingDir),
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
return [...npmScripts, ...dockerServices];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Validate a working directory exists
|
|
466
|
+
*/
|
|
467
|
+
export function validateWorkingDirectory(dir: string): boolean {
|
|
468
|
+
const expandedDir = dir.replace(/^~/, process.env.HOME || "~");
|
|
469
|
+
try {
|
|
470
|
+
return fs.existsSync(expandedDir) && fs.statSync(expandedDir).isDirectory();
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============= Repository Management =============
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get repositories for a project
|
|
480
|
+
*/
|
|
481
|
+
export async function getProjectRepositories(projectId: string): Promise<ProjectRepository[]> {
|
|
482
|
+
const rawRepos = await queries.getProjectRepositories(projectId);
|
|
483
|
+
return rawRepos.map((r) => ({
|
|
484
|
+
...r,
|
|
485
|
+
is_primary: Boolean(r.is_primary),
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Add a repository to a project
|
|
491
|
+
*/
|
|
492
|
+
export async function addProjectRepository(
|
|
493
|
+
projectId: string,
|
|
494
|
+
opts: CreateRepositoryOptions
|
|
495
|
+
): Promise<ProjectRepository> {
|
|
496
|
+
const id = generateRepositoryId();
|
|
497
|
+
|
|
498
|
+
// Get next sort order
|
|
499
|
+
const existing = await getProjectRepositories(projectId);
|
|
500
|
+
const maxOrder = existing.reduce(
|
|
501
|
+
(max, repo) => Math.max(max, repo.sort_order),
|
|
502
|
+
-1
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// If this is the first repository or marked as primary, ensure no other is primary
|
|
506
|
+
const isPrimary = opts.isPrimary || existing.length === 0;
|
|
507
|
+
if (isPrimary) {
|
|
508
|
+
// Clear primary flag from other repositories
|
|
509
|
+
for (const repo of existing) {
|
|
510
|
+
if (repo.is_primary) {
|
|
511
|
+
await queries.updateProjectRepository(
|
|
512
|
+
repo.name, repo.path, false, repo.sort_order, repo.id
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await queries.createProjectRepository(
|
|
519
|
+
id, projectId, opts.name, opts.path, isPrimary, maxOrder + 1
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const raw = (await queries.getProjectRepository(id))!;
|
|
523
|
+
return {
|
|
524
|
+
...raw,
|
|
525
|
+
is_primary: Boolean(raw.is_primary),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Update a repository
|
|
531
|
+
*/
|
|
532
|
+
export async function updateProjectRepository(
|
|
533
|
+
id: string,
|
|
534
|
+
updates: Partial<CreateRepositoryOptions & { sortOrder?: number }>
|
|
535
|
+
): Promise<ProjectRepository | undefined> {
|
|
536
|
+
const raw = await queries.getProjectRepository(id);
|
|
537
|
+
if (!raw) return undefined;
|
|
538
|
+
|
|
539
|
+
const existing = {
|
|
540
|
+
...raw,
|
|
541
|
+
is_primary: Boolean(raw.is_primary),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// If setting as primary, clear other primaries
|
|
545
|
+
const newIsPrimary =
|
|
546
|
+
updates.isPrimary !== undefined ? updates.isPrimary : existing.is_primary;
|
|
547
|
+
if (newIsPrimary && !existing.is_primary) {
|
|
548
|
+
const allRepos = await getProjectRepositories(existing.project_id);
|
|
549
|
+
for (const repo of allRepos) {
|
|
550
|
+
if (repo.is_primary && repo.id !== id) {
|
|
551
|
+
await queries.updateProjectRepository(
|
|
552
|
+
repo.name, repo.path, false, repo.sort_order, repo.id
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
await queries.updateProjectRepository(
|
|
559
|
+
updates.name ?? existing.name,
|
|
560
|
+
updates.path ?? existing.path,
|
|
561
|
+
newIsPrimary,
|
|
562
|
+
updates.sortOrder ?? existing.sort_order,
|
|
563
|
+
id
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const updatedRaw = (await queries.getProjectRepository(id))!;
|
|
567
|
+
return {
|
|
568
|
+
...updatedRaw,
|
|
569
|
+
is_primary: Boolean(updatedRaw.is_primary),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Delete a repository
|
|
575
|
+
*/
|
|
576
|
+
export async function deleteProjectRepository(id: string): Promise<void> {
|
|
577
|
+
await queries.deleteProjectRepository(id);
|
|
578
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const PROVIDER_IDS = ["claude"] as const;
|
|
2
|
+
|
|
3
|
+
export type ProviderId = (typeof PROVIDER_IDS)[number];
|
|
4
|
+
|
|
5
|
+
export interface ProviderDefinition {
|
|
6
|
+
id: ProviderId;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
cli: string;
|
|
10
|
+
configDir: string;
|
|
11
|
+
autoApproveFlag?: string;
|
|
12
|
+
supportsResume: boolean;
|
|
13
|
+
supportsFork: boolean;
|
|
14
|
+
resumeFlag?: string;
|
|
15
|
+
modelFlag?: string;
|
|
16
|
+
initialPromptFlag?: string;
|
|
17
|
+
defaultArgs?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PROVIDERS: ProviderDefinition[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "claude",
|
|
23
|
+
name: "Claude Code",
|
|
24
|
+
description: "Anthropic's official CLI",
|
|
25
|
+
cli: "claude",
|
|
26
|
+
configDir: "~/.claude",
|
|
27
|
+
autoApproveFlag: "--dangerously-skip-permissions",
|
|
28
|
+
supportsResume: true,
|
|
29
|
+
supportsFork: true,
|
|
30
|
+
resumeFlag: "--resume",
|
|
31
|
+
initialPromptFlag: "",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const PROVIDER_MAP = new Map<ProviderId, ProviderDefinition>(
|
|
36
|
+
PROVIDERS.map((provider) => [provider.id, provider])
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export function getProviderDefinition(id: ProviderId): ProviderDefinition {
|
|
40
|
+
const provider = PROVIDER_MAP.get(id);
|
|
41
|
+
if (!provider) {
|
|
42
|
+
throw new Error(`Unknown provider: ${id}`);
|
|
43
|
+
}
|
|
44
|
+
return provider;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getAllProviderDefinitions(): ProviderDefinition[] {
|
|
48
|
+
return PROVIDERS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isValidProviderId(value: string): value is ProviderId {
|
|
52
|
+
return PROVIDER_MAP.has(value as ProviderId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getManagedSessionPattern(): RegExp {
|
|
56
|
+
return /^claude-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getProviderIdFromSessionName(
|
|
60
|
+
sessionName: string
|
|
61
|
+
): ProviderId | null {
|
|
62
|
+
if (sessionName.startsWith("claude-")) {
|
|
63
|
+
return "claude";
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSessionIdFromName(sessionName: string): string {
|
|
69
|
+
return sessionName.replace(/^claude-/i, "");
|
|
70
|
+
}
|