Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions docs/developer/plugin-daemons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Plugin-Side Daemons

This guide covers how to spawn background daemons from OpenCLI plugins. This pattern is
used by the built-in `browser-bridge` daemon and can be adapted for plugin-specific
background services.

## When to Use a Plugin Daemon

Consider a plugin daemon when your plugin needs:

- A long-running process that persists across CLI invocations
- A server (HTTP, WebSocket, IPC) that other processes connect to
- Background processing that shouldn't block CLI commands

**Example use cases:**
- Custom browser automation services
- Local HTTP servers exposing plugin-specific APIs
- IPC bridges to external tools or services

## Verified Spawn Pattern

The following pattern has been tested and proven stable across macOS and Linux:

```typescript
import { spawn } from 'node:child_process';

this._daemonProc = spawn(spawnArgs[0], spawnArgs.slice(1), {
detached: true,
stdio: 'ignore',
env: { ...process.env },
});
this._daemonProc.unref();
```

**Source:** `src/browser/bridge.ts:132-137`

### Key Properties Explained

| Property | Value | Purpose |
|----------|-------|---------|
| `detached: true` | Boolean | Creates a new process group, allowing the daemon to outlive the parent |
| `stdio: 'ignore'` | String | Prevents stdin/stdout/stderr pipes from keeping the parent alive |
| `.unref()` | Method | Removes the parent's reference to the child, so parent exit doesn't kill daemon |
| `env: { ...process.env }` | Object | Daemon inherits the caller's environment (including `OPENCLI_*` vars and `PATH`) |

### Why `detached: true` + `stdio: 'ignore'`

This combination is sufficient for most use cases:

1. **`detached: true`** detaches the child process from the parent's process group
2. **`stdio: 'ignore'`** prevents the parent's file descriptors from being inherited
3. **`.unref()`** removes the parent's reference to the child

Together, these ensure the daemon survives when the CLI exits. **No `setsid` or explicit
process group management is needed** on macOS or Linux.

### Environment Inheritance

```typescript
env: { ...process.env }
```

This is intentional. The daemon inherits:
- `OPENCLI_*` environment variables (plugin-specific configuration)
- `PATH` (required for finding executables)
- Any other variables set by the user or system

If you need a clean environment, explicitly set only required variables:

```typescript
env: {
PATH: process.env.PATH,
MY_PLUGIN_VAR: 'value',
}
```

### Lazy Spawning

Spawn the daemon from the CLI entry point, not at module import time:

```typescript
// ✅ Good: spawn when adapter actually runs
class MyAdapter {
private daemon: ChildProcess | null = null;

async connect() {
if (!this.daemon) {
this.daemon = spawn(/* ... */);
}
}
}

// ❌ Avoid: spawn at import time
const daemon = spawn(/* ... */); // Runs even if adapter never used
```

Lazy spawning ensures:
- Fast module loading
- No daemon startup for commands that don't need it
- Proper integration with CLI timeout/idle logic

## Daemon Communication

### HTTP Server Pattern

Most plugin daemons expose an HTTP API:

```typescript
import http from 'node:http';

const server = http.createServer((req, res) => {
// Handle requests
});

server.listen(port, () => {
console.log(`Daemon listening on port ${port}`);
});
```

### Port Selection

Use a configurable port with a default:

```typescript
const DEFAULT_PORT = 19826; // Avoid default browser-bridge port (19825)

const port = parseInt(process.env.MY_PLUGIN_PORT || String(DEFAULT_PORT), 10);
```

## Lifecycle Integration

### Status Reporting

When `opencli daemon status` is implemented, your daemon should respond to:

```
GET /status
```

Return JSON with daemon information:

```json
{
"name": "my-plugin",
"pid": 12345,
"uptime": 7200000,
"port": 19826
}
```

### Graceful Shutdown

Handle `SIGTERM` for clean shutdowns:

```typescript
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down...');
server.close(() => {
process.exit(0);
});
});
```

