@atercates/claude-deck 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/api/claude/hidden/route.ts +66 -0
  4. package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
  5. package/app/api/claude/projects/route.ts +44 -0
  6. package/app/api/code-search/available/route.ts +12 -0
  7. package/app/api/code-search/route.ts +47 -0
  8. package/app/api/dev-servers/[id]/logs/route.ts +23 -0
  9. package/app/api/dev-servers/[id]/restart/route.ts +20 -0
  10. package/app/api/dev-servers/[id]/route.ts +51 -0
  11. package/app/api/dev-servers/[id]/stop/route.ts +20 -0
  12. package/app/api/dev-servers/detect/route.ts +39 -0
  13. package/app/api/dev-servers/route.ts +48 -0
  14. package/app/api/exec/route.ts +60 -0
  15. package/app/api/files/content/route.ts +76 -0
  16. package/app/api/files/route.ts +37 -0
  17. package/app/api/files/upload-temp/route.ts +41 -0
  18. package/app/api/git/check/route.ts +54 -0
  19. package/app/api/git/clone/route.ts +99 -0
  20. package/app/api/git/commit/route.ts +75 -0
  21. package/app/api/git/discard/route.ts +38 -0
  22. package/app/api/git/file-content/route.ts +64 -0
  23. package/app/api/git/history/[hash]/diff/route.ts +38 -0
  24. package/app/api/git/history/[hash]/route.ts +34 -0
  25. package/app/api/git/history/route.ts +27 -0
  26. package/app/api/git/multi-status/route.ts +46 -0
  27. package/app/api/git/pr/route.ts +164 -0
  28. package/app/api/git/push/route.ts +64 -0
  29. package/app/api/git/stage/route.ts +40 -0
  30. package/app/api/git/status/route.ts +51 -0
  31. package/app/api/git/unstage/route.ts +46 -0
  32. package/app/api/groups/[...path]/route.ts +136 -0
  33. package/app/api/groups/route.ts +93 -0
  34. package/app/api/orchestrate/spawn/route.ts +45 -0
  35. package/app/api/orchestrate/workers/[id]/route.ts +89 -0
  36. package/app/api/orchestrate/workers/route.ts +31 -0
  37. package/app/api/projects/[id]/detect/route.ts +27 -0
  38. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
  39. package/app/api/projects/[id]/dev-servers/route.ts +51 -0
  40. package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
  41. package/app/api/projects/[id]/repositories/route.ts +74 -0
  42. package/app/api/projects/[id]/route.ts +108 -0
  43. package/app/api/projects/detect/route.ts +33 -0
  44. package/app/api/projects/route.ts +59 -0
  45. package/app/api/sessions/[id]/claude-session/route.ts +42 -0
  46. package/app/api/sessions/[id]/fork/route.ts +74 -0
  47. package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
  48. package/app/api/sessions/[id]/messages/route.ts +60 -0
  49. package/app/api/sessions/[id]/pr/route.ts +188 -0
  50. package/app/api/sessions/[id]/preview/route.ts +42 -0
  51. package/app/api/sessions/[id]/route.ts +229 -0
  52. package/app/api/sessions/[id]/send-keys/route.ts +119 -0
  53. package/app/api/sessions/[id]/summarize/route.ts +331 -0
  54. package/app/api/sessions/init-script/route.ts +84 -0
  55. package/app/api/sessions/route.ts +209 -0
  56. package/app/api/sessions/status/route.ts +237 -0
  57. package/app/api/system/route.ts +9 -0
  58. package/app/api/tmux/kill-all/route.ts +57 -0
  59. package/app/api/tmux/rename/route.ts +30 -0
  60. package/app/globals.css +174 -0
  61. package/app/icon.svg +11 -0
  62. package/app/layout.tsx +122 -0
  63. package/app/page.tsx +629 -0
  64. package/components/ChatMessage.tsx +65 -0
  65. package/components/ChatView.tsx +276 -0
  66. package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
  67. package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
  68. package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
  69. package/components/ClaudeProjects/index.ts +1 -0
  70. package/components/CodeSearch/CodeSearchResults.tsx +177 -0
  71. package/components/ConductorPanel.tsx +256 -0
  72. package/components/DevServers/DevServerCard.tsx +311 -0
  73. package/components/DevServers/DevServersSection.tsx +91 -0
  74. package/components/DevServers/ServerLogsModal.tsx +151 -0
  75. package/components/DevServers/StartServerDialog.tsx +359 -0
  76. package/components/DevServers/index.ts +4 -0
  77. package/components/DiffViewer/DiffModal.tsx +151 -0
  78. package/components/DiffViewer/UnifiedDiff.tsx +185 -0
  79. package/components/DiffViewer/index.tsx +2 -0
  80. package/components/DirectoryPicker.tsx +355 -0
  81. package/components/FileExplorer/FileEditor.tsx +276 -0
  82. package/components/FileExplorer/FileTabs.tsx +118 -0
  83. package/components/FileExplorer/FileTree.tsx +214 -0
  84. package/components/FileExplorer/HtmlRenderer.tsx +16 -0
  85. package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
  86. package/components/FileExplorer/index.tsx +520 -0
  87. package/components/FilePicker.tsx +339 -0
  88. package/components/FolderPicker.tsx +201 -0
  89. package/components/GitDrawer/FileEditDialog.tsx +400 -0
  90. package/components/GitDrawer/index.tsx +464 -0
  91. package/components/GitPanel/CommitForm.tsx +205 -0
  92. package/components/GitPanel/CommitHistory.tsx +174 -0
  93. package/components/GitPanel/CommitItem.tsx +196 -0
  94. package/components/GitPanel/FileChanges.tsx +414 -0
  95. package/components/GitPanel/GitPanelTabs.tsx +39 -0
  96. package/components/GitPanel/index.tsx +817 -0
  97. package/components/MessageInput.tsx +82 -0
  98. package/components/NewClaudeSessionDialog.tsx +166 -0
  99. package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
  100. package/components/NewSessionDialog/AgentSelector.tsx +37 -0
  101. package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
  102. package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
  103. package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
  104. package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
  105. package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
  106. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
  107. package/components/NewSessionDialog/index.tsx +106 -0
  108. package/components/NotificationSettings.tsx +127 -0
  109. package/components/PRCreationModal.tsx +272 -0
  110. package/components/Pane/DesktopTabBar.tsx +353 -0
  111. package/components/Pane/MobileTabBar.tsx +210 -0
  112. package/components/Pane/OpenInVSCode.tsx +69 -0
  113. package/components/Pane/PaneSkeletons.tsx +57 -0
  114. package/components/Pane/index.tsx +558 -0
  115. package/components/PaneLayout.tsx +60 -0
  116. package/components/Projects/DevServersSection.tsx +140 -0
  117. package/components/Projects/DirectoryField.tsx +92 -0
  118. package/components/Projects/NewProjectDialog.tsx +188 -0
  119. package/components/Projects/NewProjectDialog.types.ts +46 -0
  120. package/components/Projects/ProjectCard.tsx +276 -0
  121. package/components/Projects/ProjectSettingsDialog.tsx +811 -0
  122. package/components/Projects/hooks/useNewProjectForm.ts +249 -0
  123. package/components/Projects/index.ts +3 -0
  124. package/components/Providers.tsx +49 -0
  125. package/components/QuickSwitcher.tsx +306 -0
  126. package/components/SessionList/KillAllConfirm.tsx +46 -0
  127. package/components/SessionList/SelectionToolbar.tsx +164 -0
  128. package/components/SessionList/SessionList.types.ts +37 -0
  129. package/components/SessionList/SessionListHeader.tsx +71 -0
  130. package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
  131. package/components/SessionList/index.tsx +189 -0
  132. package/components/ShellDrawer/index.tsx +106 -0
  133. package/components/SidebarFooter.tsx +55 -0
  134. package/components/Terminal/KeybarToggleButton.tsx +45 -0
  135. package/components/Terminal/ScrollToBottomButton.tsx +32 -0
  136. package/components/Terminal/SearchBar.tsx +71 -0
  137. package/components/Terminal/TerminalToolbar.tsx +551 -0
  138. package/components/Terminal/VirtualKeyboard.tsx +711 -0
  139. package/components/Terminal/constants.ts +20 -0
  140. package/components/Terminal/hooks/index.ts +5 -0
  141. package/components/Terminal/hooks/resize-handlers.ts +140 -0
  142. package/components/Terminal/hooks/terminal-init.ts +151 -0
  143. package/components/Terminal/hooks/touch-scroll.ts +155 -0
  144. package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
  145. package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
  146. package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
  147. package/components/Terminal/hooks/websocket-connection.ts +274 -0
  148. package/components/Terminal/index.tsx +320 -0
  149. package/components/ThemeToggle.tsx +168 -0
  150. package/components/TmuxSessions.tsx +132 -0
  151. package/components/ToolCallDisplay.tsx +71 -0
  152. package/components/WorkerCard.tsx +245 -0
  153. package/components/a/ABadge.tsx +115 -0
  154. package/components/a/AButton.tsx +163 -0
  155. package/components/a/ADialog.tsx +93 -0
  156. package/components/a/ADropdownMenu.tsx +279 -0
  157. package/components/a/AIconButton.tsx +190 -0
  158. package/components/a/ASheet.tsx +150 -0
  159. package/components/a/ATooltip.tsx +77 -0
  160. package/components/a/index.ts +64 -0
  161. package/components/mobile/SwipeSidebar.tsx +122 -0
  162. package/components/ui/badge.tsx +41 -0
  163. package/components/ui/button.tsx +60 -0
  164. package/components/ui/context-menu.tsx +197 -0
  165. package/components/ui/dialog.tsx +143 -0
  166. package/components/ui/dropdown-menu.tsx +257 -0
  167. package/components/ui/input.tsx +21 -0
  168. package/components/ui/scroll-area.tsx +52 -0
  169. package/components/ui/select.tsx +159 -0
  170. package/components/ui/skeleton.tsx +111 -0
  171. package/components/ui/switch.tsx +31 -0
  172. package/components/ui/textarea.tsx +21 -0
  173. package/components/ui/tooltip.tsx +32 -0
  174. package/components/views/DesktopView.tsx +244 -0
  175. package/components/views/MobileView.tsx +110 -0
  176. package/components/views/types.ts +75 -0
  177. package/contexts/PaneContext.tsx +336 -0
  178. package/data/claude/index.ts +9 -0
  179. package/data/claude/keys.ts +6 -0
  180. package/data/claude/queries.ts +120 -0
  181. package/data/claude/useClaudeUpdates.ts +37 -0
  182. package/data/code-search/index.ts +2 -0
  183. package/data/code-search/keys.ts +7 -0
  184. package/data/code-search/queries.ts +61 -0
  185. package/data/dev-servers/index.ts +8 -0
  186. package/data/dev-servers/keys.ts +4 -0
  187. package/data/dev-servers/queries.ts +104 -0
  188. package/data/files/index.ts +3 -0
  189. package/data/files/keys.ts +4 -0
  190. package/data/files/queries.ts +25 -0
  191. package/data/git/keys.ts +15 -0
  192. package/data/git/queries.ts +395 -0
  193. package/data/groups/index.ts +1 -0
  194. package/data/groups/mutations.ts +95 -0
  195. package/data/projects/index.ts +10 -0
  196. package/data/projects/keys.ts +4 -0
  197. package/data/projects/queries.ts +193 -0
  198. package/data/repositories/index.ts +7 -0
  199. package/data/repositories/keys.ts +5 -0
  200. package/data/repositories/queries.ts +122 -0
  201. package/data/sessions/index.ts +12 -0
  202. package/data/sessions/keys.ts +8 -0
  203. package/data/sessions/queries.ts +218 -0
  204. package/data/statuses/index.ts +1 -0
  205. package/data/statuses/queries.ts +69 -0
  206. package/hooks/useCopyToClipboard.ts +48 -0
  207. package/hooks/useDevServersManager.ts +73 -0
  208. package/hooks/useDirectoryBrowser.ts +90 -0
  209. package/hooks/useDrawerAnimation.ts +27 -0
  210. package/hooks/useFileDrop.ts +87 -0
  211. package/hooks/useFileEditor.ts +184 -0
  212. package/hooks/useGroups.ts +37 -0
  213. package/hooks/useHomePath.ts +34 -0
  214. package/hooks/useKeyRepeat.ts +55 -0
  215. package/hooks/useKeybarVisibility.ts +42 -0
  216. package/hooks/useNotifications.ts +257 -0
  217. package/hooks/useProjects.ts +53 -0
  218. package/hooks/useSessionStatuses.ts +30 -0
  219. package/hooks/useSessions.ts +86 -0
  220. package/hooks/useSpeechRecognition.ts +124 -0
  221. package/hooks/useViewport.ts +32 -0
  222. package/hooks/useViewportHeight.ts +50 -0
  223. package/lib/async-operations.ts +35 -0
  224. package/lib/banner.ts +81 -0
  225. package/lib/claude/jsonl-cache.ts +86 -0
  226. package/lib/claude/jsonl-reader.ts +271 -0
  227. package/lib/claude/process-manager.ts +278 -0
  228. package/lib/claude/stream-parser.ts +173 -0
  229. package/lib/claude/types.ts +154 -0
  230. package/lib/claude/watcher.ts +71 -0
  231. package/lib/client/session-registry.ts +111 -0
  232. package/lib/code-search.ts +121 -0
  233. package/lib/db/index.ts +48 -0
  234. package/lib/db/migrations.ts +45 -0
  235. package/lib/db/queries.ts +460 -0
  236. package/lib/db/schema.ts +114 -0
  237. package/lib/db/types.ts +92 -0
  238. package/lib/db.ts +2 -0
  239. package/lib/dev-servers.ts +509 -0
  240. package/lib/diff-parser.ts +221 -0
  241. package/lib/env-setup.ts +285 -0
  242. package/lib/file-upload.ts +34 -0
  243. package/lib/file-utils.ts +50 -0
  244. package/lib/files.ts +207 -0
  245. package/lib/git-history.ts +294 -0
  246. package/lib/git-status.ts +391 -0
  247. package/lib/git.ts +257 -0
  248. package/lib/mcp-config.ts +81 -0
  249. package/lib/multi-repo-git.ts +179 -0
  250. package/lib/notifications.ts +219 -0
  251. package/lib/orchestration.ts +448 -0
  252. package/lib/panes.ts +232 -0
  253. package/lib/ports.ts +97 -0
  254. package/lib/pr-generation.ts +307 -0
  255. package/lib/pr.ts +234 -0
  256. package/lib/projects.ts +578 -0
  257. package/lib/providers/registry.ts +70 -0
  258. package/lib/providers.ts +121 -0
  259. package/lib/query-client.ts +14 -0
  260. package/lib/rangeSelectionUtils.ts +65 -0
  261. package/lib/status-detector.ts +375 -0
  262. package/lib/terminal-themes.ts +265 -0
  263. package/lib/theme-config.ts +327 -0
  264. package/lib/utils.ts +6 -0
  265. package/lib/worktrees.ts +262 -0
  266. package/mcp/orchestration-server.ts +438 -0
  267. package/package.json +139 -0
  268. package/postcss.config.mjs +7 -0
  269. package/public/icon.svg +10 -0
  270. package/public/icons/icon-128x128.png +0 -0
  271. package/public/icons/icon-144x144.png +0 -0
  272. package/public/icons/icon-152x152.png +0 -0
  273. package/public/icons/icon-192x192.png +0 -0
  274. package/public/icons/icon-384x384.png +0 -0
  275. package/public/icons/icon-512x512.png +0 -0
  276. package/public/icons/icon-72x72.png +0 -0
  277. package/public/icons/icon-96x96.png +0 -0
  278. package/public/manifest.json +61 -0
  279. package/public/sw.js +64 -0
  280. package/scripts/agent-os +91 -0
  281. package/scripts/install.sh +48 -0
  282. package/scripts/lib/ai-clis.sh +132 -0
  283. package/scripts/lib/commands.sh +487 -0
  284. package/scripts/lib/common.sh +89 -0
  285. package/scripts/lib/prerequisites.sh +462 -0
  286. package/scripts/setup.sh +134 -0
  287. package/server.ts +155 -0
  288. package/stores/fileOpen.ts +26 -0
  289. package/stores/index.ts +1 -0
  290. package/stores/initialPrompt.ts +24 -0
  291. package/stores/sessionSelection.ts +48 -0
  292. package/styles/themes.css +603 -0
  293. package/tsconfig.json +33 -0
@@ -0,0 +1,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
+ }