Files
slopus--happy/docs/cli-architecture.md
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

382 lines
11 KiB
Markdown

# CLI Architecture
This document describes the Happy CLI (`packages/happy-cli`) and its daemon. The CLI is both an interactive tool and a background session manager that keeps machine state in sync with the server.
## System overview
```mermaid
graph TB
subgraph "Happy CLI"
Entry[src/index.ts]
API[API Client]
Daemon[Daemon Process]
Agents[Agent Runners]
Persist[Persistence]
end
subgraph "~/.happy"
Settings[settings.json]
AccessKey[access.key]
DaemonState[daemon.state.json]
Logs[logs/]
end
subgraph Server
HTTP[HTTP API]
Socket[Socket.IO]
end
Entry --> API
Entry --> Daemon
Entry --> Agents
Entry --> Persist
Persist --> Settings & AccessKey & DaemonState & Logs
API --> HTTP & Socket
Daemon --> API
Agents --> API
```
## High-level layout
- **Entry point:** `src/index.ts` parses subcommands and routes execution.
- **API client:** `src/api` handles HTTP + Socket.IO, encryption, and RPC.
- **Daemon:** `src/daemon` runs in the background, spawns sessions, and maintains machine state.
- **Persistence/config:** `src/persistence.ts` + `src/configuration.ts` manage local state in `~/.happy`.
- **Agents:** `src/claude`, `src/codex`, `src/gemini` provide provider-specific runners.
## CLI entry flow
```mermaid
flowchart TD
Start([happy ...]) --> Parse[Parse subcommand]
Parse --> Doctor{doctor?}
Parse --> Auth{auth?}
Parse --> Connect{connect?}
Parse --> Agent{codex/gemini?}
Parse --> Default{default}
Doctor --> RunDoctor[Run diagnostics]
Auth --> RunAuth[Auth flow]
Connect --> RunConnect[Connect machine]
Agent --> Setup[authAndSetupMachineIfNeeded]
Default --> Setup
Setup --> Context{Background?}
Context --> |Yes| StartDaemon[Start daemon]
Context --> |No| RunAgent[Run agent directly]
StartDaemon --> SpawnSession[Spawn session]
```
`src/index.ts` is the CLI router. It:
- Parses subcommands (`doctor`, `auth`, `connect`, `codex`, `gemini`, and default run flows).
- Ensures auth and machine setup when needed (`authAndSetupMachineIfNeeded`).
- Starts the daemon or runs an agent directly based on subcommand/context.
## Local state and configuration
```mermaid
graph LR
subgraph "~/.happy"
direction TB
settings["settings.json<br/><i>profile, onboarding</i>"]
access["access.key<br/><i>encryption keys</i>"]
daemon["daemon.state.json<br/><i>PID, port, version</i>"]
logs["logs/<br/><i>CLI/daemon logs</i>"]
end
subgraph "Environment Overrides"
direction TB
E1[HAPPY_HOME_DIR]
E2[HAPPY_SERVER_URL]
E3[HAPPY_WEBAPP_URL]
E4[HAPPY_VARIANT]
E5[HAPPY_EXPERIMENTAL]
E6[HAPPY_DISABLE_CAFFEINATE]
end
E1 -.-> settings & access & daemon & logs
```
Local state lives under `~/.happy` (or `HAPPY_HOME_DIR`):
- `settings.json`: onboarding and profile settings (validated/migrated).
- `access.key`: local key material for encryption/auth.
- `daemon.state.json`: daemon PID + control port + version.
- `logs/`: CLI/daemon logs.
Configuration lives in `src/configuration.ts`:
- `HAPPY_SERVER_URL` and `HAPPY_WEBAPP_URL` override defaults.
- `HAPPY_VARIANT`, `HAPPY_EXPERIMENTAL`, `HAPPY_DISABLE_CAFFEINATE` control behavior.
## API client architecture
```mermaid
graph TB
subgraph "API Clients"
Base[ApiClient]
Session[ApiSessionClient]
Machine[ApiMachineClient]
Encrypt[encryption.ts]
end
subgraph "Server"
HTTP[HTTP API]
Socket[Socket.IO]
end
Base --> |POST /v1/sessions| HTTP
Base --> |POST /v1/machines| HTTP
Session --> |session-scoped| Socket
Machine --> |machine-scoped| Socket
Encrypt --> Base & Session & Machine
```
### HTTP
`ApiClient` (`src/api/api.ts`) handles:
- Session creation (`POST /v1/sessions`) with encrypted metadata/state.
- Machine registration (`POST /v1/machines`) with encrypted metadata/daemon state.
- Other CRUD actions through `ApiSessionClient` and `ApiMachineClient`.
### WebSocket
```mermaid
graph LR
subgraph "ApiSessionClient"
S_In[Receive: update]
S_Out[Emit: message, update-metadata,<br/>update-state, session-alive, usage-report]
end
subgraph "ApiMachineClient"
M_In[Receive: machine updates]
M_Out[Emit: machine-alive,<br/>update metadata/state]
end
Server((Socket.IO)) --> S_In & M_In
S_Out & M_Out --> Server
```
`ApiSessionClient` (`src/api/apiSession.ts`) connects to Socket.IO as a **session-scoped** client:
- Receives `update` events and decrypts message content.
- Emits `message`, `update-metadata`, `update-state`, `session-alive`, and `usage-report`.
`ApiMachineClient` (`src/api/apiMachine.ts`) connects as a **machine-scoped** client:
- Sends `machine-alive` heartbeats.
- Updates machine metadata/daemon state with optimistic concurrency.
- Receives machine updates and merges them locally.
### Encryption
```mermaid
flowchart LR
subgraph "Client-side"
Plain[Plaintext Data]
Encrypt[encryption.ts]
B64[Base64 Encoded]
end
Plain --> |encrypt| Encrypt --> B64 --> |send| Server[(Server)]
Server --> |receive| B64 --> |decrypt| Encrypt --> Plain
style Plain fill:#e8f5e9
style B64 fill:#fff3e0
```
The CLI encrypts client content before it leaves the machine using `src/api/encryption.ts`.
- Session metadata, agent state, messages, machine state, artifacts, and KV values are encrypted client-side.
- On-wire encoding is base64; see `encryption.md`.
## Daemon architecture
```mermaid
graph TB
subgraph "Daemon Process"
Control[Control Server<br/>127.0.0.1:port]
Sessions[Session Map]
MachineClient[ApiMachineClient]
end
subgraph "Child Processes"
S1[Session 1]
S2[Session 2]
S3[Session N]
end
CLI[CLI] --> |IPC| Control
Control --> Sessions
Sessions --> S1 & S2 & S3
MachineClient --> |heartbeat| Server[(Server)]
MachineClient --> |state sync| Server
```
The daemon is a long-lived process responsible for running sessions in the background and maintaining machine presence.
### Lifecycle
```mermaid
flowchart TD
Start([startDaemon]) --> Validate[Validate version]
Validate --> Lock[Acquire lock file]
Lock --> Auth[Authenticate]
Auth --> Register[Register machine with server]
Register --> Control[Start control server]
Control --> Track[Track child sessions]
Track --> Sync[Sync daemon state to server]
Sync --> Running([Running])
Running --> |SIGTERM| Shutdown[Cleanup & exit]
```
1. `startDaemon()` validates the running version and acquires a lock file.
2. It authenticates and registers the machine with the server.
3. It starts a local **control server** for IPC.
4. It keeps a map of tracked child sessions and updates daemon state on the server.
### Control server (local IPC)
```mermaid
sequenceDiagram
participant CLI
participant State as daemon.state.json
participant Control as Control Server
participant Daemon
CLI->>State: Read port
State-->>CLI: port: 12345
CLI->>Control: GET /list
Control-->>CLI: [sessions...]
CLI->>Control: POST /spawn-session
Control->>Daemon: Spawn child process
Daemon-->>Control: Session started
Control-->>CLI: OK
CLI->>Control: POST /stop
Control->>Daemon: Shutdown
```
`startDaemonControlServer()` (`src/daemon/controlServer.ts`) runs an HTTP server on `127.0.0.1` and exposes:
- `/list` (list active sessions)
- `/stop-session`
- `/spawn-session`
- `/stop` (shutdown daemon)
- `/session-started` (session self-report)
The CLI talks to this server via `controlClient.ts`, using a port stored in `daemon.state.json`.
### Session spawning
```mermaid
flowchart LR
subgraph "Session Sources"
CLI[CLI<br/><i>foreground</i>]
Daemon[Daemon<br/><i>background</i>]
Remote[Mobile/Web<br/><i>via RPC</i>]
end
subgraph "Session Process"
Session[Agent Session]
Handlers[RPC Handlers]
end
CLI --> Session
Daemon --> Session
Remote --> |spawn-session| Daemon --> Session
Session --> Handlers
subgraph "RPC Surface"
Handlers --> Bash[bash]
Handlers --> Files[file read/write]
Handlers --> Search[ripgrep]
Handlers --> Diff[difftastic]
end
```
Sessions can be started by:
- The CLI directly (foreground).
- The daemon (background).
- Remote requests over RPC (from mobile/web via machine connection).
Daemon session spawning uses `registerCommonHandlers` to expose a controlled RPC surface (shell commands, file operations, search/diff helpers).
### Machine state
```mermaid
graph TB
subgraph "Machine Metadata (static)"
M1[host]
M2[platform]
M3[CLI version]
M4[paths]
end
subgraph "Daemon State (dynamic)"
D1[pid]
D2[httpPort]
D3[startedAt]
D4[shutdown info]
end
subgraph "Sync Targets"
Server[(Server)]
Local[daemon.state.json]
end
ApiMachine[ApiMachineClient]
M1 & M2 & M3 & M4 --> ApiMachine
D1 & D2 & D3 & D4 --> ApiMachine
D1 & D2 & D3 & D4 --> Local
ApiMachine --> Server
```
- **Machine metadata** is static info (host, platform, CLI version, paths).
- **Daemon state** is dynamic (pid, httpPort, startedAt, shutdown info).
The daemon updates these via `ApiMachineClient` and mirrors local state into `daemon.state.json` for control/diagnostics.
## RPC and tool bridge
```mermaid
sequenceDiagram
participant Mobile
participant Server
participant Daemon
participant Session
Mobile->>Server: RPC: spawn-session
Server->>Daemon: Forward via Socket.IO
Daemon->>Session: Spawn process
Session-->>Daemon: Running
Mobile->>Server: RPC: bash "ls -la"
Server->>Session: Forward via Socket.IO
Session->>Session: Execute command
Session-->>Server: Result
Server-->>Mobile: Result
Note over Mobile,Session: All RPC flows through Socket.IO<br/>No direct REST exposure
```
RPC is used to send commands over the Socket.IO connection:
- Sessions register RPC handlers (e.g., `bash`, file read/write, `ripgrep`, `difftastic`).
- The daemon registers a spawn-session handler so the server/mobile client can ask it to start a local session.
This mechanism allows the server and mobile clients to drive local actions without exposing a broad REST surface.
## Implementation references
- CLI entry: `packages/happy-cli/src/index.ts`
- Daemon: `packages/happy-cli/src/daemon`
- Control server/client: `packages/happy-cli/src/daemon/controlServer.ts`, `packages/happy-cli/src/daemon/controlClient.ts`
- API clients: `packages/happy-cli/src/api`
- Persistence: `packages/happy-cli/src/persistence.ts`
- Config: `packages/happy-cli/src/configuration.ts`