@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,285 @@
1
+ /**
2
+ * Environment Setup for Worktrees
3
+ *
4
+ * Handles copying env files, installing dependencies, and running setup scripts
5
+ * when creating new worktrees.
6
+ */
7
+
8
+ import { exec } from "child_process";
9
+ import { promisify } from "util";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+
13
+ const execAsync = promisify(exec);
14
+
15
+ export interface WorktreeConfig {
16
+ setup?: string[];
17
+ devServer?: {
18
+ command: string;
19
+ portEnvVar?: string;
20
+ };
21
+ }
22
+
23
+ export interface SetupResult {
24
+ success: boolean;
25
+ steps: Array<{
26
+ name: string;
27
+ command: string;
28
+ success: boolean;
29
+ output?: string;
30
+ error?: string;
31
+ }>;
32
+ envFilesCopied: string[];
33
+ packageManager?: string;
34
+ port?: number;
35
+ }
36
+
37
+ /**
38
+ * Read worktree config from project
39
+ */
40
+ export async function readWorktreeConfig(
41
+ projectPath: string
42
+ ): Promise<WorktreeConfig | null> {
43
+ const configPaths = [
44
+ path.join(projectPath, ".claude-deck", "worktrees.json"),
45
+ path.join(projectPath, ".claude-deck.json"),
46
+ ];
47
+
48
+ for (const configPath of configPaths) {
49
+ try {
50
+ if (fs.existsSync(configPath)) {
51
+ const content = await fs.promises.readFile(configPath, "utf-8");
52
+ return JSON.parse(content);
53
+ }
54
+ } catch {
55
+ // Continue to next path
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Detect package manager from lockfiles
64
+ */
65
+ export function detectPackageManager(projectPath: string): {
66
+ name: string;
67
+ installCommand: string;
68
+ } | null {
69
+ const lockfiles = [
70
+ { file: "bun.lockb", name: "bun", command: "bun install" },
71
+ { file: "pnpm-lock.yaml", name: "pnpm", command: "pnpm install" },
72
+ { file: "yarn.lock", name: "yarn", command: "yarn install" },
73
+ { file: "package-lock.json", name: "npm", command: "npm install" },
74
+ ];
75
+
76
+ for (const { file, name, command } of lockfiles) {
77
+ if (fs.existsSync(path.join(projectPath, file))) {
78
+ return { name, installCommand: command };
79
+ }
80
+ }
81
+
82
+ // Fallback: check if package.json exists
83
+ if (fs.existsSync(path.join(projectPath, "package.json"))) {
84
+ return { name: "npm", installCommand: "npm install" };
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Find env files to copy (excludes .env.example)
92
+ */
93
+ export function findEnvFiles(projectPath: string): string[] {
94
+ try {
95
+ const files = fs.readdirSync(projectPath);
96
+ return files.filter(
97
+ (f) =>
98
+ f.startsWith(".env") &&
99
+ !f.endsWith(".example") &&
100
+ fs.statSync(path.join(projectPath, f)).isFile()
101
+ );
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Copy env files from source to worktree
109
+ */
110
+ export async function copyEnvFiles(
111
+ sourcePath: string,
112
+ worktreePath: string
113
+ ): Promise<string[]> {
114
+ const envFiles = findEnvFiles(sourcePath);
115
+ const copied: string[] = [];
116
+
117
+ for (const file of envFiles) {
118
+ try {
119
+ const src = path.join(sourcePath, file);
120
+ const dest = path.join(worktreePath, file);
121
+ await fs.promises.copyFile(src, dest);
122
+ copied.push(file);
123
+ } catch (error) {
124
+ console.error(`Failed to copy ${file}:`, error);
125
+ }
126
+ }
127
+
128
+ return copied;
129
+ }
130
+
131
+ /**
132
+ * Run a setup command in the worktree directory
133
+ */
134
+ async function runCommand(
135
+ command: string,
136
+ cwd: string,
137
+ env: Record<string, string> = {}
138
+ ): Promise<{ success: boolean; output: string; error?: string }> {
139
+ try {
140
+ const { stdout, stderr } = await execAsync(command, {
141
+ cwd,
142
+ timeout: 300000, // 5 minutes
143
+ env: { ...process.env, ...env },
144
+ });
145
+ return {
146
+ success: true,
147
+ output: stdout + (stderr ? `\n${stderr}` : ""),
148
+ };
149
+ } catch (error: unknown) {
150
+ const err = error as { stdout?: string; stderr?: string; message?: string };
151
+ return {
152
+ success: false,
153
+ output: err.stdout || "",
154
+ error: err.stderr || err.message || "Unknown error",
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Run setup for a new worktree
161
+ */
162
+ export async function setupWorktree(options: {
163
+ worktreePath: string;
164
+ sourcePath: string;
165
+ port?: number;
166
+ skipInstall?: boolean;
167
+ }): Promise<SetupResult> {
168
+ const { worktreePath, sourcePath, port, skipInstall } = options;
169
+
170
+ const result: SetupResult = {
171
+ success: true,
172
+ steps: [],
173
+ envFilesCopied: [],
174
+ port,
175
+ };
176
+
177
+ // 1. Read config if exists
178
+ const config = await readWorktreeConfig(sourcePath);
179
+
180
+ // 2. Copy env files
181
+ result.envFilesCopied = await copyEnvFiles(sourcePath, worktreePath);
182
+ if (result.envFilesCopied.length > 0) {
183
+ result.steps.push({
184
+ name: "Copy env files",
185
+ command: `cp ${result.envFilesCopied.join(" ")}`,
186
+ success: true,
187
+ output: `Copied: ${result.envFilesCopied.join(", ")}`,
188
+ });
189
+ }
190
+
191
+ // Build env vars for commands
192
+ const envVars: Record<string, string> = {
193
+ ROOT_WORKTREE_PATH: sourcePath,
194
+ WORKTREE_PATH: worktreePath,
195
+ };
196
+ if (port) {
197
+ envVars.PORT = String(port);
198
+ }
199
+
200
+ // 3. Run config setup commands if present
201
+ if (config?.setup && config.setup.length > 0) {
202
+ for (const cmd of config.setup) {
203
+ // Expand variables in command
204
+ let expandedCmd = cmd;
205
+ for (const [key, value] of Object.entries(envVars)) {
206
+ expandedCmd = expandedCmd.replace(new RegExp(`\\$${key}`, "g"), value);
207
+ }
208
+
209
+ const cmdResult = await runCommand(expandedCmd, worktreePath, envVars);
210
+ result.steps.push({
211
+ name: `Config: ${cmd.slice(0, 50)}${cmd.length > 50 ? "..." : ""}`,
212
+ command: expandedCmd,
213
+ success: cmdResult.success,
214
+ output: cmdResult.output,
215
+ error: cmdResult.error,
216
+ });
217
+
218
+ if (!cmdResult.success) {
219
+ result.success = false;
220
+ }
221
+ }
222
+ } else if (!skipInstall) {
223
+ // 4. Auto-detect and install dependencies
224
+ const pm = detectPackageManager(sourcePath);
225
+ if (pm) {
226
+ result.packageManager = pm.name;
227
+ const installResult = await runCommand(
228
+ pm.installCommand,
229
+ worktreePath,
230
+ envVars
231
+ );
232
+ result.steps.push({
233
+ name: `Install dependencies (${pm.name})`,
234
+ command: pm.installCommand,
235
+ success: installResult.success,
236
+ output: installResult.output,
237
+ error: installResult.error,
238
+ });
239
+
240
+ if (!installResult.success) {
241
+ result.success = false;
242
+ }
243
+ }
244
+ }
245
+
246
+ return result;
247
+ }
248
+
249
+ /**
250
+ * Get dev server command from config or package.json
251
+ */
252
+ export async function getDevServerCommand(
253
+ projectPath: string,
254
+ port?: number
255
+ ): Promise<{ command: string; port: number } | null> {
256
+ // Check config first
257
+ const config = await readWorktreeConfig(projectPath);
258
+ if (config?.devServer) {
259
+ const portEnvVar = config.devServer.portEnvVar || "PORT";
260
+ const finalPort = port || 3000;
261
+ return {
262
+ command: `${portEnvVar}=${finalPort} ${config.devServer.command}`,
263
+ port: finalPort,
264
+ };
265
+ }
266
+
267
+ // Check package.json for dev script
268
+ const pkgPath = path.join(projectPath, "package.json");
269
+ if (fs.existsSync(pkgPath)) {
270
+ try {
271
+ const pkg = JSON.parse(await fs.promises.readFile(pkgPath, "utf-8"));
272
+ if (pkg.scripts?.dev) {
273
+ const finalPort = port || 3000;
274
+ return {
275
+ command: `PORT=${finalPort} npm run dev`,
276
+ port: finalPort,
277
+ };
278
+ }
279
+ } catch {
280
+ // Ignore parse errors
281
+ }
282
+ }
283
+
284
+ return null;
285
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Upload a file to temporary storage and return its path.
3
+ * Converts the file to base64 and POSTs to /api/files/upload-temp.
4
+ *
5
+ * @param file - The file to upload
6
+ * @returns The path to the uploaded file, or null if upload failed
7
+ */
8
+ export async function uploadFileToTemp(file: File): Promise<string | null> {
9
+ const buffer = await file.arrayBuffer();
10
+ const base64 = btoa(
11
+ new Uint8Array(buffer).reduce(
12
+ (data, byte) => data + String.fromCharCode(byte),
13
+ ""
14
+ )
15
+ );
16
+
17
+ const res = await fetch("/api/files/upload-temp", {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({
21
+ filename: file.name || `file-${Date.now()}`,
22
+ base64,
23
+ mimeType: file.type || "application/octet-stream",
24
+ }),
25
+ });
26
+
27
+ const data = await res.json();
28
+ if (data.path) {
29
+ return data.path;
30
+ }
31
+
32
+ console.error("Upload failed:", data.error);
33
+ return null;
34
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Client-safe file utilities (no Node.js dependencies)
3
+ */
4
+
5
+ export interface FileNode {
6
+ name: string;
7
+ path: string;
8
+ type: "file" | "directory";
9
+ size?: number;
10
+ extension?: string;
11
+ children?: FileNode[];
12
+ }
13
+
14
+ /**
15
+ * Get file extension for syntax highlighting
16
+ */
17
+ export function getLanguageFromExtension(ext: string): string {
18
+ const languageMap: Record<string, string> = {
19
+ js: "javascript",
20
+ jsx: "javascript",
21
+ ts: "typescript",
22
+ tsx: "typescript",
23
+ py: "python",
24
+ rb: "ruby",
25
+ go: "go",
26
+ rs: "rust",
27
+ java: "java",
28
+ c: "c",
29
+ cpp: "cpp",
30
+ cs: "csharp",
31
+ php: "php",
32
+ html: "html",
33
+ css: "css",
34
+ scss: "scss",
35
+ json: "json",
36
+ xml: "xml",
37
+ yaml: "yaml",
38
+ yml: "yaml",
39
+ md: "markdown",
40
+ sh: "bash",
41
+ bash: "bash",
42
+ zsh: "bash",
43
+ sql: "sql",
44
+ graphql: "graphql",
45
+ vue: "vue",
46
+ svelte: "svelte",
47
+ };
48
+
49
+ return languageMap[ext.toLowerCase()] || "plaintext";
50
+ }
package/lib/files.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * File system utilities for file explorer (server-only)
3
+ */
4
+
5
+ import { readdirSync, statSync, readFileSync, writeFileSync } from "fs";
6
+ import { join, extname } from "path";
7
+
8
+ // Re-export client-safe types and utilities
9
+ export type { FileNode } from "./file-utils";
10
+ export { getLanguageFromExtension } from "./file-utils";
11
+ import type { FileNode } from "./file-utils";
12
+
13
+ /**
14
+ * Default exclude patterns (matches common ignore patterns)
15
+ */
16
+ const DEFAULT_EXCLUDES = [
17
+ "node_modules",
18
+ ".git",
19
+ ".next",
20
+ "dist",
21
+ "build",
22
+ "out",
23
+ "coverage",
24
+ ".cache",
25
+ ".vercel",
26
+ ".turbo",
27
+ "__pycache__",
28
+ ".pytest_cache",
29
+ ".mypy_cache",
30
+ ".venv",
31
+ "venv",
32
+ ".DS_Store",
33
+ "*.log",
34
+ ".env",
35
+ ".env.local",
36
+ ".env.*.local",
37
+ "*.db",
38
+ "*.db-wal",
39
+ "*.db-shm",
40
+ ];
41
+
42
+ /**
43
+ * Check if a file/directory should be excluded
44
+ */
45
+ function shouldExclude(name: string, excludePatterns: string[]): boolean {
46
+ return excludePatterns.some((pattern) => {
47
+ if (pattern.includes("*")) {
48
+ // Simple glob pattern matching
49
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
50
+ return regex.test(name);
51
+ }
52
+ return name === pattern;
53
+ });
54
+ }
55
+
56
+ /**
57
+ * List directory contents with exclusions
58
+ */
59
+ export function listDirectory(
60
+ dirPath: string,
61
+ options: {
62
+ excludePatterns?: string[];
63
+ recursive?: boolean;
64
+ maxDepth?: number;
65
+ currentDepth?: number;
66
+ } = {}
67
+ ): FileNode[] {
68
+ const {
69
+ excludePatterns = DEFAULT_EXCLUDES,
70
+ recursive = false,
71
+ maxDepth = 3,
72
+ currentDepth = 0,
73
+ } = options;
74
+
75
+ try {
76
+ const entries = readdirSync(dirPath);
77
+ const nodes: FileNode[] = [];
78
+
79
+ for (const entry of entries) {
80
+ // Skip excluded files
81
+ if (shouldExclude(entry, excludePatterns)) {
82
+ continue;
83
+ }
84
+
85
+ const fullPath = join(dirPath, entry);
86
+ let stat;
87
+
88
+ try {
89
+ stat = statSync(fullPath);
90
+ } catch {
91
+ // Permission denied or other error, skip
92
+ continue;
93
+ }
94
+
95
+ const isDir = stat.isDirectory();
96
+ const node: FileNode = {
97
+ name: entry,
98
+ path: fullPath,
99
+ type: isDir ? "directory" : "file",
100
+ };
101
+
102
+ if (!isDir) {
103
+ node.size = stat.size;
104
+ node.extension = extname(entry).slice(1); // Remove leading dot
105
+ }
106
+
107
+ // Recursively load children if requested and not at max depth
108
+ if (isDir && recursive && currentDepth < maxDepth) {
109
+ node.children = listDirectory(fullPath, {
110
+ excludePatterns,
111
+ recursive: true,
112
+ maxDepth,
113
+ currentDepth: currentDepth + 1,
114
+ });
115
+ }
116
+
117
+ nodes.push(node);
118
+ }
119
+
120
+ // Sort: directories first, then files, alphabetically within each group
121
+ return nodes.sort((a, b) => {
122
+ if (a.type !== b.type) {
123
+ return a.type === "directory" ? -1 : 1;
124
+ }
125
+ return a.name.localeCompare(b.name);
126
+ });
127
+ } catch (error) {
128
+ console.error(`Failed to list directory ${dirPath}:`, error);
129
+ return [];
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Read file contents with encoding detection
135
+ */
136
+ export function readFileContent(
137
+ filePath: string,
138
+ options: { maxSize?: number } = {}
139
+ ): { content: string; isBinary: boolean; size: number } {
140
+ const { maxSize = 1024 * 1024 } = options; // Default 1MB max
141
+
142
+ try {
143
+ const stat = statSync(filePath);
144
+
145
+ if (stat.size > maxSize) {
146
+ return {
147
+ content: `File too large (${(stat.size / 1024 / 1024).toFixed(2)}MB). Maximum size: ${(maxSize / 1024 / 1024).toFixed(2)}MB`,
148
+ isBinary: false,
149
+ size: stat.size,
150
+ };
151
+ }
152
+
153
+ const buffer = readFileSync(filePath);
154
+
155
+ // Simple binary detection: check for null bytes
156
+ const isBinary = buffer.includes(0);
157
+
158
+ if (isBinary) {
159
+ return {
160
+ content: `Binary file (${(stat.size / 1024).toFixed(2)}KB)`,
161
+ isBinary: true,
162
+ size: stat.size,
163
+ };
164
+ }
165
+
166
+ return {
167
+ content: buffer.toString("utf-8"),
168
+ isBinary: false,
169
+ size: stat.size,
170
+ };
171
+ } catch (error) {
172
+ throw new Error(
173
+ `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`
174
+ );
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Write content to a file
180
+ */
181
+ export function writeFileContent(
182
+ filePath: string,
183
+ content: string,
184
+ options: { maxSize?: number } = {}
185
+ ): { success: boolean; size: number } {
186
+ const { maxSize = 1024 * 1024 } = options; // Default 1MB max
187
+
188
+ const contentBuffer = Buffer.from(content, "utf-8");
189
+
190
+ if (contentBuffer.length > maxSize) {
191
+ throw new Error(
192
+ `Content too large (${(contentBuffer.length / 1024 / 1024).toFixed(2)}MB). Maximum size: ${(maxSize / 1024 / 1024).toFixed(2)}MB`
193
+ );
194
+ }
195
+
196
+ try {
197
+ writeFileSync(filePath, content, "utf-8");
198
+ return {
199
+ success: true,
200
+ size: contentBuffer.length,
201
+ };
202
+ } catch (error) {
203
+ throw new Error(
204
+ `Failed to write file: ${error instanceof Error ? error.message : "Unknown error"}`
205
+ );
206
+ }
207
+ }