@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saad Naveed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ClaudeDeck
|
|
2
|
+
|
|
3
|
+
A mobile-first web UI for managing AI coding sessions.
|
|
4
|
+
|
|
5
|
+
[](https://discord.gg/cSjutkCGAh)
|
|
6
|
+
|
|
7
|
+
https://github.com/user-attachments/assets/0e2e66f7-037e-4739-99ec-608d1840df0a
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### Via npm (Recommended)
|
|
14
|
+
|
|
15
|
+
If you already have Node.js 24+ installed:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Install globally
|
|
19
|
+
npm install -g @atercates/claude-deck
|
|
20
|
+
|
|
21
|
+
# Run setup (checks/installs tmux, ripgrep, builds app)
|
|
22
|
+
claude-deck install
|
|
23
|
+
|
|
24
|
+
# Start the server
|
|
25
|
+
claude-deck start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Via curl (Installs everything)
|
|
29
|
+
|
|
30
|
+
For fresh installs without Node.js:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -fsSL https://raw.githubusercontent.com/atercates/claude-deck/main/scripts/install.sh | bash
|
|
34
|
+
claude-deck start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Desktop App
|
|
38
|
+
|
|
39
|
+
Download native desktop apps from [Releases](https://github.com/atercates/claude-deck/releases):
|
|
40
|
+
|
|
41
|
+
- macOS (Apple Silicon): `.dmg`
|
|
42
|
+
- Linux: `.deb` or `.AppImage`
|
|
43
|
+
|
|
44
|
+
> **Note:** The desktop app is a native wrapper around the web UI. You still need to install and run ClaudeDeck (via the installer script above) for the backend server. The desktop app just provides a convenient native window instead of using your browser.
|
|
45
|
+
|
|
46
|
+
> **Don't want to self-host?** Try [ClaudeDeck Cloud](https://runagentos.com) - pre-configured cloud VMs for AI coding.
|
|
47
|
+
|
|
48
|
+
### Manual Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/atercates/claude-deck
|
|
52
|
+
cd claude-deck
|
|
53
|
+
npm install
|
|
54
|
+
npm run dev # http://localhost:3011
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Prerequisites
|
|
58
|
+
|
|
59
|
+
- Node.js 24+
|
|
60
|
+
- tmux
|
|
61
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep) (for code search - auto-installed by installer script, or run `claude-deck update`)
|
|
62
|
+
- At least one AI CLI: [Claude Code](https://github.com/anthropics/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://github.com/anomalyco/opencode), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Aider](https://aider.chat/), or [Cursor CLI](https://cursor.com/cli)
|
|
63
|
+
|
|
64
|
+
## Supported Agents
|
|
65
|
+
|
|
66
|
+
| Agent | Resume | Fork | Auto-Approve |
|
|
67
|
+
| ----------- | ------ | ---- | -------------------------------- |
|
|
68
|
+
| Claude Code | ✅ | ✅ | `--dangerously-skip-permissions` |
|
|
69
|
+
| Codex | ❌ | ❌ | `--approval-mode full-auto` |
|
|
70
|
+
| OpenCode | ❌ | ❌ | Config file |
|
|
71
|
+
| Gemini CLI | ❌ | ❌ | `--yolomode` |
|
|
72
|
+
| Aider | ❌ | ❌ | `--yes` |
|
|
73
|
+
| Cursor CLI | ❌ | ❌ | N/A |
|
|
74
|
+
| Amp | ❌ | ❌ | `--dangerously-allow-all` |
|
|
75
|
+
| Pi | ❌ | ❌ | N/A |
|
|
76
|
+
| Oh My Pi | ❌ | ❌ | N/A |
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **Mobile-first** - Full functionality from your phone, not a dumbed-down responsive view
|
|
81
|
+
- **Voice-to-text** - Dictate prompts to your coding sessions hands-free
|
|
82
|
+
- **Multi-pane layout** - Run up to 4 sessions side-by-side
|
|
83
|
+
- **Code search** - Fast codebase search with syntax-highlighted results (Cmd+K)
|
|
84
|
+
- **File picker** - Browse and attach files to sessions, with direct upload from mobile
|
|
85
|
+
- **Clone from GitHub** - Clone repos directly from the UI when creating projects
|
|
86
|
+
- **Git integration** - Status, diffs, commits, PRs from the UI
|
|
87
|
+
- **Git worktrees** - Isolated branches with auto-setup
|
|
88
|
+
- **Dev servers** - Start/stop Node.js and Docker servers
|
|
89
|
+
- **Session orchestration** - Conductor/worker model via MCP
|
|
90
|
+
|
|
91
|
+
## CLI Commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude-deck run # Start and open browser
|
|
95
|
+
claude-deck start # Start in background
|
|
96
|
+
claude-deck stop # Stop server
|
|
97
|
+
claude-deck status # Show URLs
|
|
98
|
+
claude-deck logs # Tail logs
|
|
99
|
+
claude-deck update # Update to latest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Mobile Access
|
|
103
|
+
|
|
104
|
+
Use [Tailscale](https://tailscale.com) for secure access from your phone:
|
|
105
|
+
|
|
106
|
+
1. Install Tailscale on your dev machine and phone
|
|
107
|
+
2. Sign in with the same account
|
|
108
|
+
3. Access `http://100.x.x.x:3011` from your phone
|
|
109
|
+
|
|
110
|
+
## Documentation
|
|
111
|
+
|
|
112
|
+
For configuration and advanced usage, see the [docs](https://www.runagentos.com/docs).
|
|
113
|
+
|
|
114
|
+
## Related Projects
|
|
115
|
+
|
|
116
|
+
- **[aTerm](https://github.com/atercates/aTerm)** - A Tauri-based desktop terminal workspace for AI-assisted coding. While ClaudeDeck is a mobile-first web UI, aTerm is a native desktop app with multi-pane layouts optimized for running AI coding agents (Claude Code, Aider, OpenCode) alongside shells, dev servers, and a built-in git panel. Choose ClaudeDeck for mobile access and browser-based workflows, or aTerm for a native desktop terminal experience.
|
|
117
|
+
- **[LumifyHub](https://lumifyhub.io)** - Team collaboration platform with real-time chat and structured documentation. Useful alongside ClaudeDeck for coordinating multi-agent work across a team — share session context, document architectural decisions from coding sessions, and track progress across parallel agent workflows.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT License - Free and open source.
|
|
122
|
+
|
|
123
|
+
See [LICENSE](LICENSE) for full terms.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { queries } from "@/lib/db";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const items = await queries.getAllHiddenItems();
|
|
7
|
+
return NextResponse.json({ items });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error("Error fetching hidden items:", error);
|
|
10
|
+
return NextResponse.json(
|
|
11
|
+
{ error: "Failed to fetch hidden items" },
|
|
12
|
+
{ status: 500 }
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
const { itemType, itemId } = await request.json();
|
|
20
|
+
|
|
21
|
+
if (!itemType || !itemId) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: "itemType and itemId are required" },
|
|
24
|
+
{ status: 400 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!["project", "session"].includes(itemType)) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: "itemType must be 'project' or 'session'" },
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await queries.hideItem(itemType, itemId);
|
|
36
|
+
return NextResponse.json({ hidden: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error hiding item:", error);
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: "Failed to hide item" },
|
|
41
|
+
{ status: 500 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function DELETE(request: NextRequest) {
|
|
47
|
+
try {
|
|
48
|
+
const { itemType, itemId } = await request.json();
|
|
49
|
+
|
|
50
|
+
if (!itemType || !itemId) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: "itemType and itemId are required" },
|
|
53
|
+
{ status: 400 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await queries.unhideItem(itemType, itemId);
|
|
58
|
+
return NextResponse.json({ hidden: false });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Error unhiding item:", error);
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{ error: "Failed to unhide item" },
|
|
63
|
+
{ status: 500 }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getCachedSessions, getCachedProjects } from "@/lib/claude/jsonl-cache";
|
|
3
|
+
import { queries } from "@/lib/db";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
function resolveValidCwd(
|
|
8
|
+
cwd: string | null,
|
|
9
|
+
projectDirectory: string | null
|
|
10
|
+
): string {
|
|
11
|
+
if (!cwd && projectDirectory) return projectDirectory;
|
|
12
|
+
if (!cwd) return process.env.HOME || "/";
|
|
13
|
+
if (fs.existsSync(cwd)) return cwd;
|
|
14
|
+
let dir = cwd;
|
|
15
|
+
while (dir && dir !== "/" && !fs.existsSync(dir)) {
|
|
16
|
+
dir = path.dirname(dir);
|
|
17
|
+
}
|
|
18
|
+
if (dir && dir !== "/" && fs.existsSync(dir)) return dir;
|
|
19
|
+
return projectDirectory || process.env.HOME || "/";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RouteParams {
|
|
23
|
+
params: Promise<{ name: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
27
|
+
try {
|
|
28
|
+
const { name } = await params;
|
|
29
|
+
const { searchParams } = new URL(request.url);
|
|
30
|
+
const limit = parseInt(searchParams.get("limit") || "50", 10);
|
|
31
|
+
const offset = parseInt(searchParams.get("offset") || "0", 10);
|
|
32
|
+
const includeHidden = searchParams.get("includeHidden") === "true";
|
|
33
|
+
|
|
34
|
+
const [allSessions, allProjects] = await Promise.all([
|
|
35
|
+
getCachedSessions(name),
|
|
36
|
+
getCachedProjects(),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const project = allProjects.find((p) => p.name === name);
|
|
40
|
+
const projectDir = project?.directory || null;
|
|
41
|
+
|
|
42
|
+
const hiddenItems = await queries.getHiddenItems("session");
|
|
43
|
+
const hiddenSet = new Set(hiddenItems.map((h) => h.item_id));
|
|
44
|
+
|
|
45
|
+
const enriched = allSessions
|
|
46
|
+
.filter((s) => s.messageCount > 2 || s.cwd)
|
|
47
|
+
.map((s) => ({
|
|
48
|
+
...s,
|
|
49
|
+
cwd: resolveValidCwd(s.cwd, projectDir),
|
|
50
|
+
hidden: hiddenSet.has(s.sessionId),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const filtered = includeHidden
|
|
54
|
+
? enriched
|
|
55
|
+
: enriched.filter((s) => !s.hidden);
|
|
56
|
+
|
|
57
|
+
const paginated = filtered.slice(offset, offset + limit);
|
|
58
|
+
|
|
59
|
+
return NextResponse.json({
|
|
60
|
+
sessions: paginated,
|
|
61
|
+
total: filtered.length,
|
|
62
|
+
hasMore: offset + limit < filtered.length,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error("Error fetching sessions:", error);
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: "Failed to fetch sessions" },
|
|
68
|
+
{ status: 500 }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getCachedProjects } from "@/lib/claude/jsonl-cache";
|
|
3
|
+
import { queries } from "@/lib/db";
|
|
4
|
+
|
|
5
|
+
export interface ClaudeProject {
|
|
6
|
+
name: string;
|
|
7
|
+
directory: string | null;
|
|
8
|
+
displayName: string;
|
|
9
|
+
sessionCount: number;
|
|
10
|
+
lastActivity: string | null;
|
|
11
|
+
hidden: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET() {
|
|
15
|
+
try {
|
|
16
|
+
const cachedProjects = await getCachedProjects();
|
|
17
|
+
const hiddenItems = await queries.getHiddenItems("project");
|
|
18
|
+
const hiddenSet = new Set(hiddenItems.map((h) => h.item_id));
|
|
19
|
+
|
|
20
|
+
const projects: ClaudeProject[] = cachedProjects.map((p) => ({
|
|
21
|
+
...p,
|
|
22
|
+
hidden: hiddenSet.has(p.name),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
projects.sort((a, b) => {
|
|
26
|
+
if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
|
|
27
|
+
if (!a.lastActivity && !b.lastActivity) return 0;
|
|
28
|
+
if (!a.lastActivity) return 1;
|
|
29
|
+
if (!b.lastActivity) return -1;
|
|
30
|
+
return (
|
|
31
|
+
new Date(b.lastActivity).getTime() -
|
|
32
|
+
new Date(a.lastActivity).getTime()
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return NextResponse.json({ projects });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error discovering Claude projects:", error);
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: "Failed to discover projects" },
|
|
41
|
+
{ status: 500 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { isRipgrepAvailable } from "@/lib/code-search";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const available = isRipgrepAvailable();
|
|
7
|
+
return NextResponse.json({ available });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error("Error checking ripgrep availability:", error);
|
|
10
|
+
return NextResponse.json({ available: false });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { searchCode, formatSearchResults } from "@/lib/code-search";
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = request.nextUrl;
|
|
7
|
+
const query = searchParams.get("query");
|
|
8
|
+
const path = searchParams.get("path");
|
|
9
|
+
const maxResults = parseInt(searchParams.get("maxResults") || "100");
|
|
10
|
+
const contextLines = parseInt(searchParams.get("contextLines") || "2");
|
|
11
|
+
|
|
12
|
+
if (!query) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "Query parameter is required" },
|
|
15
|
+
{ status: 400 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!path) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Path parameter is required" },
|
|
22
|
+
{ status: 400 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const expandedPath = path.replace(/^~/, process.env.HOME || "");
|
|
27
|
+
|
|
28
|
+
const rawMatches = searchCode(expandedPath, query, {
|
|
29
|
+
maxResults,
|
|
30
|
+
contextLines,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const results = formatSearchResults(rawMatches);
|
|
34
|
+
|
|
35
|
+
return NextResponse.json({
|
|
36
|
+
results,
|
|
37
|
+
query,
|
|
38
|
+
path: expandedPath,
|
|
39
|
+
count: results.length,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Code search error:", error);
|
|
43
|
+
const message =
|
|
44
|
+
error instanceof Error ? error.message : "Failed to search code";
|
|
45
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getServerLogs } from "@/lib/dev-servers";
|
|
3
|
+
|
|
4
|
+
// GET /api/dev-servers/[id]/logs - Get server logs
|
|
5
|
+
export async function GET(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const { searchParams } = new URL(request.url);
|
|
12
|
+
const lines = parseInt(searchParams.get("lines") || "100", 10);
|
|
13
|
+
|
|
14
|
+
const logs = await getServerLogs(id, lines);
|
|
15
|
+
return NextResponse.json({ logs });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("Error getting dev server logs:", error);
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "Failed to get dev server logs" },
|
|
20
|
+
{ status: 500 }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { restartServer } from "@/lib/dev-servers";
|
|
3
|
+
|
|
4
|
+
// POST /api/dev-servers/[id]/restart - Restart a server
|
|
5
|
+
export async function POST(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const server = await restartServer(id);
|
|
12
|
+
return NextResponse.json({ server });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Error restarting dev server:", error);
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: "Failed to restart dev server" },
|
|
17
|
+
{ status: 500 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { queries, type DevServer } from "@/lib/db";
|
|
3
|
+
import { getServerStatus, removeServer } from "@/lib/dev-servers";
|
|
4
|
+
|
|
5
|
+
// GET /api/dev-servers/[id] - Get single server with live status
|
|
6
|
+
export async function GET(
|
|
7
|
+
request: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const server = await queries.getDevServer(id) as DevServer | undefined;
|
|
13
|
+
|
|
14
|
+
if (!server) {
|
|
15
|
+
return NextResponse.json({ error: "Server not found" }, { status: 404 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Get live status
|
|
19
|
+
const liveStatus = await getServerStatus(server);
|
|
20
|
+
if (liveStatus !== server.status) {
|
|
21
|
+
await queries.updateDevServerStatus(liveStatus, id);
|
|
22
|
+
server.status = liveStatus;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return NextResponse.json({ server });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Error getting dev server:", error);
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: "Failed to get dev server" },
|
|
30
|
+
{ status: 500 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// DELETE /api/dev-servers/[id] - Stop and remove server
|
|
36
|
+
export async function DELETE(
|
|
37
|
+
request: NextRequest,
|
|
38
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
39
|
+
) {
|
|
40
|
+
try {
|
|
41
|
+
const { id } = await params;
|
|
42
|
+
await removeServer(id);
|
|
43
|
+
return NextResponse.json({ success: true });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error removing dev server:", error);
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: "Failed to remove dev server" },
|
|
48
|
+
{ status: 500 }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { stopServer } from "@/lib/dev-servers";
|
|
3
|
+
|
|
4
|
+
// POST /api/dev-servers/[id]/stop - Stop a server
|
|
5
|
+
export async function POST(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
await stopServer(id);
|
|
12
|
+
return NextResponse.json({ success: true });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Error stopping dev server:", error);
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: "Failed to stop dev server" },
|
|
17
|
+
{ status: 500 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { detectServers } from "@/lib/dev-servers";
|
|
3
|
+
import { queries, type Project } from "@/lib/db";
|
|
4
|
+
|
|
5
|
+
// GET /api/dev-servers/detect?projectId=X - Auto-detect available dev servers
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const { searchParams } = new URL(request.url);
|
|
9
|
+
const projectId = searchParams.get("projectId");
|
|
10
|
+
|
|
11
|
+
if (!projectId) {
|
|
12
|
+
return NextResponse.json(
|
|
13
|
+
{ error: "projectId is required" },
|
|
14
|
+
{ status: 400 }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const project = await queries.getProject(projectId) as
|
|
19
|
+
| Project
|
|
20
|
+
| undefined;
|
|
21
|
+
if (!project) {
|
|
22
|
+
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Expand ~ to home directory
|
|
26
|
+
const expandedPath = project.working_directory.startsWith("~")
|
|
27
|
+
? project.working_directory.replace("~", process.env.HOME || "")
|
|
28
|
+
: project.working_directory;
|
|
29
|
+
|
|
30
|
+
const servers = await detectServers(expandedPath);
|
|
31
|
+
return NextResponse.json({ servers, workingDirectory: expandedPath });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Error detecting dev servers:", error);
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: "Failed to detect dev servers" },
|
|
36
|
+
{ status: 500 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getAllServers, startServer } from "@/lib/dev-servers";
|
|
3
|
+
|
|
4
|
+
// GET /api/dev-servers - List all servers with live status
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
const servers = await getAllServers();
|
|
8
|
+
return NextResponse.json({ servers });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error("Error getting dev servers:", error);
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "Failed to get dev servers" },
|
|
13
|
+
{ status: 500 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// POST /api/dev-servers - Start a new server
|
|
19
|
+
export async function POST(request: NextRequest) {
|
|
20
|
+
try {
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
const { projectId, type, name, command, workingDirectory, ports } = body;
|
|
23
|
+
|
|
24
|
+
if (!projectId || !type || !name || !command || !workingDirectory) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: "Missing required fields" },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const server = await startServer({
|
|
32
|
+
projectId,
|
|
33
|
+
type,
|
|
34
|
+
name,
|
|
35
|
+
command,
|
|
36
|
+
workingDirectory,
|
|
37
|
+
ports,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return NextResponse.json({ server });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Error starting dev server:", error);
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: "Failed to start dev server" },
|
|
45
|
+
{ status: 500 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
// Max execution time (10 seconds)
|
|
8
|
+
const TIMEOUT = 10000;
|
|
9
|
+
|
|
10
|
+
export async function POST(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await request.json();
|
|
13
|
+
const { command } = body;
|
|
14
|
+
|
|
15
|
+
if (!command) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: "No command specified" },
|
|
18
|
+
{ status: 400 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
26
|
+
timeout: TIMEOUT,
|
|
27
|
+
shell: "/bin/zsh",
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
PATH: `/usr/local/bin:/opt/homebrew/bin:${process.env.PATH}`,
|
|
31
|
+
HOME: process.env.HOME,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const duration = Date.now() - startTime;
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
success: true,
|
|
39
|
+
output: stdout || stderr,
|
|
40
|
+
duration,
|
|
41
|
+
});
|
|
42
|
+
} catch (execError: unknown) {
|
|
43
|
+
const duration = Date.now() - startTime;
|
|
44
|
+
const error = execError as {
|
|
45
|
+
stdout?: string;
|
|
46
|
+
stderr?: string;
|
|
47
|
+
message?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return NextResponse.json({
|
|
51
|
+
success: false,
|
|
52
|
+
output:
|
|
53
|
+
error.stderr || error.stdout || error.message || "Unknown error",
|
|
54
|
+
duration,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
}
|