CLI Design
Overview
nexusctl is the command-line interface for managing Firecracker microVMs through the Nexus daemon. It is a thin HTTP client – all state lives in nexusd, and nexusctl is stateless aside from configuration.
The recommended alias is nxc:
nexusctl vm list
nxc vm list # identical
Command Grammar
Every command follows noun-verb ordering:
nexusctl <resource> <action> [name] [flags]
Resources map directly to Nexus API entities. Actions are consistent across all resources.
Resources
| Resource | Shortname | Description |
|---|---|---|
vm | vm | Firecracker microVM instances |
workspace | ws | btrfs subvolume snapshots attached to VMs |
agent | a | Agent runtime sessions (portal + work VM pairs) |
image | img | Master images (read-only base subvolumes) |
network | net | Bridge and tap device configuration |
route | rt | vsock routes between VMs |
service | svc | vsock services registered by guest agents |
Shortnames work anywhere the full resource name works:
nexusctl ws list
nexusctl workspace list # identical
Standard Actions
Every resource supports a consistent set of verbs. Not every verb applies to every resource – attempting an unsupported action returns a clear error.
| Action | Description | Applies to |
|---|---|---|
list | List resources in a table | all |
create | Create a new resource | vm, workspace, agent, route |
inspect | Show detailed resource state | all |
delete | Remove a resource | all |
start | Start a stopped resource | vm, agent |
stop | Stop a running resource | vm, agent |
logs | Stream or tail logs | vm, agent |
Special Commands
Some commands live outside the resource-action pattern:
| Command | Description |
|---|---|
nexusctl attach <vm> | Open a terminal session to a VM (WebSocket/ttyd) |
nexusctl init | Interactive project setup wizard |
nexusctl apply | Apply declarative configuration from nexus.yaml |
nexusctl config <subcommand> | Manage CLI and daemon configuration |
nexusctl completion <shell> | Generate shell completions |
nexusctl version | Print version, daemon version, and API version |
nexusctl status | Quick system health check (daemon, VMs, network) |
Command Map
graph TD
nexusctl["nexusctl"]
nexusctl --> vm["vm"]
nexusctl --> ws["workspace (ws)"]
nexusctl --> agent["agent (a)"]
nexusctl --> image["image (img)"]
nexusctl --> network["network (net)"]
nexusctl --> route["route (rt)"]
nexusctl --> service["service (svc)"]
nexusctl --> config["config"]
nexusctl --> special["attach / init / apply / status"]
vm --> vm_actions["list | create | inspect | delete | start | stop | logs"]
ws --> ws_actions["list | create | inspect | delete | snapshot | restore"]
agent --> agent_actions["list | create | inspect | delete | start | stop | logs"]
image --> image_actions["list | inspect | delete | import"]
network --> network_actions["list | inspect"]
route --> route_actions["list | create | inspect | delete"]
service --> service_actions["list | inspect"]
config --> config_actions["list | edit | set | get"]
Output Formatting
Default: Columnar Tables
All list commands produce clean, aligned tables with no decoration:
$ nexusctl vm list
NAME ROLE STATE VCPU MEM IP AGE
agent-code-1 work running 2 512M 172.16.0.11 3h
portal-oc-1 portal running 1 256M 172.16.0.10 3h
git-server service stopped 1 128M 172.16.0.12 2d
The --output / -o flag controls format:
| Format | Flag | Description |
|---|---|---|
| Table | -o table | Default. Human-readable columns. |
| Wide | -o wide | Table with additional columns (IDs, paths, timestamps). |
| JSON | -o json | Full JSON array. Machine-readable. |
| YAML | -o yaml | Full YAML. Matches config file format. |
| Name | -o name | One resource name per line. For piping. |
$ nexusctl vm list -o name
agent-code-1
portal-oc-1
git-server
$ nexusctl vm list -o json
[
{
"name": "agent-code-1",
"role": "work",
"state": "running",
"vcpu_count": 2,
"mem_size_mib": 512,
"ip_address": "172.16.0.11",
"created_at": "2026-02-18T10:30:00Z"
}
]
JSON Field Selection
The --json flag selects specific fields and implies JSON output. Combine with --jq for inline filtering.
$ nexusctl vm list --json name,state,ip_address
[
{"name": "agent-code-1", "state": "running", "ip_address": "172.16.0.11"},
{"name": "portal-oc-1", "state": "running", "ip_address": "172.16.0.10"}
]
$ nexusctl vm list --json name,state --jq '.[] | select(.state == "running") | .name'
"agent-code-1"
"portal-oc-1"
Layered Detail
Information density increases through progressive commands:
graph LR
A["list"] -->|more columns| B["-o wide"]
B -->|single resource| C["inspect"]
C -->|machine parse| D["inspect -o json"]
$ nexusctl vm list # summary table
$ nexusctl vm list -o wide # adds ID, CID, PID, socket path
$ nexusctl vm inspect agent-code-1 # full detail, formatted
$ nexusctl vm inspect agent-code-1 -o json # full detail, structured
Color
Semantic colors convey state at a glance:
| Color | Meaning |
|---|---|
| Green | Running, success, healthy |
| Yellow | Pending, warning, created |
| Red | Error, stopped, crashed, failed |
| Dim | Metadata, secondary info, timestamps |
Color behavior:
- TTY detected: Colors enabled by default.
- Pipe / redirect: Colors disabled automatically.
NO_COLORenv set: Colors disabled (per no-color.org).FORCE_COLORenv set: Colors forced on regardless of TTY.
Progress Indicators
- Discrete steps:
Creating workspace... (2 of 4)with step descriptions. - Indeterminate waits: Spinner with elapsed time:
Waiting for VM to boot... (3.2s). - Non-TTY: Progress messages printed as plain lines, no ANSI escape sequences.
Interactive Behavior
TTY Gating
Every interactive prompt has a non-interactive equivalent. If stdin is not a TTY and a required value is missing, the command fails with an explicit error:
$ echo | nexusctl vm delete agent-code-1
Error: refusing to delete VM without confirmation
Run with --yes to skip confirmation: nexusctl vm delete agent-code-1 --yes
Override Flags
| Flag | Effect |
|---|---|
--yes / -y | Skip all confirmation prompts |
--interactive | Force interactive mode even without TTY |
--no-interactive | Force non-interactive mode even with TTY |
Destructive Operations
Commands that destroy data or stop running processes require confirmation:
$ nexusctl vm delete agent-code-1
VM "agent-code-1" is currently running with 1 attached workspace.
Delete this VM? This will stop it and detach all workspaces. [y/N] y
Deleted VM "agent-code-1"
Bypass with -y:
$ nexusctl vm delete agent-code-1 -y
Deleted VM "agent-code-1"
Error Messages
Every error has three parts: what failed, why it failed, and how to fix it.
$ nexusctl vm start agent-code-1
Error: cannot start VM "agent-code-1"
VM is already running (state: running, PID: 4821)
To restart, stop it first: nexusctl vm stop agent-code-1
$ nexusctl vm create --image nonexistent/image
Error: cannot create VM
Image "nonexistent/image" not found
Available images: nexusctl image list
$ nexusctl vm lis
Error: unknown action "lis" for resource "vm"
Did you mean: list
Available actions: list, create, inspect, delete, start, stop, logs
Error Output
- All errors go to stderr.
- All normal output goes to stdout.
- Non-zero exit codes on failure.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Usage error (bad flags, missing args) |
| 3 | Daemon unreachable |
| 4 | Resource not found |
| 5 | Conflict (e.g., VM already running) |
Onboarding
First Run Detection
When nexusctl cannot reach nexusd, it detects whether the daemon is installed and guides the user:
$ nexusctl vm list
Error: cannot connect to Nexus daemon at 127.0.0.1:9600
The daemon does not appear to be running.
Start it: systemctl --user start nexus.service
Enable it: systemctl --user enable nexus.service
Check status: systemctl --user status nexus.service
If the daemon is not installed at all:
$ nexusctl vm list
Error: cannot connect to Nexus daemon at 127.0.0.1:9600
The nexus package does not appear to be installed.
Install it: sudo pacman -S nexus
Init Wizard
nexusctl init creates a project-level nexus.yaml through guided prompts:
$ nexusctl init
Nexus project setup
? Base image for work VMs: workfort/code-agent (default)
? vCPUs per work VM: 2
? Memory per work VM (MiB): 512
? Agent runtime: openclaw
Created nexus.yaml
Next steps:
nexusctl apply Apply this configuration
nexusctl agent create dev Create an agent from this config
Next-Command Hints
After successful operations, suggest the logical next step:
$ nexusctl vm create --image workfort/code-agent --name agent-code-1
Created VM "agent-code-1" (state: created)
Start it: nexusctl vm start agent-code-1
$ nexusctl vm start agent-code-1
Started VM "agent-code-1" (state: running, IP: 172.16.0.11)
Attach terminal: nexusctl attach agent-code-1
View logs: nexusctl vm logs agent-code-1
Hints are suppressed in non-TTY environments and when using -o json or -o name.
Configuration
Format
All configuration is YAML. No exceptions.
Precedence
Configuration resolves in this order (highest wins):
graph TD
A["CLI flags"] --> B["Environment variables"]
B --> C["Project config ./nexus.yaml"]
C --> D["User config ~/.config/nexusctl/config.yaml"]
D --> E["System config /etc/nexus/nexus.yaml"]
E --> F["Built-in defaults"]
style A fill:#2d6,stroke:#333,color:#000
style F fill:#999,stroke:#333,color:#000
- CLI flags –
--api-url,--output, etc. - Environment variables –
NEXUS_API_URL,NEXUS_OUTPUT, etc. - Project config –
./nexus.yamlin the current directory (or parent search). - User config –
~/.config/nexusctl/config.yaml(respectsXDG_CONFIG_HOME). - System config –
/etc/nexus/nexus.yaml. - Built-in defaults.
Environment Variables
All env vars use the NEXUS_ prefix. Flag names map to env vars by uppercasing and replacing hyphens with underscores:
| Flag | Env Var |
|---|---|
--api-url | NEXUS_API_URL |
--output | NEXUS_OUTPUT |
--no-color | NEXUS_NO_COLOR |
User Config
# ~/.config/nexusctl/config.yaml
api_url: "http://127.0.0.1:9600"
output: table
defaults:
vm:
vcpu_count: 2
mem_size_mib: 512
workspace:
base_image: workfort/code-agent
Config Commands
$ nexusctl config list
KEY SOURCE VALUE
api_url /etc/nexus/nexus.yaml http://127.0.0.1:9600
output default table
defaults.vm.vcpu ~/.config/nexusctl/config.yaml 2
defaults.vm.mem ~/.config/nexusctl/config.yaml 512
$ nexusctl config get api_url
http://127.0.0.1:9600
$ nexusctl config set defaults.vm.vcpu_count 4
Set defaults.vm.vcpu_count = 4 in ~/.config/nexusctl/config.yaml
$ nexusctl config edit
# opens ~/.config/nexusctl/config.yaml in $EDITOR
config list shows every effective value and which source it came from. This eliminates the “where is this setting coming from?” problem.
Declarative Configuration
Project File
A nexus.yaml in the project root declares the desired infrastructure:
# nexus.yaml
version: 1
agents:
dev:
image: workfort/code-agent
vcpu: 2
mem: 512
runtime: openclaw
workspaces:
- name: code
base: workfort/code-agent
- name: data
base: empty
size: 2G
network:
bridge: nexbr0
cidr: 172.16.0.0/24
Apply with Diff and Dry Run
$ nexusctl apply --dry-run
Comparing nexus.yaml against current state...
+ create agent "dev"
+ create VM "dev-portal" (portal, 1 vCPU, 256M)
+ create VM "dev-work" (work, 2 vCPU, 512M)
+ create workspace "dev-code" from workfort/code-agent
+ create workspace "dev-data" (empty, 2G)
+ create route dev-portal:9000 → dev-work:9000
No changes applied (dry run)
$ nexusctl apply
Comparing nexus.yaml against current state...
+ create agent "dev"
+ create VM "dev-portal" (portal, 1 vCPU, 256M)
+ create VM "dev-work" (work, 2 vCPU, 512M)
+ create workspace "dev-code" from workfort/code-agent
+ create workspace "dev-data" (empty, 2G)
+ create route dev-portal:9000 → dev-work:9000
Apply these changes? [y/N] y
Creating agent "dev"... done (2.1s)
Agent "dev" is running.
Attach: nexusctl attach dev
Shell Completion
Generation
$ nexusctl completion bash >> ~/.bashrc
$ nexusctl completion zsh > ~/.zfunc/_nexusctl
$ nexusctl completion fish > ~/.config/fish/completions/nexusctl.fish
Dynamic Completions
Completions query the daemon for live state. Tab-completing a VM name fetches the current VM list:
$ nexusctl vm inspect <TAB>
agent-code-1 (work, running)
portal-oc-1 (portal, running)
git-server (service, stopped)
Zsh completions include descriptions. Resource shortnames complete identically to full names.
Help System
Progressive Disclosure
Default help shows only core commands. Advanced usage is available but not shown upfront.
$ nexusctl --help
nexusctl - manage Firecracker microVMs via Nexus
Usage: nexusctl <resource> <action> [name] [flags]
Core Commands:
vm Manage virtual machines
workspace Manage btrfs workspaces
agent Manage agent sessions
attach Open terminal to a VM
Getting Started:
init Set up a new project
apply Apply declarative configuration
status System health check
Configuration:
config View and edit settings
completion Generate shell completions
Run 'nexusctl <resource> --help' for resource-specific actions.
Run 'nexusctl help --all' for the full command list.
Full help includes every resource, action, and global flag:
$ nexusctl help --all
Resource Help
Each resource shows its actions, 2-3 usage examples, and flag groups:
$ nexusctl vm --help
Manage Firecracker virtual machines
Usage: nexusctl vm <action> [name] [flags]
Actions:
list List all VMs
create Create a new VM
inspect Show VM details
delete Remove a VM
start Start a stopped VM
stop Stop a running VM
logs View VM console logs
Examples:
nexusctl vm list
nexusctl vm create --image workfort/code-agent --name my-vm
nexusctl vm create my-vm # uses default image
nexusctl vm inspect my-vm -o json
Basic Flags:
-o, --output <format> Output format: table, wide, json, yaml, name
--json <fields> Select JSON fields (implies -o json)
--jq <expr> Filter JSON output with jq expression
Resource Flags:
--image <name> Base image for the VM
--role <role> VM role: work, portal, service (default: work)
--vcpu <n> vCPU count (default: 1)
--mem <mib> Memory in MiB (default: 128)
Advanced Flags:
--cid <n> Override vsock context ID (auto-assigned by default)
--no-start Create without starting
--dry-run Show what would happen without executing
Flag Grouping
Flags in --help are organized into groups: basic, resource-specific, and advanced. Basic flags appear on every command. Advanced flags are things most users never touch.
Smart Defaults
The CLI minimizes required input. Sensible defaults make the common case trivial:
$ nexusctl vm create my-vm
This single command:
- Uses the default image from config (or
workfort/code-agent) - Assigns role
work - Allocates 1 vCPU, 128 MiB memory (or user config defaults)
- Auto-assigns a vsock CID
- Auto-assigns an IP address
- Creates and starts the VM
Explicit flags override any default:
$ nexusctl vm create my-vm --vcpu 4 --mem 1024 --role portal --no-start
Dry Run
All commands that create, modify, or destroy state support --dry-run:
$ nexusctl vm delete agent-code-1 --dry-run
Would delete VM "agent-code-1" (state: running)
Would stop VM first
Would detach workspace "agent-code-1-ws" (data preserved)
No changes applied (dry run)
$ nexusctl ws create --base workfort/code-agent --name new-ws --dry-run
Would create workspace "new-ws"
Source: workfort/code-agent
Type: btrfs snapshot
No changes applied (dry run)
--dry-run returns exit code 0 on success (the operation would succeed) and non-zero if it would fail.
Composability
Piping
-o name produces one name per line for piping:
$ nexusctl vm list -o name | xargs -I{} nexusctl vm stop {}
$ nexusctl vm list --json name,state --jq '.[] | select(.state == "stopped") | .name' \
| xargs -I{} nexusctl vm delete {} -y
Scripting
Structured JSON output and consistent exit codes make nexusctl scriptable:
#!/bin/bash
set -e
# Create and wait for VM
nexusctl vm create --image workfort/code-agent --name build-vm -o json > /dev/null
nexusctl vm start build-vm
# Check state
state=$(nexusctl vm inspect build-vm --json state --jq '.state' -r)
if [ "$state" != "running" ]; then
echo "VM failed to start" >&2
exit 1
fi
# Do work...
nexusctl attach build-vm --exec "make build"
# Cleanup
nexusctl vm delete build-vm -y
Exit Codes in Conditionals
if nexusctl vm inspect my-vm &>/dev/null; then
echo "VM exists"
else
echo "VM not found, creating..."
nexusctl vm create my-vm
fi
Performance Targets
| Operation | Target |
|---|---|
| CLI startup (parse args, load config) | < 50ms |
vm list (daemon query + render) | < 200ms |
vm inspect (daemon query + render) | < 100ms |
| Shell completion (query + return) | < 300ms |
apply diff computation | < 500ms |
The CLI does no heavy computation. It parses arguments, reads config, makes one HTTP call, and formats the response. If it feels slow, the daemon is the bottleneck, not the CLI.
Architecture
graph LR
subgraph CLI["nexusctl"]
Args["Arg Parser"]
Config["Config Loader"]
Client["HTTP Client"]
Formatter["Output Formatter"]
end
subgraph Daemon["nexusd"]
API["HTTP API :9600"]
end
Args --> Client
Config --> Client
Client -->|"HTTP/JSON"| API
API -->|"JSON response"| Formatter
Formatter -->|"table / json / yaml"| Stdout["stdout"]
Formatter -->|"errors"| Stderr["stderr"]
nexusctl is a single static binary. No runtime dependencies beyond libc. The binary contains:
- Argument parser – Clap with derive macros. Handles subcommands, aliases, shortnames, completions.
- Config loader – Reads YAML from all sources, merges by precedence, resolves env vars.
- HTTP client – reqwest (blocking). One request per command invocation. WebSocket for
attach. - Output formatter – Renders JSON responses into the requested format. Handles color, alignment, truncation.
Startup Sequence
1. Parse args (~5ms)
2. Load config (flags + env + files) (~10ms)
3. HTTP request to daemon (network-bound)
4. Format + print response (~5ms)
No daemon connection is made for --help, --version, completion, or config commands.
Full Command Reference
nexusctl vm list [--role <role>] [--state <state>]
nexusctl vm create [name] [--image <img>] [--role <role>] [--vcpu <n>] [--mem <mib>]
nexusctl vm inspect <name>
nexusctl vm delete <name> [-y]
nexusctl vm start <name>
nexusctl vm stop <name> [--force]
nexusctl vm logs <name> [--follow] [--tail <n>]
nexusctl workspace list [--vm <name>]
nexusctl workspace create [name] [--base <image>] [--size <size>]
nexusctl workspace inspect <name>
nexusctl workspace delete <name> [-y]
nexusctl workspace snapshot <name> [--tag <label>]
nexusctl workspace restore <name> --snapshot <tag>
nexusctl agent list
nexusctl agent create [name] [--image <img>] [--runtime <rt>]
nexusctl agent inspect <name>
nexusctl agent delete <name> [-y]
nexusctl agent start <name>
nexusctl agent stop <name>
nexusctl agent logs <name> [--follow] [--tail <n>]
nexusctl image list
nexusctl image inspect <name>
nexusctl image delete <name> [-y]
nexusctl image import <path> [--name <name>]
nexusctl network list
nexusctl network inspect <bridge>
nexusctl route list [--vm <name>]
nexusctl route create --from <vm:port> --to <vm:port>
nexusctl route inspect <id>
nexusctl route delete <id> [-y]
nexusctl service list [--vm <name>]
nexusctl service inspect <name>
nexusctl attach <vm> [--exec <command>]
nexusctl init
nexusctl apply [--dry-run] [-y] [-f <file>]
nexusctl status
nexusctl version
nexusctl config list
nexusctl config get <key>
nexusctl config set <key> <value>
nexusctl config edit
nexusctl completion bash|zsh|fish
Global Flags
These flags are available on every command:
-o, --output <format> Output format: table, wide, json, yaml, name
--json <fields> Select JSON fields (comma-separated)
--jq <expr> jq filter expression (requires --json or -o json)
--api-url <url> Nexus daemon URL (default: http://127.0.0.1:9600)
--no-color Disable colored output
--no-interactive Disable interactive prompts
-y, --yes Skip confirmation prompts
--dry-run Preview changes without applying
-v, --verbose Increase log verbosity (repeatable: -vvv)
-q, --quiet Suppress non-error output
-h, --help Show help
--version Show version