@tom2012/cc-web 1.5.10
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 +339 -0
- package/backend/dist/auth.d.ts +15 -0
- package/backend/dist/auth.d.ts.map +1 -0
- package/backend/dist/auth.js +92 -0
- package/backend/dist/auth.js.map +1 -0
- package/backend/dist/config.d.ts +33 -0
- package/backend/dist/config.d.ts.map +1 -0
- package/backend/dist/config.js +155 -0
- package/backend/dist/config.js.map +1 -0
- package/backend/dist/index.d.ts +3 -0
- package/backend/dist/index.d.ts.map +1 -0
- package/backend/dist/index.js +499 -0
- package/backend/dist/index.js.map +1 -0
- package/backend/dist/routes/auth.d.ts +3 -0
- package/backend/dist/routes/auth.d.ts.map +1 -0
- package/backend/dist/routes/auth.js +108 -0
- package/backend/dist/routes/auth.js.map +1 -0
- package/backend/dist/routes/filesystem.d.ts +3 -0
- package/backend/dist/routes/filesystem.d.ts.map +1 -0
- package/backend/dist/routes/filesystem.js +243 -0
- package/backend/dist/routes/filesystem.js.map +1 -0
- package/backend/dist/routes/projects.d.ts +3 -0
- package/backend/dist/routes/projects.d.ts.map +1 -0
- package/backend/dist/routes/projects.js +235 -0
- package/backend/dist/routes/projects.js.map +1 -0
- package/backend/dist/routes/shortcuts.d.ts +3 -0
- package/backend/dist/routes/shortcuts.d.ts.map +1 -0
- package/backend/dist/routes/shortcuts.js +88 -0
- package/backend/dist/routes/shortcuts.js.map +1 -0
- package/backend/dist/routes/update.d.ts +3 -0
- package/backend/dist/routes/update.d.ts.map +1 -0
- package/backend/dist/routes/update.js +104 -0
- package/backend/dist/routes/update.js.map +1 -0
- package/backend/dist/session-manager.d.ts +47 -0
- package/backend/dist/session-manager.d.ts.map +1 -0
- package/backend/dist/session-manager.js +345 -0
- package/backend/dist/session-manager.js.map +1 -0
- package/backend/dist/terminal-manager.d.ts +27 -0
- package/backend/dist/terminal-manager.d.ts.map +1 -0
- package/backend/dist/terminal-manager.js +211 -0
- package/backend/dist/terminal-manager.js.map +1 -0
- package/backend/dist/types.d.ts +17 -0
- package/backend/dist/types.d.ts.map +1 -0
- package/backend/dist/types.js +3 -0
- package/backend/dist/types.js.map +1 -0
- package/backend/dist/usage-terminal.d.ts +18 -0
- package/backend/dist/usage-terminal.d.ts.map +1 -0
- package/backend/dist/usage-terminal.js +189 -0
- package/backend/dist/usage-terminal.js.map +1 -0
- package/backend/package-lock.json +1965 -0
- package/backend/package.json +31 -0
- package/bin/ccweb.js +478 -0
- package/electron/dist/main.js +455 -0
- package/frontend/dist/assets/index-CQjbS4zv.css +32 -0
- package/frontend/dist/assets/index-CtyR65A4.js +434 -0
- package/frontend/dist/index.html +14 -0
- package/frontend/dist/terminal.svg +4 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zbc0315
|
|
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,339 @@
|
|
|
1
|
+
# CC Web
|
|
2
|
+
|
|
3
|
+
A self-hosted web application (and macOS Electron desktop app) that provides a browser-based interface for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI sessions. Create projects, each with a persistent terminal running Claude Code, and interact with them through a real-time terminal UI.
|
|
4
|
+
|
|
5
|
+
**Current version**: v1.5.10 | [GitHub](https://github.com/zbc0315/cc-web) | MIT License
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Project Management**: Create, open, start, stop, and delete projects from a dashboard
|
|
10
|
+
- **Real-time Terminal**: Full xterm.js terminal in the browser, connected to Claude Code via WebSocket
|
|
11
|
+
- **Persistent Sessions**: Conversation history stored in each project's `.ccweb/` folder — survives uninstall/reinstall (max 20 sessions per project, auto-pruned)
|
|
12
|
+
- **Permission Modes**: Run Claude in limited mode (asks before acting) or unlimited mode (`--dangerously-skip-permissions`)
|
|
13
|
+
- **Shortcuts Panel**: Define reusable prompt commands at project or global level with inheritance
|
|
14
|
+
- **Session History**: Browse past conversations with full message history
|
|
15
|
+
- **Graph Visualization**: Topology graph from `.notebook/graph.yaml` with zoom/pan (layered DAG layout)
|
|
16
|
+
- **File Browser**: Browse directories and preview/edit files with zoom-level memory per file
|
|
17
|
+
- **Auto-restart**: Terminals automatically recover from crashes
|
|
18
|
+
- **Usage Tracking**: Monitor Claude Code plan usage directly from the dashboard
|
|
19
|
+
- **In-app Updates**: Check GitHub releases, download, and install without leaving the app
|
|
20
|
+
- **Localhost Auto-auth**: Local access skips login entirely; JWT only required for remote access
|
|
21
|
+
- **Auto Port Switching**: Backend tries ports 3001–3020 and reports the actual port
|
|
22
|
+
- **Dark/Light Theme**: Toggle between themes
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- **Node.js** >= 18
|
|
27
|
+
- **Claude Code CLI** installed and authenticated (`claude` command available in PATH)
|
|
28
|
+
|
|
29
|
+
## Quick Start — npm / npx
|
|
30
|
+
|
|
31
|
+
The fastest way to get running:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Try without installing (one-time)
|
|
35
|
+
npx cc-web
|
|
36
|
+
|
|
37
|
+
# Or install globally
|
|
38
|
+
npm install -g cc-web
|
|
39
|
+
ccweb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
On first launch you'll be prompted to set a username and password. The server auto-selects an available port (starting from 3001) and opens your browser automatically.
|
|
43
|
+
|
|
44
|
+
### CLI Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ccweb # start (interactive prompts)
|
|
48
|
+
ccweb start --daemon # start in background, no prompts
|
|
49
|
+
ccweb start --foreground # start in foreground, no prompts
|
|
50
|
+
ccweb stop # stop background server
|
|
51
|
+
ccweb status # show PID, port, data location
|
|
52
|
+
ccweb open # open browser to running server
|
|
53
|
+
ccweb setup # reconfigure username / password
|
|
54
|
+
ccweb enable-autostart # start automatically on login
|
|
55
|
+
ccweb disable-autostart # remove auto-start
|
|
56
|
+
ccweb logs # tail background log file
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All user data (credentials, projects, sessions) is stored in `~/.ccweb/` and survives package updates.
|
|
60
|
+
|
|
61
|
+
**Auto-start on login**: `ccweb enable-autostart` registers a launchd agent (macOS) or systemd user service (Linux) so the server starts automatically when you log in.
|
|
62
|
+
|
|
63
|
+
## Quick Start — from source (development)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Clone the repository
|
|
67
|
+
git clone https://github.com/zbc0315/cc-web.git
|
|
68
|
+
cd cc-web
|
|
69
|
+
|
|
70
|
+
# 2. Install dependencies
|
|
71
|
+
npm run install:all
|
|
72
|
+
|
|
73
|
+
# 3. First-time setup (creates login credentials)
|
|
74
|
+
npm run setup
|
|
75
|
+
|
|
76
|
+
# 4. Start backend (Terminal 1)
|
|
77
|
+
npm run dev:backend
|
|
78
|
+
|
|
79
|
+
# 5. Start frontend (Terminal 2)
|
|
80
|
+
npm run dev:frontend
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Open http://localhost:5173 in your browser.
|
|
84
|
+
|
|
85
|
+
## Architecture
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Browser (React/Vite :5173 dev | Express :3001 prod)
|
|
89
|
+
│
|
|
90
|
+
├── REST API ──────────► Express (:3001, auto-switches port if busy)
|
|
91
|
+
│ │
|
|
92
|
+
└── WebSocket ─────────► ws server (same port)
|
|
93
|
+
│
|
|
94
|
+
TerminalManager
|
|
95
|
+
│
|
|
96
|
+
node-pty (PTY, user's $SHELL -ilc "claude")
|
|
97
|
+
│
|
|
98
|
+
claude / claude --dangerously-skip-permissions
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Backend (`backend/src/`)
|
|
102
|
+
|
|
103
|
+
| File | Purpose |
|
|
104
|
+
|------|---------|
|
|
105
|
+
| `index.ts` | Express + WS server, route mounting, static frontend serving, auto port switching, project config migration |
|
|
106
|
+
| `auth.ts` | JWT middleware, localhost auto-auth (`isLocalRequest`), `generateLocalToken()` |
|
|
107
|
+
| `config.ts` | File-based JSON store, `.ccweb/` per-project config helpers |
|
|
108
|
+
| `terminal-manager.ts` | PTY lifecycle (`$SHELL -ilc "claude"`), scrollback buffer (5 MB), auto-restart, activity tracking |
|
|
109
|
+
| `session-manager.ts` | Tails Claude's JSONL files, stores sessions in `.ccweb/sessions/`, prunes to latest 20 per project |
|
|
110
|
+
| `usage-terminal.ts` | Claude Code OAuth usage stats |
|
|
111
|
+
| `routes/auth.ts` | `POST /login`, `GET /local-token` (localhost only) |
|
|
112
|
+
| `routes/projects.ts` | CRUD + start/stop + `POST /open` (restore from `.ccweb/`) |
|
|
113
|
+
| `routes/update.ts` | `GET /check-running`, `POST /prepare` (save memory → wait idle → stop all) |
|
|
114
|
+
| `routes/filesystem.ts` | Directory browser, file read/write |
|
|
115
|
+
| `routes/shortcuts.ts` | Global shortcut CRUD with inheritance |
|
|
116
|
+
|
|
117
|
+
### Frontend (`frontend/src/`)
|
|
118
|
+
|
|
119
|
+
| File/Dir | Purpose |
|
|
120
|
+
|----------|---------|
|
|
121
|
+
| `App.tsx` | Router with auto-auth `PrivateRoute` (local token for localhost) |
|
|
122
|
+
| `pages/LoginPage.tsx` | Login form, auto-login on localhost |
|
|
123
|
+
| `pages/DashboardPage.tsx` | Project grid, new/open project, fullscreen toggle, update button |
|
|
124
|
+
| `pages/ProjectPage.tsx` | Three-panel layout: FileTree \| WebTerminal \| RightPanel |
|
|
125
|
+
| `components/WebTerminal.tsx` | xterm.js terminal with fit addon |
|
|
126
|
+
| `components/RightPanel.tsx` | Three tabs: Shortcuts / History / Graph |
|
|
127
|
+
| `components/ShortcutPanel.tsx` | Project + global shortcuts, dialog editor for add/edit |
|
|
128
|
+
| `components/GraphPreview.tsx` | SVG topology graph of `.notebook/graph.yaml` (layered DAG, zoom/pan) |
|
|
129
|
+
| `components/FileTree.tsx` | Expandable directory tree |
|
|
130
|
+
| `components/FilePreviewDialog.tsx` | File viewer with plain/rendered/edit modes, zoom memory per file |
|
|
131
|
+
| `components/UpdateButton.tsx` | In-app update: check GitHub → save memory → download → install |
|
|
132
|
+
| `components/OpenProjectDialog.tsx` | Open existing project from `.ccweb/` folder |
|
|
133
|
+
| `components/NewProjectDialog.tsx` | 3-step wizard: name → folder → permissions |
|
|
134
|
+
| `lib/api.ts` | Typed REST client, dynamic base URL (relative in prod, localhost:3001 in dev) |
|
|
135
|
+
| `lib/websocket.ts` | `useProjectWebSocket` hook, dynamic WS URL |
|
|
136
|
+
| `components/ui/` | shadcn/ui components (zinc theme) |
|
|
137
|
+
|
|
138
|
+
### Data Storage
|
|
139
|
+
|
|
140
|
+
**Application data** (`data/` — gitignored, or `~/Library/Application Support/cc-web/data/` in Electron):
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
data/
|
|
144
|
+
├── config.json ← credentials & JWT secret
|
|
145
|
+
├── projects.json ← registered project list
|
|
146
|
+
└── global-shortcuts.json ← shared shortcut commands
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Per-project data** (inside each project folder, portable):
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
your-project/
|
|
153
|
+
├── .ccweb/
|
|
154
|
+
│ ├── project.json ← project metadata (id, name, mode, created)
|
|
155
|
+
│ └── sessions/ ← conversation history (max 20, auto-pruned)
|
|
156
|
+
│ └── {timestamp}-{uuid}.json
|
|
157
|
+
└── .notebook/ ← structured notes
|
|
158
|
+
├── pages/
|
|
159
|
+
└── graph.yaml
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `.ccweb/` folder travels with the project. If you reinstall CC Web later, use **Open Project** to point at the folder and all history is restored.
|
|
163
|
+
|
|
164
|
+
## WebSocket Protocol
|
|
165
|
+
|
|
166
|
+
**Client → Server:**
|
|
167
|
+
|
|
168
|
+
| Type | Payload | Purpose |
|
|
169
|
+
|------|---------|---------|
|
|
170
|
+
| `auth` | `{ token }` | Authenticate (skipped for localhost) |
|
|
171
|
+
| `terminal_subscribe` | `{ cols, rows }` | Subscribe + replay scrollback |
|
|
172
|
+
| `terminal_input` | `{ data }` | Keystrokes to PTY |
|
|
173
|
+
| `terminal_resize` | `{ cols, rows }` | Resize PTY |
|
|
174
|
+
|
|
175
|
+
**Server → Client:**
|
|
176
|
+
|
|
177
|
+
| Type | Payload | Purpose |
|
|
178
|
+
|------|---------|---------|
|
|
179
|
+
| `connected` | `{ projectId }` | Ready |
|
|
180
|
+
| `status` | `{ status }` | running/stopped/restarting |
|
|
181
|
+
| `terminal_data` | `{ data }` | PTY output |
|
|
182
|
+
| `terminal_subscribed` | `{}` | Subscription confirmed |
|
|
183
|
+
| `error` | `{ message }` | Error |
|
|
184
|
+
|
|
185
|
+
Localhost WebSocket connections are pre-authenticated — no `auth` message needed.
|
|
186
|
+
|
|
187
|
+
## REST API
|
|
188
|
+
|
|
189
|
+
| Method | Endpoint | Purpose |
|
|
190
|
+
|--------|----------|---------|
|
|
191
|
+
| `POST` | `/api/auth/login` | Login, returns JWT |
|
|
192
|
+
| `GET` | `/api/auth/local-token` | Get local token (localhost only) |
|
|
193
|
+
| `GET` | `/api/projects` | List all projects |
|
|
194
|
+
| `POST` | `/api/projects` | Create new project |
|
|
195
|
+
| `POST` | `/api/projects/open` | Open existing project by folder path |
|
|
196
|
+
| `DELETE` | `/api/projects/:id` | Delete project |
|
|
197
|
+
| `PATCH` | `/api/projects/:id/start` | Start project terminal |
|
|
198
|
+
| `PATCH` | `/api/projects/:id/stop` | Stop project terminal |
|
|
199
|
+
| `GET` | `/api/projects/:id/sessions` | List sessions |
|
|
200
|
+
| `GET` | `/api/projects/:id/sessions/:sid` | Get session with messages |
|
|
201
|
+
| `GET` | `/api/projects/activity` | Terminal activity timestamps |
|
|
202
|
+
| `GET` | `/api/projects/usage` | Claude Code usage stats |
|
|
203
|
+
| `GET/POST/PUT/DELETE` | `/api/shortcuts` | Global shortcut CRUD |
|
|
204
|
+
| `GET` | `/api/filesystem` | Browse directories |
|
|
205
|
+
| `POST` | `/api/filesystem/mkdir` | Create folder |
|
|
206
|
+
| `GET/PUT` | `/api/filesystem/file` | Read/write files |
|
|
207
|
+
| `GET` | `/api/update/check-running` | Check if processes are running |
|
|
208
|
+
| `POST` | `/api/update/prepare` | Save memory, wait idle, stop all |
|
|
209
|
+
|
|
210
|
+
## macOS Desktop App (Electron)
|
|
211
|
+
|
|
212
|
+
CC Web can be packaged as a standalone macOS app using Electron.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Install all dependencies first
|
|
216
|
+
npm run install:all
|
|
217
|
+
npm install
|
|
218
|
+
|
|
219
|
+
# Build DMG (outputs to release/)
|
|
220
|
+
npm run dist:dmg
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The DMG will be at `release/CCWeb-{version}-arm64.dmg`. Double-click to install.
|
|
224
|
+
|
|
225
|
+
On first launch, the app auto-generates login credentials and displays them in a dialog. Local access (localhost) skips login entirely. You'll need `claude` CLI installed and authenticated on your machine.
|
|
226
|
+
|
|
227
|
+
**Update flow**: Download zip from GitHub releases → extract with `ditto` → replace app via detached shell script (no code signing required).
|
|
228
|
+
|
|
229
|
+
### Building for other architectures
|
|
230
|
+
|
|
231
|
+
Edit the `arch` field in `package.json` under `build.mac.target`:
|
|
232
|
+
- `["arm64"]` — Apple Silicon (default)
|
|
233
|
+
- `["x64"]` — Intel Mac
|
|
234
|
+
- `["arm64", "x64"]` — Universal
|
|
235
|
+
|
|
236
|
+
## Server Deployment (without Electron)
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Build everything
|
|
240
|
+
npm run build
|
|
241
|
+
|
|
242
|
+
# Run backend (serves built frontend statically)
|
|
243
|
+
cd backend
|
|
244
|
+
npm start
|
|
245
|
+
|
|
246
|
+
# Or use pm2
|
|
247
|
+
pm2 start backend/dist/index.js --name cc-web
|
|
248
|
+
pm2 save
|
|
249
|
+
pm2 startup
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Environment variables:
|
|
253
|
+
|
|
254
|
+
| Variable | Purpose | Default |
|
|
255
|
+
|----------|---------|---------|
|
|
256
|
+
| `CCWEB_DATA_DIR` | Override data directory | `data/` relative to backend |
|
|
257
|
+
| `CCWEB_PORT` | Preferred server port | `3001` |
|
|
258
|
+
|
|
259
|
+
## Build & Release
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Full build (frontend + backend + electron)
|
|
263
|
+
npm run build:all
|
|
264
|
+
|
|
265
|
+
# Build DMG + ZIP for release
|
|
266
|
+
npm run dist
|
|
267
|
+
|
|
268
|
+
# Release checklist:
|
|
269
|
+
# 1. Bump version in package.json
|
|
270
|
+
# 2. Update currentVersion in frontend/src/components/UpdateButton.tsx
|
|
271
|
+
# 3. npm run dist
|
|
272
|
+
# 4. git add -A && git commit && git push
|
|
273
|
+
# 5. gh release create vX.Y.Z --title "CCWeb vX.Y.Z" \
|
|
274
|
+
# release/CCWeb-X.Y.Z-arm64.dmg \
|
|
275
|
+
# release/CCWeb-X.Y.Z-arm64-mac.zip \
|
|
276
|
+
# release/CCWeb-X.Y.Z-arm64-mac.zip.blockmap \
|
|
277
|
+
# release/latest-mac.yml
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
> **Note**: `productName` in `package.json` must not contain spaces (currently `CCWeb`), otherwise GitHub mangles the filename and auto-update gets 404.
|
|
281
|
+
|
|
282
|
+
## Development Guide
|
|
283
|
+
|
|
284
|
+
### Project Structure
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
cc-web/
|
|
288
|
+
├── package.json ← Root scripts + Electron build config
|
|
289
|
+
├── setup.js ← Interactive credential setup
|
|
290
|
+
├── electron/
|
|
291
|
+
│ ├── main.ts ← Electron main process
|
|
292
|
+
│ └── tsconfig.json
|
|
293
|
+
├── backend/
|
|
294
|
+
│ ├── package.json
|
|
295
|
+
│ ├── tsconfig.json
|
|
296
|
+
│ └── src/ ← TypeScript source
|
|
297
|
+
└── frontend/
|
|
298
|
+
├── package.json
|
|
299
|
+
├── tsconfig.json
|
|
300
|
+
├── vite.config.ts
|
|
301
|
+
├── tailwind.config.js
|
|
302
|
+
└── src/ ← React + TypeScript source
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Key Design Decisions
|
|
306
|
+
|
|
307
|
+
- **PTY-first**: Spawns the real `claude` CLI via `node-pty` using the user's `$SHELL -ilc`. All Claude Code features (slash commands, MCP, hooks, etc.) work natively.
|
|
308
|
+
- **No database**: Pure JSON files, in-memory CRUD. Simple to understand, back up, and debug.
|
|
309
|
+
- **Per-project `.ccweb/`**: Data travels with the project folder, survives app reinstall.
|
|
310
|
+
- **Session tailing**: Reads Claude Code's native JSONL (`~/.claude/projects/`) rather than parsing PTY output.
|
|
311
|
+
- **Scrollback buffer**: 5 MB per terminal for client reconnect replay.
|
|
312
|
+
- **Session pruning**: Keeps latest 20 sessions per project, deletes oldest on new session start.
|
|
313
|
+
- **Zoom memory**: `FilePreviewDialog` persists zoom level per file path in `localStorage`.
|
|
314
|
+
- **Auto port switching**: Backend tries ports 3001–3020, reports actual port to Electron via IPC.
|
|
315
|
+
- **Localhost auto-auth**: Local requests skip JWT verification entirely.
|
|
316
|
+
|
|
317
|
+
### Adding a New API Endpoint
|
|
318
|
+
|
|
319
|
+
1. Add the route handler in `backend/src/routes/*.ts`
|
|
320
|
+
2. Auth is already applied — routes are mounted under `authMiddleware`
|
|
321
|
+
3. Add the typed call in `frontend/src/lib/api.ts`
|
|
322
|
+
4. Call it from your component
|
|
323
|
+
|
|
324
|
+
### Adding a New Frontend Page
|
|
325
|
+
|
|
326
|
+
1. Create `frontend/src/pages/YourPage.tsx`
|
|
327
|
+
2. Add a route in `frontend/src/App.tsx`
|
|
328
|
+
3. Use existing UI components from `frontend/src/components/ui/`
|
|
329
|
+
|
|
330
|
+
### Tech Stack
|
|
331
|
+
|
|
332
|
+
**Backend**: Node.js, Express, WebSocket (ws), node-pty, TypeScript
|
|
333
|
+
**Frontend**: React 18, Vite, Tailwind CSS, shadcn/ui, xterm.js, TypeScript
|
|
334
|
+
**Desktop**: Electron
|
|
335
|
+
**Auth**: JWT (bcryptjs for password hashing)
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export interface AuthRequest extends Request {
|
|
3
|
+
user?: {
|
|
4
|
+
username: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
/** Check if request originates from localhost */
|
|
8
|
+
export declare function isLocalRequest(req: Request): boolean;
|
|
9
|
+
export declare function verifyToken(token: string): {
|
|
10
|
+
username: string;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Generate a JWT for local access (no credentials needed) */
|
|
13
|
+
export declare function generateLocalToken(): string;
|
|
14
|
+
export declare function authMiddleware(req: AuthRequest, res: Response, next: NextFunction): void;
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI1D,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAOpD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQtE;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA2BxF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isLocalRequest = isLocalRequest;
|
|
37
|
+
exports.verifyToken = verifyToken;
|
|
38
|
+
exports.generateLocalToken = generateLocalToken;
|
|
39
|
+
exports.authMiddleware = authMiddleware;
|
|
40
|
+
const jwt = __importStar(require("jsonwebtoken"));
|
|
41
|
+
const config_1 = require("./config");
|
|
42
|
+
/** Check if request originates from localhost */
|
|
43
|
+
function isLocalRequest(req) {
|
|
44
|
+
const ip = req.ip || req.socket.remoteAddress || '';
|
|
45
|
+
return (ip === '127.0.0.1' ||
|
|
46
|
+
ip === '::1' ||
|
|
47
|
+
ip === '::ffff:127.0.0.1');
|
|
48
|
+
}
|
|
49
|
+
function verifyToken(token) {
|
|
50
|
+
try {
|
|
51
|
+
const config = (0, config_1.getConfig)();
|
|
52
|
+
const decoded = jwt.verify(token, config.jwtSecret);
|
|
53
|
+
return { username: decoded.username };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Generate a JWT for local access (no credentials needed) */
|
|
60
|
+
function generateLocalToken() {
|
|
61
|
+
const config = (0, config_1.getConfig)();
|
|
62
|
+
return jwt.sign({ username: config.username }, config.jwtSecret, { expiresIn: '30d' });
|
|
63
|
+
}
|
|
64
|
+
function authMiddleware(req, res, next) {
|
|
65
|
+
// Local access: auto-authenticate without token
|
|
66
|
+
if (isLocalRequest(req)) {
|
|
67
|
+
try {
|
|
68
|
+
const config = (0, config_1.getConfig)();
|
|
69
|
+
req.user = { username: config.username };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
req.user = { username: 'local' };
|
|
73
|
+
}
|
|
74
|
+
next();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Remote access: require Bearer token
|
|
78
|
+
const authHeader = req.headers['authorization'];
|
|
79
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
80
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const token = authHeader.slice(7);
|
|
84
|
+
const user = verifyToken(token);
|
|
85
|
+
if (!user) {
|
|
86
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
req.user = user;
|
|
90
|
+
next();
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASA,wCAOC;AAED,kCAQC;AAGD,gDAGC;AAED,wCA2BC;AA5DD,kDAAoC;AACpC,qCAAqC;AAMrC,iDAAiD;AACjD,SAAgB,cAAc,CAAC,GAAY;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;IACpD,OAAO,CACL,EAAE,KAAK,WAAW;QAClB,EAAE,KAAK,KAAK;QACZ,EAAE,KAAK,kBAAkB,CAC1B,CAAC;AACJ,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,kBAAS,GAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAyB,CAAC;QAC5E,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,8DAA8D;AAC9D,SAAgB,kBAAkB;IAChC,MAAM,MAAM,GAAG,IAAA,kBAAS,GAAE,CAAC;IAC3B,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;AACzF,CAAC;AAED,SAAgB,cAAc,CAAC,GAAgB,EAAE,GAAa,EAAE,IAAkB;IAChF,gDAAgD;IAChD,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAA,kBAAS,GAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QACnC,CAAC;QACD,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,sCAAsC;IACtC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAChD,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC5D,OAAO;IACT,CAAC;IACD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,IAAI,EAAE,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Project, Config, CliTool } from './types';
|
|
2
|
+
export interface GlobalShortcut {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
command: string;
|
|
6
|
+
parentId?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DATA_DIR: string;
|
|
9
|
+
export declare function initDataDirs(): void;
|
|
10
|
+
export declare function getConfig(): Config;
|
|
11
|
+
export declare function getProjects(): Project[];
|
|
12
|
+
export declare function saveProjects(projects: Project[]): void;
|
|
13
|
+
export declare function getProject(id: string): Project | undefined;
|
|
14
|
+
export declare function saveProject(project: Project): void;
|
|
15
|
+
export declare function deleteProject(id: string): void;
|
|
16
|
+
export declare function getGlobalShortcuts(): GlobalShortcut[];
|
|
17
|
+
export declare function saveGlobalShortcuts(shortcuts: GlobalShortcut[]): void;
|
|
18
|
+
export interface ProjectConfig {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
permissionMode: 'limited' | 'unlimited';
|
|
22
|
+
cliTool: CliTool;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function ccwebDir(folderPath: string): string;
|
|
26
|
+
export declare function ccwebSessionsDir(folderPath: string): string;
|
|
27
|
+
/** Write .ccweb/project.json into the project folder */
|
|
28
|
+
export declare function writeProjectConfig(folderPath: string, project: Project): void;
|
|
29
|
+
/** Read .ccweb/project.json from a project folder. Returns null if not found. */
|
|
30
|
+
export declare function readProjectConfig(folderPath: string): ProjectConfig | null;
|
|
31
|
+
/** Update .ccweb/project.json (partial update) */
|
|
32
|
+
export declare function updateProjectConfig(folderPath: string, updates: Partial<ProjectConfig>): void;
|
|
33
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEnD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,QAAQ,QAAmE,CAAC;AAYzF,wBAAgB,YAAY,IAAI,IAAI,CAOnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAMlC;AAED,wBAAgB,WAAW,IAAI,OAAO,EAAE,CAIvC;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAEtD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CASlD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,kBAAkB,IAAI,cAAc,EAAE,CAGrD;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI,CAErE;AAOD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,SAAS,GAAG,WAAW,CAAC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAW7E;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAQ1E;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAM7F"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DATA_DIR = void 0;
|
|
37
|
+
exports.initDataDirs = initDataDirs;
|
|
38
|
+
exports.getConfig = getConfig;
|
|
39
|
+
exports.getProjects = getProjects;
|
|
40
|
+
exports.saveProjects = saveProjects;
|
|
41
|
+
exports.getProject = getProject;
|
|
42
|
+
exports.saveProject = saveProject;
|
|
43
|
+
exports.deleteProject = deleteProject;
|
|
44
|
+
exports.getGlobalShortcuts = getGlobalShortcuts;
|
|
45
|
+
exports.saveGlobalShortcuts = saveGlobalShortcuts;
|
|
46
|
+
exports.ccwebDir = ccwebDir;
|
|
47
|
+
exports.ccwebSessionsDir = ccwebSessionsDir;
|
|
48
|
+
exports.writeProjectConfig = writeProjectConfig;
|
|
49
|
+
exports.readProjectConfig = readProjectConfig;
|
|
50
|
+
exports.updateProjectConfig = updateProjectConfig;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
exports.DATA_DIR = process.env.CCWEB_DATA_DIR || path.join(__dirname, '../../data');
|
|
54
|
+
/** Atomic write: write to temp file then rename, preventing corruption on crash */
|
|
55
|
+
function atomicWriteSync(filePath, data) {
|
|
56
|
+
const tmpPath = filePath + `.tmp.${process.pid}`;
|
|
57
|
+
fs.writeFileSync(tmpPath, data, 'utf-8');
|
|
58
|
+
fs.renameSync(tmpPath, filePath);
|
|
59
|
+
}
|
|
60
|
+
const CONFIG_FILE = path.join(exports.DATA_DIR, 'config.json');
|
|
61
|
+
const PROJECTS_FILE = path.join(exports.DATA_DIR, 'projects.json');
|
|
62
|
+
const SHORTCUTS_FILE = path.join(exports.DATA_DIR, 'global-shortcuts.json');
|
|
63
|
+
function initDataDirs() {
|
|
64
|
+
if (!fs.existsSync(exports.DATA_DIR)) {
|
|
65
|
+
fs.mkdirSync(exports.DATA_DIR, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
if (!fs.existsSync(PROJECTS_FILE)) {
|
|
68
|
+
fs.writeFileSync(PROJECTS_FILE, JSON.stringify([], null, 2), 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function getConfig() {
|
|
72
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
73
|
+
throw new Error('Config file not found. Please run: npm run setup');
|
|
74
|
+
}
|
|
75
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
function getProjects() {
|
|
79
|
+
if (!fs.existsSync(PROJECTS_FILE))
|
|
80
|
+
return [];
|
|
81
|
+
const raw = fs.readFileSync(PROJECTS_FILE, 'utf-8');
|
|
82
|
+
return JSON.parse(raw);
|
|
83
|
+
}
|
|
84
|
+
function saveProjects(projects) {
|
|
85
|
+
atomicWriteSync(PROJECTS_FILE, JSON.stringify(projects, null, 2));
|
|
86
|
+
}
|
|
87
|
+
function getProject(id) {
|
|
88
|
+
return getProjects().find((p) => p.id === id);
|
|
89
|
+
}
|
|
90
|
+
function saveProject(project) {
|
|
91
|
+
const projects = getProjects();
|
|
92
|
+
const index = projects.findIndex((p) => p.id === project.id);
|
|
93
|
+
if (index >= 0) {
|
|
94
|
+
projects[index] = project;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
projects.push(project);
|
|
98
|
+
}
|
|
99
|
+
saveProjects(projects);
|
|
100
|
+
}
|
|
101
|
+
function deleteProject(id) {
|
|
102
|
+
saveProjects(getProjects().filter((p) => p.id !== id));
|
|
103
|
+
}
|
|
104
|
+
function getGlobalShortcuts() {
|
|
105
|
+
if (!fs.existsSync(SHORTCUTS_FILE))
|
|
106
|
+
return [];
|
|
107
|
+
return JSON.parse(fs.readFileSync(SHORTCUTS_FILE, 'utf-8'));
|
|
108
|
+
}
|
|
109
|
+
function saveGlobalShortcuts(shortcuts) {
|
|
110
|
+
atomicWriteSync(SHORTCUTS_FILE, JSON.stringify(shortcuts, null, 2));
|
|
111
|
+
}
|
|
112
|
+
// ── .ccweb/ per-project config ────────────────────────────────────────────────
|
|
113
|
+
const CCWEB_DIR = '.ccweb';
|
|
114
|
+
const PROJECT_CONFIG_FILE = 'project.json';
|
|
115
|
+
function ccwebDir(folderPath) {
|
|
116
|
+
return path.join(folderPath, CCWEB_DIR);
|
|
117
|
+
}
|
|
118
|
+
function ccwebSessionsDir(folderPath) {
|
|
119
|
+
return path.join(folderPath, CCWEB_DIR, 'sessions');
|
|
120
|
+
}
|
|
121
|
+
/** Write .ccweb/project.json into the project folder */
|
|
122
|
+
function writeProjectConfig(folderPath, project) {
|
|
123
|
+
const dir = ccwebDir(folderPath);
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
const config = {
|
|
126
|
+
id: project.id,
|
|
127
|
+
name: project.name,
|
|
128
|
+
permissionMode: project.permissionMode,
|
|
129
|
+
cliTool: project.cliTool,
|
|
130
|
+
createdAt: project.createdAt,
|
|
131
|
+
};
|
|
132
|
+
atomicWriteSync(path.join(dir, PROJECT_CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
133
|
+
}
|
|
134
|
+
/** Read .ccweb/project.json from a project folder. Returns null if not found. */
|
|
135
|
+
function readProjectConfig(folderPath) {
|
|
136
|
+
const file = path.join(ccwebDir(folderPath), PROJECT_CONFIG_FILE);
|
|
137
|
+
if (!fs.existsSync(file))
|
|
138
|
+
return null;
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Update .ccweb/project.json (partial update) */
|
|
147
|
+
function updateProjectConfig(folderPath, updates) {
|
|
148
|
+
const existing = readProjectConfig(folderPath);
|
|
149
|
+
if (!existing)
|
|
150
|
+
return;
|
|
151
|
+
const merged = { ...existing, ...updates };
|
|
152
|
+
const dir = ccwebDir(folderPath);
|
|
153
|
+
atomicWriteSync(path.join(dir, PROJECT_CONFIG_FILE), JSON.stringify(merged, null, 2));
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=config.js.map
|