# Workspace daemon installer

Install the workspace server as a supervised always-on daemon via the host's native service manager. The daemon auto-starts at login and auto-restarts on crash.

## Status

- macOS (launchd LaunchAgent): supported
- Linux (systemd --user): supported
- Windows: not implemented; prints an explicit error (exit 2). Use `node core/tools/workspace/ws.mjs start` manually.

## Why a separate tool

The scheduler at `core/tools/scheduler/` installs time-triggered jobs (cron, launchd StartCalendarInterval, systemd timers). A daemon is continuous-supervision: `KeepAlive=true` / `Restart=always` / `RunAtLoad=true` / `WantedBy=default.target`. Different primitives, different shape. Rather than fold both into one abstraction, this installer is a sibling tool with a narrow surface.

## Why the daemon bypasses ws.mjs

`ws.mjs start` double-forks and detaches (it is a user-facing CLI daemoniser). If launchd or systemd invoked `ws.mjs start`, the service manager would see the initial parent exit immediately after fork and interpret it as a crashed service — triggering an infinite restart loop.

The installer writes plist/unit files that invoke `server.mjs` **directly**, in the foreground, with the flags `ws.mjs` would have passed: `--no-timeout --port N --no-open`. The service manager supervises the real server process.

## Commands

```
node core/tools/workspace/install-daemon.mjs install   [--port N] [--dry-run] [--migrate]
node core/tools/workspace/install-daemon.mjs uninstall [--dry-run]
node core/tools/workspace/install-daemon.mjs status
node core/tools/workspace/install-daemon.mjs --help
```

## Unit naming

Unit labels are derived from your `paName` in `operator/config/preferences.json` (device-local overlay applied), following the scheduler's `com.{agent}.{id}` convention with `id = workspace`. Shape by platform:

| Platform | Unit | Example (`paName: alex`) |
|---|---|---|
| macOS (launchd) | `com.<paName>.workspace` | `com.alex.workspace` |
| Linux (systemd) | `<paName>-workspace.service` | `alex-workspace.service` |

Missing / unresolvable `paName` falls back to `dave`, matching the pre-0.1.44 label (`com.dave.workspace` / `dave-workspace.service`) so an existing HQ install never needs a rewrite.

### install

Writes the plist (macOS) or unit file (Linux), then loads / enables it so the daemon starts immediately and at every subsequent login. Idempotent: repeating `install` unloads the existing service first, rewrites the file, then reloads — safe to run after changing the port in `operator/config/preferences.json` or after upgrading Node.

Port precedence:
1. `--port N` CLI flag
2. `operator/config/preferences.json` field `workspacePort`
3. Default `4200`

### uninstall

Unloads / disables the service and removes the plist / unit file. Safe to run when nothing is installed.

### status

Reports whether the plist / unit file exists on disk and whether the service is currently running.

### --dry-run

Prints what `install` / `uninstall` would do — including the full plist / unit contents for `install` — without touching the filesystem or the service manager. Use before running `install` on a new host to review the paths.

### --migrate

For hosts that installed the daemon under 0.1.43 or earlier: unloads the legacy `com.dave.workspace` / `dave-workspace.service` unit and removes its file before writing the paName-derived one. Required whenever the derived label differs from the legacy label and the legacy file is still on disk; without it, `install` refuses (exit 2) rather than let two daemons race for the same port. Idempotent — after a successful migration the flag is a no-op.

## File locations

| Platform | Service file | Stdout log | Stderr log |
|---|---|---|---|
| macOS | `~/Library/LaunchAgents/com.<paName>.workspace.plist` | `~/Library/Logs/<paName>-workspace.log` | `~/Library/Logs/<paName>-workspace.err.log` |
| Linux | `~/.config/systemd/user/<paName>-workspace.service` | `~/.local/state/<paName>/workspace.log` | `~/.local/state/<paName>/workspace.err.log` |

For a solo install with `paName: alex` this materialises as `~/Library/LaunchAgents/com.alex.workspace.plist` and `~/Library/Logs/alex-workspace.log` on macOS, and `~/.config/systemd/user/alex-workspace.service` with logs under `~/.local/state/alex/` on Linux.

## Reinstall workflow

Port changes or Node version changes require re-install:

```bash
node core/tools/workspace/install-daemon.mjs install   # picks up new state
```

No explicit uninstall-then-install needed — the installer unloads any existing instance before writing the new file.

To fully remove:

```bash
node core/tools/workspace/install-daemon.mjs uninstall
```

## Exit codes

| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Usage error (bad flag, unknown command) |
| 2 | Unsupported platform, or logical failure (e.g. status on an absent service) |
| 3 | Transport failure (the `launchctl` / `systemctl` call itself errored) |

## Coexistence with `ws.mjs start`

The supervised daemon and the `ws.mjs start` CLI both bind the same port. Only one should run at a time. If `ws.mjs start` is already running when you install the daemon, the install will succeed but the daemon process will exit immediately with a port conflict and launchd/systemd will restart it in a loop until the conflicting process is stopped. Graceful handover:

```bash
node core/tools/workspace/ws.mjs stop               # stop the manual instance
node core/tools/workspace/install-daemon.mjs install # daemon takes over
```

## Tests

```bash
node --test core/tools/workspace/install-daemon.test.mjs
```

Tests cover the pure builders (plist / unit file generation), port resolution, platform routing, and dry-run output. They never invoke `launchctl` / `systemctl` for real — real install/uninstall mutates host state and is verified by the operator.

## Tooling standard

Node-only, zero deps, `node:test`, camelCase JSON, README as contract. See [`core/knowledge/standards/tooling.md`](../../knowledge/standards/tooling.md). Windows support is an explicit-error stub rather than a silent no-op, per the cross-platform parity rule.
