Files
kandev/docs/specs/native-kandev-cli/spec.md
T

226 lines
14 KiB
Markdown

---
status: draft
created: 2026-06-15
amended: 2026-07-31
owner: tbd
---
# Native Kandev CLI
## Why
Users start Kandev from the terminal through `kandev`, but the current launcher is a Node.js/TypeScript program even when Kandev is installed from Homebrew or run from a local production build. This adds a runtime dependency and extra moving parts to the most basic startup path. Users should keep the same `kandev` command while the launcher becomes a native executable.
## What
- `kandev` remains the public command for Homebrew, npm/npx, global npm installs, service units, and local development.
- Homebrew and release bundle installs provide a native `bin/kandev` executable that can launch Kandev without executing the TypeScript CLI bundle.
- The native `kandev` executable supports the public launcher commands users need after the web runtime merge: default run, `run`, `start`, `service`, `--help`, `--version`, `--port`, `--backend-port`, `--verbose`, `--debug`, and `--headless`.
- Native `dev` mode is supported for source checkouts through the Go launcher (`kandev dev` / `--dev`); installed release entrypoints do not expose it — it requires the repository checkout that `make dev` provides.
- The native launcher starts the backend as a supervised child process by re-executing the same `bin/kandev` binary in a hidden backend mode.
- The hidden backend mode is not a public command and is not shown in normal help output.
- Backend restarts restart only the backend child process; the launcher/supervisor remains alive unless the shutdown policy requires the whole app to exit.
- npm/npx continues to expose the `kandev` command through the `kandev` npm package. The npm package may use a minimal JavaScript shim, but that shim only resolves the platform runtime package and execs its native `bin/kandev`.
- `make start` launches through the native `apps/backend/bin/kandev start` path after building local artifacts.
- Existing service installs continue to be managed by `kandev service ...`; newly generated service units execute the public `kandev` launcher path.
- Reinstalling or updating a system service preserves the service account recorded by the existing
root-controlled Kandev unit or plist. The account changes only when the operator supplies
`--run-as <user>` explicitly.
- A first system install invoked from a root login does not silently select root. It requires an
explicit `--run-as <user>`; selecting root is allowed only as the explicit `--run-as root`
choice. A first install through `sudo` may continue to select the non-root invoking user.
- Before rewriting or restarting a system service, the installer verifies that the configured
Kandev home is owned by the selected service account. A mismatch fails with recovery guidance;
the installer does not recursively change ownership.
- Service units preserve install-time Node/npm/npx bin directories when they are discoverable, so agents can still invoke npm-managed CLIs under service managers that do not source shell profiles.
- Startup output continues to show the URL users should open, the MCP URL, database path when applicable, and log level when applicable.
- Production `run` and `start` do not execute a Node.js web runtime; the Go backend serves the embedded Vite SPA assets.
## API surface
### Public CLI
```text
kandev [--port <port>] [--verbose] [--debug] [--headless]
kandev run [--port <port>] [--verbose] [--debug] [--headless]
kandev start [--port <port>] [--verbose] [--debug] [--headless]
kandev service install [--system] [--run-as <user>] [--port <port>] [--home-dir <path>] [--no-boot-start]
kandev service uninstall|start|stop|restart|status|logs|config [--system]
kandev --version
kandev --help
```
Port flags and environment variables:
- `--port` and `--backend-port` select the public backend port.
- `KANDEV_PORT` and `KANDEV_BACKEND_PORT` provide backend port defaults.
- There is no production web port flag or environment variable. The Go backend serves the embedded SPA on the backend port.
- `--runtime-version` is not supported by the native launcher and is rejected as a usage error.
Logging/debug flags:
- `--verbose` shows backend info logs.
- `--debug` shows debug logs and enables current debug environment behavior.
- `--headless` skips browser opening and prints the ready URL.
System-service identity flags:
- `--run-as <user>` is valid only for `kandev service install --system`.
- On reinstall, omitting `--run-as` preserves the account in the existing Kandev-managed
root-controlled unit or plist, even when the update command is run from a different login.
- On a first install, omitting `--run-as` selects a non-root `SUDO_USER` when available. A root
login without that context must choose explicitly, including when the desired account is root.
- The service-owned `service/install.json` metadata records the effective account but is not
trusted by a privileged installer as authority for choosing a system account.
### Hidden backend mode
The native launcher has a private backend mode:
```text
kandev __backend [backend flags]
```
This mode starts the backend server and is invoked only by the launcher/supervisor. It is intentionally hidden from public help output. Existing backend diagnostic flags may remain available in this mode for tests and direct diagnostics.
### Runtime bundle
Release bundles expose this layout:
```text
kandev/
├── bin/kandev
├── bin/agentctl
├── bin/agentctl-linux-amd64
├── bin/agentctl-darwin-arm64
└── bin/agentctl-darwin-amd64
```
`bin/kandev` is both the public launcher and the hidden backend-mode executable.
### Supervisor manifest
When backend restart supervision is enabled, the launch manifest records the same executable plus hidden backend argv:
```json
{
"version": 1,
"backend_executable": "/absolute/path/to/bin/kandev",
"argv": ["__backend"],
"cwd": "/absolute/path/to/bin",
"env": {
"KANDEV_SERVER_PORT": "38429",
"KANDEV_AGENT_STANDALONE_PORT": "39429",
"KANDEV_RESTART_ADAPTER": "supervisor"
},
"home_dir": "/absolute/path/to/home",
"port": 38429,
"mode": "run",
"created_at": "timestamp"
}
```
## State machine
Launcher process lifecycle:
- `idle`: command parsed but no children started.
- `backend-starting`: launcher has spawned `kandev __backend` and is waiting for backend health.
- `ready`: backend is healthy and serving API, WebSocket, and SPA routes; browser may be opened unless headless.
- `restarting-backend`: supervisor received a restart request and is replacing only the backend child.
- `shutting-down`: launcher is terminating child processes after signal, service stop, or child exit policy.
- `failed`: startup failed and the launcher exits non-zero after surfacing the actionable error.
Transitions:
- `idle` -> `backend-starting`: user runs `kandev`, `kandev run`, `kandev start`, or service unit starts.
- `backend-starting` -> `failed`: backend exits or health timeout expires.
- `backend-starting` -> `ready`: backend health endpoint reports ready.
- `ready` -> `restarting-backend`: backend restart adapter requests restart.
- `restarting-backend` -> `ready`: replacement backend becomes healthy.
- any active state -> `shutting-down`: signal, service stop, or non-restartable child exit.
## Failure modes
- If a required backend artifact is missing in `start` mode, `kandev start` exits non-zero with a message that tells the user to run `make build`.
- If the runtime bundle is missing required files in `run` mode, `kandev run` exits non-zero and names the missing artifact.
- If the backend does not become healthy before timeout, the launcher exits non-zero and includes captured backend output when running in quiet mode.
- If the backend child exits unexpectedly after startup, the launcher either restarts it through the supervisor path when the exit is restart-controlled or shuts down the app with the backend exit code.
- If npm/npx optional runtime resolution fails, the JavaScript shim exits non-zero with an actionable message explaining that the platform runtime package is missing.
- If a privileged reinstall finds an existing Kandev-managed system unit or plist, it fails before
rewriting or restarting the service when that root-controlled definition is malformed or its
account cannot be resolved.
- If a first system install is run from a root login without `--run-as`, it exits with usage
guidance instead of silently creating a root service.
- If the configured system Kandev home owner does not match the selected service account, install
exits before changing the service definition and explains how to preserve the existing account
or deliberately reconcile ownership. It does not add a global Git trust exception or chown the
data tree automatically.
## Persistence guarantees
- Service installation metadata remains durable across restarts and upgrades.
- The effective system-service account remains stable across package upgrades and repeated
`service install --system` calls unless an operator explicitly changes it with `--run-as`.
- For an existing system service, the root-controlled unit or plist is authoritative for service
identity. Service-owned metadata may corroborate that identity but cannot authorize a
privileged identity change.
- Supervisor manifest/control files remain under the configured Kandev home directory and are overwritten on each launch with the current executable path and backend argv.
- The launcher does not persist child process state beyond the existing supervisor manifest. After a launcher restart, it starts a fresh backend child.
- User data, database files, task worktrees, and backend-managed persistence remain owned by existing backend persistence behavior; the launcher migration does not change their retention.
## Scenarios
- **GIVEN** Kandev is installed from Homebrew, **WHEN** the user runs `kandev --help`, **THEN** the help output describes the public launcher commands and does not show `__backend`.
- **GIVEN** Kandev is installed from Homebrew, **WHEN** the user runs `kandev --version`, **THEN** the command prints the installed Kandev version without executing the Node CLI bundle.
- **GIVEN** a valid release bundle, **WHEN** the user runs `kandev --headless`, **THEN** the native launcher starts `kandev __backend`, waits for the backend to serve API and SPA routes, and prints the backend URL.
- **GIVEN** a local checkout with built backend and web artifacts, **WHEN** the user runs `make start`, **THEN** the Makefile launches through `apps/backend/bin/kandev start` and does not invoke the TypeScript CLI launcher (`kandev dev` likewise goes through the same Go binary).
- **GIVEN** the backend has requested a restart through the restart adapter, **WHEN** the launcher receives the restart request, **THEN** only the `kandev __backend` child process is replaced.
- **GIVEN** the user presses Ctrl-C while Kandev is running, **WHEN** the launcher handles the signal, **THEN** it terminates the backend child process before exiting.
- **GIVEN** a new service install on Linux or macOS, **WHEN** the user runs `kandev service install`, **THEN** the generated service unit executes the public `kandev` launcher path.
- **GIVEN** a system service already runs as `brewuser`, **WHEN** root later runs
`kandev service install --system` during an upgrade without `--run-as`, **THEN** the generated
definition still runs as `brewuser`.
- **GIVEN** an existing system service, **WHEN** an operator intentionally supplies
`--run-as another-user`, **THEN** Kandev validates that account and the configured home owner
before changing the service definition.
- **GIVEN** no existing system service and no non-root `SUDO_USER`, **WHEN** root runs
`kandev service install --system` without `--run-as`, **THEN** installation fails and asks for an
explicit account rather than defaulting to root.
- **GIVEN** a selected service account whose UID differs from the configured system home owner,
**WHEN** installation runs, **THEN** it fails before rewriting or restarting the service and does
not recursively chown the home.
- **GIVEN** service-owned install metadata names a more privileged account than the existing
root-controlled service definition, **WHEN** a privileged reinstall runs, **THEN** the metadata
cannot change the selected account.
- **GIVEN** a native user-service install on Linux or macOS, **WHEN** the service starts, **THEN** its environment and durable install metadata identify it as a kandev-managed service and the guarded System-page updater can launch the native self-update helper.
- **GIVEN** Node is managed by nvm/fnm/asdf/volta/mise and `node`, `npm`, or `npx` resolve from that manager at install time, **WHEN** the user runs `kandev service install`, **THEN** the generated service environment includes the detected Node tool bin directory in `PATH`.
- **GIVEN** a user runs `npx kandev@latest --help`, **WHEN** npm has installed the platform runtime package, **THEN** the npm shim execs the runtime package's native `bin/kandev` and the user sees the same public help output.
## Out of scope
- Making `npx` itself Node-free.
- Introducing `kandevctl` as a user-facing command.
- Requiring a separate shipped backend binary such as `kandev-backend`.
- Changing task, workflow, agent, or integration behavior after the backend has started.
- Automatically changing ownership, managing ACLs, or adding broad Git `safe.directory` entries
when a system-service identity mismatch is detected.
- Managing Windows services or inferring service identity from an untrusted service-owned metadata
file.
## Repair plan
See the
[system-service identity guardrails plan](../../plans/system-service-identity-guardrails/plan.md).
## Decision
System-service account continuity and ownership validation follow
[ADR-2026-07-31-system-service-user-continuity](../../decisions/2026-07-31-system-service-user-continuity.md).
## Open questions
- Should hidden backend mode be selected by the `__backend` argv token or by `KANDEV_INTERNAL_ROLE=backend`? Recommendation: `__backend`.
- Should `--runtime-version <tag>` be ported in a later native launcher implementation or remain deprecated?
- Should one release ship both the old Node CLI bundle and the native `bin/kandev` for compatibility while Homebrew/npm packaging switches over?