## Multi-Daemon Considerations

When multiple daemons may run simultaneously:

1. **Use unique ports** for each daemon
2. **Register daemon info** for discovery (future enhancement planned)
3. **Handle port conflicts** gracefully with clear error messages

### Port Allocation Strategy

| Daemon | Default Port | Environment Variable |
|--------|--------------|---------------------|
| browser-bridge | 19825 | `OPENCLI_DAEMON_PORT` |
| plugin (choose) | 19826+ | `MY_PLUGIN_PORT` |

## Testing Your Plugin Daemon

1. **Start manually:** Run your plugin and verify the daemon spawns
2. **Check status:** Verify daemon is listening on expected port
3. **Test isolation:** Run multiple CLI commands; daemon should persist
4. **Clean shutdown:** Verify daemon exits when parent exits or on explicit stop

## Related Documentation

- [Daemon Lifecycle Redesign Spec](../superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md)
- [Architecture Overview](./architecture.md)
- [TypeScript Adapter](./ts-adapter.md)
92 changes: 91 additions & 1 deletion docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,99 @@ and shows:
- Integration test: daemon exits after configured timeout when fully idle
- Integration test: `opencli daemon status/stop/restart` work correctly

## Multi-Daemon Namespace Reservation

> **Note added 2026-04-16:** This section reserves namespace for future plugin-side daemon
> support. The implementation details below are NOT yet implemented.

### Motivation

Future OpenCLI adapters may need to spawn their own background daemons (e.g., custom IPC
bridges or services). The current design assumes a single `browser-bridge` daemon. We need
to reserve namespace so multiple daemons can coexist without breaking existing flags.

### Namespace Design

#### Daemon Naming

Each daemon has a unique name used for targeting:

| Daemon | Name | Default Port |
|--------|------|--------------|
| browser-bridge | `browser-bridge` | 19825 |
| (future plugins) | `<plugin-name>` | configurable |

#### CLI Surface Changes

All daemon subcommands accept an optional `[name]` argument:

```
opencli daemon status [name]
opencli daemon stop [name]
opencli daemon restart [name]
```

**Behavior when name is omitted:**
- `status`: Returns status for all known daemons, or `browser-bridge` if only one exists
- `stop` / `restart`: Requires explicit name when multiple daemons are running (error if ambiguous)

#### Status Response Format

**Single daemon (current behavior):**
```
Daemon: running (PID 12345)
Uptime: 2h 15m
Extension: connected
Last CLI request: 8 min ago
Memory: 12.3 MB
Port: 19825
```

**Multi-daemon status:**
```
Daemons:
browser-bridge: running (PID 12345) - Extension: connected
my-plugin: running (PID 67890) - Port: 19826

Run `opencli daemon status <name>` for detailed info on a specific daemon.
```

#### Discovery Mechanism

Plugin-side daemons register with a well-known file:

```
~/.opencli/daemons/<name>.json
```

Each file contains:
```json
{
"name": "my-plugin",
"pid": 12345,
"port": 19826,
"startedAt": "2026-04-16T10:00:00Z"
}
```

The `opencli daemon` commands enumerate registered daemons from this directory.

#### Implementation Notes

1. **Backward compatibility:** Current behavior is `name = browser-bridge` implicit
2. **Registration:** Daemon writes its info file on startup, removes on graceful exit
3. **Cleanup:** Orphaned pidfiles (daemon crashed) are detected via `kill(pid, 0)` check
4. **Security:** Daemon files are user-writable only; path traversal is prevented

### Future Considerations

- Daemon health check endpoint: `GET /health` returns `{ "ok": true }`
- Daemon registry service for multi-machine setups
- OS-level integration (launchd plists, systemd units)

## Out of Scope

- OS-level daemon management (launchd/systemd) — can be added later if needed
- Daemon auto-update mechanism
- Multi-daemon coordination
- Persistent daemon state across restarts
- Plugin daemon registration API (reserved namespace only, implementation TBD)