Skip to content

Commit 187a55f

Browse files
feat: implement daemon for service management with IPC
- Added a new daemon package to manage services and client connections via Unix sockets. - Implemented message protocol for communication between clients and the daemon. - Integrated daemon client into the MCP server for service control. - Refactored TUI to support both legacy runner and daemon client modes. - Created playground environment for testing daemon functionality with example services. - Added scripts for example services to demonstrate logging and error handling.
1 parent a8e657c commit 187a55f

File tree

18 files changed

+1425
-149
lines changed

18 files changed

+1425
-149
lines changed

.claude/settings.local.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(go build:*)",
5+
"Bash(go vet:*)",
6+
"Bash(make build:*)"
7+
]
8+
},
9+
"enableAllProjectMcpServers": true,
10+
"enabledMcpjsonServers": [
11+
"devir"
12+
]
13+
}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ debug/
3636
*.tar.gz
3737
*.zip
3838
checksums.txt
39+
!.claude
40+
!CLAUDE.md

CLAUDE.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
make build # Build binary to ./devir
9+
make build-all # Build for all platforms (dist/)
10+
make test # Run tests
11+
make lint # Run golangci-lint
12+
make install # Build and copy to /usr/local/bin
13+
make clean # Remove build artifacts
14+
```
15+
16+
## Architecture
17+
18+
Devir is a terminal UI for managing multiple dev services. Written in Go using Bubble Tea TUI framework.
19+
20+
### Core Components
21+
22+
```
23+
cmd/devir/main.go # Entry point, CLI flags, mode selection (TUI vs MCP)
24+
internal/
25+
config/ # YAML config loading, service validation
26+
types/ # Shared types (LogLine, LogEntry, ServiceInfo)
27+
runner/ # Service lifecycle management, log processing
28+
tui/ # Bubble Tea model, view rendering, keyboard handling
29+
mcp/ # MCP server for Claude Code integration
30+
```
31+
32+
### Key Patterns
33+
34+
**Runner** (`internal/runner/runner.go`):
35+
- Manages service processes with platform-specific handling (Unix/Windows)
36+
- Channels for log streaming: `LogChan` (simple mode), `LogEntryChan` (TUI mode)
37+
- Log filtering via compiled regex (`filter`, `exclude` patterns)
38+
- Port detection and process killing via `process_unix.go`/`process_windows.go`
39+
40+
**TUI Model** (`internal/tui/`):
41+
- Bubble Tea architecture: Model → Update → View cycle
42+
- Tab-based service filtering (`activeTab`: -1=all, 0+=specific)
43+
- Auto-scroll with manual override on user scroll
44+
- 50ms tick for log collection
45+
46+
**MCP Server** (`internal/mcp/mcp.go`):
47+
- Exposes 7 tools: `devir_start`, `devir_stop`, `devir_status`, `devir_logs`, `devir_restart`, `devir_check_ports`, `devir_kill_ports`
48+
- Uses `go-sdk/mcp` with stdio transport
49+
- Same runner infrastructure as TUI mode
50+
51+
### Configuration
52+
53+
Config file: `devir.yaml` (searched in current dir and parents)
54+
55+
```yaml
56+
services:
57+
name:
58+
dir: relative/path # Working directory
59+
cmd: command to run # Command to execute
60+
port: 3000 # Port for status display
61+
color: blue # Log prefix color
62+
63+
defaults:
64+
- name # Services to start by default
65+
```
66+
67+
### Dependencies
68+
69+
- `github.com/charmbracelet/bubbletea` - TUI framework
70+
- `github.com/charmbracelet/bubbles` - TUI components (viewport, textinput)
71+
- `github.com/charmbracelet/lipgloss` - Styling
72+
- `github.com/modelcontextprotocol/go-sdk` - MCP server
73+
- `gopkg.in/yaml.v3` - Config parsing

cmd/devir/main.go

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
tea "github.com/charmbracelet/bubbletea"
99

1010
"devir/internal/config"
11+
"devir/internal/daemon"
1112
"devir/internal/mcp"
1213
"devir/internal/runner"
1314
"devir/internal/tui"
@@ -54,17 +55,62 @@ func main() {
5455
os.Exit(1)
5556
}
5657

58+
// Get socket path based on config directory
59+
socketPath := daemon.SocketPath(cfg.RootDir)
60+
5761
// MCP mode
5862
if mcpMode {
59-
mcpServer := mcp.New(cfg, Version)
63+
runMCPMode(cfg, socketPath)
64+
return
65+
}
66+
67+
// TUI mode
68+
runTUIMode(cfg, socketPath)
69+
}
70+
71+
func runMCPMode(cfg *config.Config, socketPath string) {
72+
// Check if daemon already exists
73+
if daemon.Exists(socketPath) {
74+
// Connect to existing daemon
75+
client, err := daemon.Connect(socketPath)
76+
if err != nil {
77+
fmt.Fprintf(os.Stderr, "Failed to connect to daemon: %v\n", err)
78+
os.Exit(1)
79+
}
80+
defer client.Close()
81+
82+
mcpServer := mcp.NewWithClient(cfg, client, Version)
6083
if err := mcpServer.Run(); err != nil {
6184
fmt.Fprintf(os.Stderr, "MCP error: %v\n", err)
6285
os.Exit(1)
6386
}
6487
return
6588
}
6689

67-
// TUI mode
90+
// Start new daemon + MCP
91+
d := daemon.New(cfg, socketPath)
92+
if err := d.Start(); err != nil {
93+
fmt.Fprintf(os.Stderr, "Failed to start daemon: %v\n", err)
94+
os.Exit(1)
95+
}
96+
defer d.Stop()
97+
98+
// Connect as client
99+
client, err := daemon.Connect(socketPath)
100+
if err != nil {
101+
fmt.Fprintf(os.Stderr, "Failed to connect to daemon: %v\n", err)
102+
os.Exit(1)
103+
}
104+
defer client.Close()
105+
106+
mcpServer := mcp.NewWithClient(cfg, client, Version)
107+
if err := mcpServer.Run(); err != nil {
108+
fmt.Fprintf(os.Stderr, "MCP error: %v\n", err)
109+
os.Exit(1)
110+
}
111+
}
112+
113+
func runTUIMode(cfg *config.Config, socketPath string) {
68114
services := flag.Args()
69115
if len(services) == 0 {
70116
services = cfg.Defaults
@@ -83,11 +129,43 @@ func main() {
83129
}
84130
}
85131

86-
// Create runner
87-
r := runner.New(cfg, services, filter, exclude)
132+
// Check if daemon already exists
133+
if daemon.Exists(socketPath) {
134+
// Connect to existing daemon (services already running)
135+
client, err := daemon.Connect(socketPath)
136+
if err != nil {
137+
fmt.Fprintf(os.Stderr, "Failed to connect to daemon: %v\n", err)
138+
os.Exit(1)
139+
}
140+
defer client.Close()
141+
142+
// Start TUI with client
143+
p := tea.NewProgram(
144+
tui.NewWithClient(client, services, cfg),
145+
tea.WithAltScreen(),
146+
tea.WithMouseCellMotion(),
147+
)
148+
149+
if _, err := p.Run(); err != nil {
150+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
151+
os.Exit(1)
152+
}
153+
return
154+
}
155+
156+
// No existing daemon - start new daemon + TUI
157+
d := daemon.New(cfg, socketPath)
158+
if err := d.Start(); err != nil {
159+
fmt.Fprintf(os.Stderr, "Failed to start daemon: %v\n", err)
160+
os.Exit(1)
161+
}
162+
defer d.Stop()
88163

89164
// Check for ports in use
165+
r := runner.New(cfg, services, filter, exclude)
90166
portsInUse := r.CheckPorts()
167+
killPorts := false
168+
91169
if len(portsInUse) > 0 {
92170
fmt.Println("\n⚠️ Aşağıdaki portlar zaten kullanımda:")
93171
for name, port := range portsInUse {
@@ -99,20 +177,28 @@ func main() {
99177
_, _ = fmt.Scanln(&answer)
100178

101179
if answer == "y" || answer == "Y" {
102-
for name, port := range portsInUse {
103-
if err := r.KillPort(port); err != nil {
104-
fmt.Printf(" ✗ %s (port %d) kapatılamadı: %v\n", name, port, err)
105-
} else {
106-
fmt.Printf(" ✓ %s (port %d) kapatıldı\n", name, port)
107-
}
108-
}
109-
fmt.Println()
180+
killPorts = true
110181
}
111182
}
112183

184+
// Connect as client
185+
client, err := daemon.Connect(socketPath)
186+
if err != nil {
187+
fmt.Fprintf(os.Stderr, "Failed to connect to daemon: %v\n", err)
188+
os.Exit(1)
189+
}
190+
defer client.Close()
191+
192+
// Start services via daemon
193+
_, err = client.StartAndWait(services, killPorts, 10*1e9) // 10 seconds timeout
194+
if err != nil {
195+
fmt.Fprintf(os.Stderr, "Failed to start services: %v\n", err)
196+
os.Exit(1)
197+
}
198+
113199
// Start TUI
114200
p := tea.NewProgram(
115-
tui.New(r),
201+
tui.NewWithClient(client, services, cfg),
116202
tea.WithAltScreen(),
117203
tea.WithMouseCellMotion(),
118204
)
@@ -133,6 +219,7 @@ Options:
133219
-c <file> Config file path (default: devir.yaml)
134220
-filter <p> Show only logs matching pattern
135221
-exclude <p> Hide logs matching pattern
222+
-mcp Run as MCP server (daemon mode)
136223
-v Show version
137224
-h Show this help
138225
@@ -142,6 +229,10 @@ Examples:
142229
devir --filter "error" # Show only errors
143230
devir --exclude "hmr" # Hide HMR logs
144231
232+
Daemon Mode:
233+
Multiple TUI/MCP clients can connect to same daemon.
234+
First instance starts daemon, others connect automatically.
235+
145236
Keyboard Shortcuts:
146237
Tab Cycle through services
147238
1-9 Select specific service

0 commit comments

Comments
 (0)