ccdock 0.1.0
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 +195 -0
- package/package.json +48 -0
- package/src/agent/hook.ts +134 -0
- package/src/config/config.ts +58 -0
- package/src/main.ts +65 -0
- package/src/sidebar.ts +556 -0
- package/src/tui/ansi.ts +148 -0
- package/src/tui/input.ts +116 -0
- package/src/tui/render.ts +321 -0
- package/src/tui/wizard.ts +166 -0
- package/src/types.ts +86 -0
- package/src/workspace/editor.ts +32 -0
- package/src/workspace/state.ts +150 -0
- package/src/workspace/window.ts +323 -0
- package/src/worktree/manager.ts +120 -0
- package/src/worktree/scanner.ts +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cchub contributors
|
|
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,195 @@
|
|
|
1
|
+
# ccdock
|
|
2
|
+
|
|
3
|
+
A TUI sidebar to orchestrate VS Code windows and track Claude Code agents.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Running multiple Claude Code agents across git worktrees is powerful — but managing the VS Code windows that go with them is a nightmare. You end up Alt-Tabbing through a dozen windows, losing track of which agent is doing what, and manually arranging editors every time you switch context.
|
|
8
|
+
|
|
9
|
+
Existing "hub" tools either force you into a CLI-only workflow or require a proprietary editor. But you already have VS Code. You just need something to **keep it organized**.
|
|
10
|
+
|
|
11
|
+
ccdock sits in a narrow terminal sidebar and takes care of the rest: auto-positioning VS Code windows, tracking every Claude Code agent in real time, and letting you switch between sessions with a single click.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **VS Code orchestration** — Auto-open, position, and switch VS Code (or Cursor) windows next to the sidebar. Click a session, and the right editor snaps into focus.
|
|
16
|
+
- **Real-time agent monitoring** — See exactly what each Claude Code agent is doing: which tool it's calling, what file it's reading, what command it's running.
|
|
17
|
+
- **Git worktree management** — Create, switch, and delete worktrees via [git-wt](https://github.com/k1LoW/git-wt) integration. Each worktree gets its own session.
|
|
18
|
+
- **Activity log** — Live feed of tool invocations with session numbers (#N) across all active agents.
|
|
19
|
+
- **Mouse + keyboard** — Click to select sessions, scroll wheel to navigate, or use vim-style `j`/`k` keys.
|
|
20
|
+
- **Auto-layout** — VS Code windows automatically resize and reposition when the terminal resizes.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- **macOS** (uses AppleScript for window management)
|
|
25
|
+
- [Bun](https://bun.sh/) runtime (v1.0+)
|
|
26
|
+
- [VS Code](https://code.visualstudio.com/) or [Cursor](https://cursor.sh/)
|
|
27
|
+
- [git-wt](https://github.com/k1LoW/git-wt) for worktree creation (`go install github.com/k1LoW/git-wt@latest`)
|
|
28
|
+
- [Ghostty](https://ghostty.org/) terminal (used for sidebar window detection)
|
|
29
|
+
- A terminal font with [Nerd Font](https://www.nerdfonts.com/) support (for icons)
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# ccdock itself
|
|
35
|
+
bun install -g ccdock
|
|
36
|
+
|
|
37
|
+
# git-wt (required for worktree creation)
|
|
38
|
+
go install github.com/k1LoW/git-wt@latest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
### 1. Configure workspace directories
|
|
44
|
+
|
|
45
|
+
Edit `~/.config/ccdock/config.json` (auto-created on first run):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"workspace_dirs": ["~/workspace"],
|
|
50
|
+
"editor": "code"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Key | Description |
|
|
55
|
+
| ---------------- | --------------------------------------------------------- |
|
|
56
|
+
| `workspace_dirs` | Directories to scan for git repositories |
|
|
57
|
+
| `editor` | Editor command: `"code"` for VS Code, `"cursor"` for Cursor |
|
|
58
|
+
|
|
59
|
+
### 2. Set up Claude Code hooks
|
|
60
|
+
|
|
61
|
+
Add to `~/.claude/settings.json` to enable agent status monitoring:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"hooks": {
|
|
66
|
+
"PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PreToolUse" }] }],
|
|
67
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PostToolUse" }] }],
|
|
68
|
+
"PermissionRequest": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PermissionRequest" }] }],
|
|
69
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Stop" }] }],
|
|
70
|
+
"Notification": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Notification" }] }],
|
|
71
|
+
"SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code SessionEnd" }] }]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
ccdock # start the sidebar TUI
|
|
80
|
+
ccdock help # show help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Keybindings
|
|
84
|
+
|
|
85
|
+
| Key | Action |
|
|
86
|
+
| ------------ | --------------------------------------- |
|
|
87
|
+
| `j` / `k` | Navigate between sessions |
|
|
88
|
+
| `Enter` | Focus editor window for selected session |
|
|
89
|
+
| `Tab` | Focus editor window (same as Enter) |
|
|
90
|
+
| `n` | Create new session (interactive wizard) |
|
|
91
|
+
| `d` | Delete session |
|
|
92
|
+
| `r` | Realign all VS Code windows |
|
|
93
|
+
| `c` | Toggle compact mode |
|
|
94
|
+
| `l` | Toggle activity log |
|
|
95
|
+
| `q` / Ctrl+C | Quit (with option to close editors) |
|
|
96
|
+
| Mouse click | Select session |
|
|
97
|
+
| Scroll wheel | Navigate between sessions |
|
|
98
|
+
|
|
99
|
+
### Session card states
|
|
100
|
+
|
|
101
|
+
| Card appearance | Meaning |
|
|
102
|
+
| --------------- | ------- |
|
|
103
|
+
| White border + green `●` | Editor is focused |
|
|
104
|
+
| Normal border + green `●` | Editor is open but not focused |
|
|
105
|
+
| Spinning `⠋` indicator | Editor is launching |
|
|
106
|
+
| Dim border, no dot | Editor is closed |
|
|
107
|
+
|
|
108
|
+
### Agent status
|
|
109
|
+
|
|
110
|
+
| Icon | Status | Description |
|
|
111
|
+
| ---- | ------ | ----------- |
|
|
112
|
+
| `●` green | running | Agent is executing tools |
|
|
113
|
+
| `●`/`○` yellow pulse | waiting | Awaiting user permission |
|
|
114
|
+
| `○` teal | idle | Agent is ready |
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
### Architecture
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Claude Code hooks --> ccdock hook --> writes agent JSON files
|
|
122
|
+
|
|
|
123
|
+
ccdock sidebar (polls every 2s) <----------+
|
|
124
|
+
|
|
|
125
|
+
+--> reads session + agent state files
|
|
126
|
+
+--> queries VS Code windows via AppleScript
|
|
127
|
+
+--> renders TUI with merged state
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **State** — `~/.local/state/ccdock/` stores session and agent state as JSON files
|
|
131
|
+
- **Hooks** — `ccdock hook` writes agent state files when Claude Code fires events
|
|
132
|
+
- **Window management** — AppleScript via `osascript` to position VS Code next to the sidebar
|
|
133
|
+
- **Wizard** — `n` key scans workspace dirs, offers create/existing/root worktree options via `git wt`
|
|
134
|
+
|
|
135
|
+
### File structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
src/
|
|
139
|
+
main.ts — CLI entry point
|
|
140
|
+
sidebar.ts — Main event loop, input handling
|
|
141
|
+
types.ts — Type definitions
|
|
142
|
+
config/config.ts — Config (~/.config/ccdock/)
|
|
143
|
+
workspace/state.ts — Session/agent state persistence
|
|
144
|
+
workspace/editor.ts — VS Code open/focus
|
|
145
|
+
workspace/window.ts — AppleScript window management
|
|
146
|
+
worktree/manager.ts — Git worktree operations
|
|
147
|
+
worktree/scanner.ts — Repository discovery
|
|
148
|
+
tui/render.ts — Sidebar rendering
|
|
149
|
+
tui/wizard.ts — Session wizard rendering
|
|
150
|
+
tui/input.ts — Keyboard input parsing
|
|
151
|
+
tui/ansi.ts — ANSI escape codes
|
|
152
|
+
agent/hook.ts — Hook handler
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# Clone
|
|
159
|
+
git clone https://github.com/shibutani/ccdock.git
|
|
160
|
+
cd ccdock
|
|
161
|
+
bun install
|
|
162
|
+
|
|
163
|
+
# Run directly
|
|
164
|
+
bun run dev
|
|
165
|
+
|
|
166
|
+
# Type check
|
|
167
|
+
bun run typecheck
|
|
168
|
+
|
|
169
|
+
# Format
|
|
170
|
+
bun run format
|
|
171
|
+
|
|
172
|
+
# Build standalone binary (optional)
|
|
173
|
+
bun run build
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Project structure
|
|
177
|
+
|
|
178
|
+
The project uses [Bun](https://bun.sh/) as runtime and [Biome](https://biomejs.dev/) for formatting.
|
|
179
|
+
|
|
180
|
+
- `src/main.ts` — CLI entry point, routes `start` / `hook` / `help` commands
|
|
181
|
+
- `src/sidebar.ts` — Main event loop: keyboard input, timers, state refresh
|
|
182
|
+
- `src/tui/` — Terminal UI rendering (cards, wizard, input parsing, ANSI codes)
|
|
183
|
+
- `src/workspace/` — File-based state, AppleScript window management, editor control
|
|
184
|
+
- `src/worktree/` — Git worktree operations and repository scanning
|
|
185
|
+
- `src/agent/` — Claude Code hook handler
|
|
186
|
+
|
|
187
|
+
### Publishing
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
npm publish
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccdock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A TUI sidebar to orchestrate VS Code windows and track Claude Code agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ccdock": "src/main.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun run src/main.ts",
|
|
11
|
+
"build": "bun build --compile src/main.ts --outfile ccdock",
|
|
12
|
+
"typecheck": "bunx tsc --noEmit",
|
|
13
|
+
"format": "bunx @biomejs/biome format --write src/"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"worktree",
|
|
18
|
+
"git",
|
|
19
|
+
"tui",
|
|
20
|
+
"sidebar",
|
|
21
|
+
"vscode",
|
|
22
|
+
"terminal"
|
|
23
|
+
],
|
|
24
|
+
"author": "shibutani",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/shibukazu/ccdock"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"bun": ">=1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin"
|
|
35
|
+
],
|
|
36
|
+
"files": [
|
|
37
|
+
"src/**/*.ts",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@biomejs/biome": "^1.9.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { AgentState, AgentType } from "../types.ts";
|
|
2
|
+
import {
|
|
3
|
+
findSessionByPath,
|
|
4
|
+
readAgentState,
|
|
5
|
+
removeAgentFile,
|
|
6
|
+
writeAgentState,
|
|
7
|
+
} from "../workspace/state.ts";
|
|
8
|
+
|
|
9
|
+
function sanitize(path: string): string {
|
|
10
|
+
return path.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 100);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extractToolDetail(toolName: string, toolInput: Record<string, unknown>): string {
|
|
14
|
+
switch (toolName) {
|
|
15
|
+
case "Bash":
|
|
16
|
+
return (toolInput.command as string) ?? "";
|
|
17
|
+
case "Read":
|
|
18
|
+
case "Write":
|
|
19
|
+
return shortenPath((toolInput.file_path as string) ?? "");
|
|
20
|
+
case "Edit":
|
|
21
|
+
return shortenPath((toolInput.file_path as string) ?? "");
|
|
22
|
+
case "Grep":
|
|
23
|
+
return `/${(toolInput.pattern as string) ?? ""}/`;
|
|
24
|
+
case "Glob":
|
|
25
|
+
return (toolInput.pattern as string) ?? "";
|
|
26
|
+
case "Agent":
|
|
27
|
+
case "Task":
|
|
28
|
+
return (toolInput.description as string) ?? (toolInput.prompt as string)?.slice(0, 80) ?? "";
|
|
29
|
+
case "WebSearch":
|
|
30
|
+
return (toolInput.query as string) ?? "";
|
|
31
|
+
case "WebFetch":
|
|
32
|
+
return (toolInput.url as string) ?? "";
|
|
33
|
+
default:
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shortenPath(path: string): string {
|
|
39
|
+
const home = process.env.HOME ?? "";
|
|
40
|
+
if (home && path.startsWith(home)) {
|
|
41
|
+
return `~${path.slice(home.length)}`;
|
|
42
|
+
}
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildPrompt(eventName: string, payload: Record<string, unknown>): string {
|
|
47
|
+
if (eventName === "PreToolUse" || eventName === "PostToolUse") {
|
|
48
|
+
const toolName = (payload.tool_name as string) ?? "";
|
|
49
|
+
return `[${eventName}] ${toolName}`;
|
|
50
|
+
}
|
|
51
|
+
if (eventName === "Stop") {
|
|
52
|
+
const reason = (payload.stop_reason as string) ?? "completed";
|
|
53
|
+
return `[Stop] ${reason}`;
|
|
54
|
+
}
|
|
55
|
+
return `[${eventName}]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STATUS_MAP: Record<string, string> = {
|
|
59
|
+
PreToolUse: "running",
|
|
60
|
+
PostToolUse: "running",
|
|
61
|
+
SubagentToolUse: "running",
|
|
62
|
+
PermissionRequest: "waiting",
|
|
63
|
+
Stop: "idle",
|
|
64
|
+
SessionEnd: "remove",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export async function handleHook(agentType: string, eventName: string): Promise<void> {
|
|
68
|
+
let payload: Record<string, unknown> = {};
|
|
69
|
+
try {
|
|
70
|
+
const input = await Bun.stdin.text();
|
|
71
|
+
if (input.trim()) {
|
|
72
|
+
payload = JSON.parse(input) as Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// No stdin or invalid JSON - continue with empty payload
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const cwd = (payload.cwd as string) ?? process.cwd();
|
|
79
|
+
const claudeSessionId = payload.session_id as string | undefined;
|
|
80
|
+
|
|
81
|
+
if (!claudeSessionId) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const filename = `${sanitize(cwd)}-${claudeSessionId}.json`;
|
|
86
|
+
|
|
87
|
+
const mappedStatus = STATUS_MAP[eventName];
|
|
88
|
+
|
|
89
|
+
if (mappedStatus === "remove") {
|
|
90
|
+
removeAgentFile(filename);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Notification doesn't carry meaningful status info — preserve previous state
|
|
95
|
+
if (eventName === "Notification") {
|
|
96
|
+
const prev = readAgentState(filename);
|
|
97
|
+
if (prev) {
|
|
98
|
+
prev.updatedAt = Date.now();
|
|
99
|
+
writeAgentState(prev, filename);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const status = mappedStatus ?? "unknown";
|
|
105
|
+
const sessionId = findSessionByPath(cwd);
|
|
106
|
+
const rawToolName = (payload.tool_name as string) ?? "";
|
|
107
|
+
const toolInput = (payload.tool_input as Record<string, unknown>) ?? {};
|
|
108
|
+
const rawToolDetail = extractToolDetail(rawToolName, toolInput);
|
|
109
|
+
|
|
110
|
+
// Preserve previous toolName/toolDetail only for events that don't carry tool info
|
|
111
|
+
// but clear them on Stop (idle) since the agent is no longer doing anything
|
|
112
|
+
let toolName = rawToolName;
|
|
113
|
+
let toolDetail = rawToolDetail;
|
|
114
|
+
if (!toolName && status !== "idle") {
|
|
115
|
+
const prev = readAgentState(filename);
|
|
116
|
+
if (prev) {
|
|
117
|
+
toolName = prev.toolName ?? "";
|
|
118
|
+
toolDetail = prev.toolDetail ?? "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const state: AgentState = {
|
|
123
|
+
sessionId,
|
|
124
|
+
agentType: agentType as AgentType,
|
|
125
|
+
status: status as AgentState["status"],
|
|
126
|
+
prompt: buildPrompt(eventName, payload),
|
|
127
|
+
toolName,
|
|
128
|
+
toolDetail,
|
|
129
|
+
cwd,
|
|
130
|
+
updatedAt: Date.now(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
writeAgentState(state, filename);
|
|
134
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { HubConfig } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR_NAME = "ccdock";
|
|
6
|
+
|
|
7
|
+
function getConfigDir(): string {
|
|
8
|
+
const home = process.env.HOME ?? "";
|
|
9
|
+
const configBase = process.env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
10
|
+
return join(configBase, CONFIG_DIR_NAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getConfigPath(): string {
|
|
14
|
+
return join(getConfigDir(), "config.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function expandTilde(p: string): string {
|
|
18
|
+
const home = process.env.HOME ?? "";
|
|
19
|
+
if (p.startsWith("~/")) {
|
|
20
|
+
return join(home, p.slice(2));
|
|
21
|
+
}
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONFIG: HubConfig = {
|
|
26
|
+
workspace_dirs: ["~/workspace"],
|
|
27
|
+
editor: "code",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function loadConfig(): HubConfig {
|
|
31
|
+
const configDir = getConfigDir();
|
|
32
|
+
const configPath = getConfigPath();
|
|
33
|
+
|
|
34
|
+
// Auto-create config directory and file on first run
|
|
35
|
+
if (!existsSync(configDir)) {
|
|
36
|
+
mkdirSync(configDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!existsSync(configPath)) {
|
|
40
|
+
writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
41
|
+
return resolveConfig(DEFAULT_CONFIG);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
46
|
+
const parsed = JSON.parse(raw) as HubConfig;
|
|
47
|
+
return resolveConfig(parsed);
|
|
48
|
+
} catch {
|
|
49
|
+
return resolveConfig(DEFAULT_CONFIG);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveConfig(config: HubConfig): HubConfig {
|
|
54
|
+
return {
|
|
55
|
+
workspace_dirs: config.workspace_dirs.map(expandTilde).filter((dir) => existsSync(dir)),
|
|
56
|
+
editor: config.editor ?? "code",
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { handleHook } from "./agent/hook.ts";
|
|
3
|
+
import { runSidebar } from "./sidebar.ts";
|
|
4
|
+
|
|
5
|
+
function printHelp(): void {
|
|
6
|
+
const help = `
|
|
7
|
+
ccdock - TUI sidebar for managing git worktree development sessions
|
|
8
|
+
|
|
9
|
+
USAGE:
|
|
10
|
+
ccdock [command]
|
|
11
|
+
|
|
12
|
+
COMMANDS:
|
|
13
|
+
start Start the sidebar TUI (default)
|
|
14
|
+
hook Handle agent hook events (called by Claude Code hooks)
|
|
15
|
+
help Show this help message
|
|
16
|
+
|
|
17
|
+
HOOK USAGE:
|
|
18
|
+
ccdock hook <agent-type> <event-name>
|
|
19
|
+
|
|
20
|
+
Agent types: claude-code, codex
|
|
21
|
+
Events: PreToolUse, PostToolUse, Stop, session.end, Notification
|
|
22
|
+
|
|
23
|
+
KEYBINDINGS (sidebar):
|
|
24
|
+
j/k Navigate sessions
|
|
25
|
+
Enter/Tab Focus editor window for selected session
|
|
26
|
+
n Create new session (wizard)
|
|
27
|
+
d Delete session
|
|
28
|
+
c Toggle compact mode
|
|
29
|
+
l Toggle activity log
|
|
30
|
+
q/Ctrl+C Quit sidebar
|
|
31
|
+
|
|
32
|
+
CONFIG:
|
|
33
|
+
~/.config/ccdock/config.json
|
|
34
|
+
|
|
35
|
+
STATE:
|
|
36
|
+
~/.local/state/ccdock/
|
|
37
|
+
`.trim();
|
|
38
|
+
|
|
39
|
+
console.log(help);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const [command, ...args] = process.argv.slice(2);
|
|
44
|
+
|
|
45
|
+
switch (command) {
|
|
46
|
+
case "start":
|
|
47
|
+
case undefined:
|
|
48
|
+
await runSidebar();
|
|
49
|
+
break;
|
|
50
|
+
case "hook":
|
|
51
|
+
await handleHook(args[0] ?? "claude-code", args[1] ?? "unknown");
|
|
52
|
+
break;
|
|
53
|
+
case "help":
|
|
54
|
+
case "--help":
|
|
55
|
+
case "-h":
|
|
56
|
+
printHelp();
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
console.error(`Unknown command: ${command}`);
|
|
60
|
+
printHelp();
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await main();
|