Step 1: nexusd Skeleton — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: A Rust daemon that starts via systemd, handles signals, logs to journald, and serves a health check endpoint.
Architecture: Single binary (nexusd) in a Cargo workspace with a shared library crate (nexus-lib). Uses tokio for async, axum for HTTP, tracing for structured logging. Config loaded from YAML with sensible defaults.
Tech Stack:
tokio1.x — async runtimeaxum— HTTP serverclap4.x — CLI argument parsingtracing+tracing-subscriber— structured loggingserde+serde_norway— config deserialization (serde_yamlis deprecated/archived)serde_json— API responsesdirs— XDG Base Directory paths
XDG Directory Layout:
- Config:
$XDG_CONFIG_HOME/nexus/nexus.yaml(default:~/.config/nexus/nexus.yaml) - Data (workspaces, images):
$XDG_DATA_HOME/nexus/(default:~/.local/share/nexus/) - State (database):
$XDG_STATE_HOME/nexus/(default:~/.local/state/nexus/) - Runtime (sockets):
$XDG_RUNTIME_DIR/nexus/(default:/run/user/$UID/nexus/)
Task 1: Create Rust Workspace
Files:
- Create:
nexus/Cargo.toml - Create:
nexus/nexusd/Cargo.toml - Create:
nexus/nexusd/src/main.rs - Create:
nexus/nexus-lib/Cargo.toml - Create:
nexus/nexus-lib/src/lib.rs
Step 1: Create directory structure
mkdir -p nexus/nexusd/src nexus/nexus-lib/src
Step 2: Write workspace Cargo.toml
# nexus/Cargo.toml
[workspace]
members = ["nexusd", "nexus-lib"]
resolver = "2"
Step 3: Write nexus-lib Cargo.toml
# nexus/nexus-lib/Cargo.toml
[package]
name = "nexus-lib"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_norway = "0.9"
dirs = "6"
Step 4: Write nexus-lib stub
#![allow(unused)]
fn main() {
// nexus/nexus-lib/src/lib.rs
pub mod config;
}
#![allow(unused)]
fn main() {
// nexus/nexus-lib/src/config.rs
// Filled in Task 2
}
Step 5: Write nexusd Cargo.toml
# nexus/nexusd/Cargo.toml
[package]
name = "nexusd"
version = "0.1.0"
edition = "2021"
[dependencies]
nexus-lib = { path = "../nexus-lib" }
tokio = { version = "1", features = ["full"] }
axum = "0.8"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
Step 6: Write minimal main.rs
// nexus/nexusd/src/main.rs
fn main() {
println!("nexusd");
}
Step 7: Verify build
Run: cd nexus && cargo build
Expected: Compiles with no errors.
Step 8: Commit
git add nexus/
git commit -m "feat: create nexus Rust workspace with nexusd and nexus-lib crates"
Task 2: Configuration Types and Loading
Files:
- Create:
nexus/nexus-lib/src/config.rs - Modify:
nexus/nexus-lib/src/lib.rs
Step 1: Write the failing test
#![allow(unused)]
fn main() {
// nexus/nexus-lib/src/config.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_minimal_config() {
let yaml = r#"
api:
listen: "127.0.0.1:8080"
"#;
let config: Config = serde_norway::from_str(yaml).unwrap();
assert_eq!(config.api.listen, "127.0.0.1:8080");
}
#[test]
fn default_config_values() {
let config = Config::default();
assert_eq!(config.api.listen, "127.0.0.1:9600");
}
#[test]
fn partial_yaml_uses_defaults() {
let yaml = "{}";
let config: Config = serde_norway::from_str(yaml).unwrap();
assert_eq!(config.api.listen, "127.0.0.1:9600");
}
#[test]
fn load_nonexistent_file_returns_not_found() {
let result = Config::load("/nonexistent/path/config.yaml");
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[test]
fn load_invalid_yaml_returns_invalid() {
let dir = std::env::temp_dir();
let path = dir.join("nexus-test-bad-config.yaml");
std::fs::write(&path, "{{invalid yaml").unwrap();
let result = Config::load(&path);
assert!(result.is_err());
assert!(!result.unwrap_err().is_not_found());
std::fs::remove_file(&path).ok();
}
}
}
Step 2: Run tests to verify they fail
Run: cd nexus && cargo test -p nexus-lib
Expected: FAIL — Config type does not exist yet.
Step 3: Implement Config
#![allow(unused)]
fn main() {
// nexus/nexus-lib/src/config.rs
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum ConfigError {
NotFound(std::io::Error),
Invalid(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::NotFound(e) => write!(f, "config file not found: {e}"),
ConfigError::Invalid(e) => write!(f, "invalid config: {e}"),
}
}
}
impl std::error::Error for ConfigError {}
impl ConfigError {
pub fn is_not_found(&self) -> bool {
matches!(self, ConfigError::NotFound(_))
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Config {
pub api: ApiConfig,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ApiConfig {
pub listen: String,
}
impl Default for Config {
fn default() -> Self {
Config {
api: ApiConfig::default(),
}
}
}
impl Default for ApiConfig {
fn default() -> Self {
ApiConfig {
listen: "127.0.0.1:9600".to_string(),
}
}
}
/// Returns the default config file path: $XDG_CONFIG_HOME/nexus/nexus.yaml
pub fn default_config_path() -> PathBuf {
let config_dir = dirs::config_dir()
.expect("cannot determine XDG_CONFIG_HOME")
.join("nexus");
config_dir.join("nexus.yaml")
}
impl Config {
pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let content = std::fs::read_to_string(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ConfigError::NotFound(e)
} else {
ConfigError::Invalid(e.to_string())
}
})?;
let config: Config =
serde_norway::from_str(&content).map_err(|e| ConfigError::Invalid(e.to_string()))?;
Ok(config)
}
}
}
Step 4: Run tests to verify they pass
Run: cd nexus && cargo test -p nexus-lib
Expected: All 4 tests PASS.
Step 5: Commit
git add nexus/nexus-lib/
git commit -m "feat(nexus-lib): add Config type with YAML loading and defaults"
Task 3: Health Endpoint
Files:
- Create:
nexus/nexusd/src/api.rs - Modify:
nexus/nexusd/src/main.rs
Step 1: Write the failing test
#![allow(unused)]
fn main() {
// nexus/nexusd/src/api.rs
use axum::{Json, Router, routing::get};
use serde::Serialize;
#[derive(Serialize)]
struct HealthResponse {
status: String,
}
async fn health() -> Json<HealthResponse> {
todo!()
}
pub fn router() -> Router {
Router::new().route("/v1/health", get(health))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[tokio::test]
async fn health_returns_ok() {
let app = router();
let response = app
.oneshot(Request::get("/v1/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok");
}
}
}
Step 2: Run test to verify it fails
Run: cd nexus && cargo test -p nexusd api::tests::health_returns_ok
Expected: FAIL — todo!() panics.
Step 3: Implement the handler
Replace todo!() with the real implementation:
#![allow(unused)]
fn main() {
async fn health() -> Json<HealthResponse> {
Json(HealthResponse {
status: "ok".to_string(),
})
}
}
Step 4: Add module to main.rs
// nexus/nexusd/src/main.rs
mod api;
fn main() {
println!("nexusd");
}
Step 5: Run test to verify it passes
Run: cd nexus && cargo test -p nexusd api::tests::health_returns_ok
Expected: PASS.
Step 6: Commit
git add nexus/nexusd/
git commit -m "feat(nexusd): add GET /v1/health endpoint"
Task 4: Signal Handling and Graceful Shutdown
Files:
- Create:
nexus/nexusd/src/server.rs
This task creates the server startup and shutdown logic. Signal handling is difficult to unit test in isolation, so it will be verified in the integration test (Task 7).
Step 1: Implement the server module
#![allow(unused)]
fn main() {
// nexus/nexusd/src/server.rs
use crate::api;
use nexus_lib::config::Config;
use tokio::net::TcpListener;
use tracing::info;
pub async fn run(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
let app = api::router();
let listener = TcpListener::bind(&config.api.listen).await?;
info!(listen = %config.api.listen, "HTTP API ready");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
info!("nexusd stopped");
Ok(())
}
async fn shutdown_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())
.expect("failed to install SIGTERM handler");
let mut sigint = signal(SignalKind::interrupt())
.expect("failed to install SIGINT handler");
tokio::select! {
_ = sigterm.recv() => info!("received SIGTERM, shutting down"),
_ = sigint.recv() => info!("received SIGINT, shutting down"),
}
}
}
Step 2: Commit
git add nexus/nexusd/src/server.rs
git commit -m "feat(nexusd): add server startup with graceful shutdown on SIGTERM/SIGINT"
Task 5: Logging
Files:
- Create:
nexus/nexusd/src/logging.rs
Step 1: Implement logging setup
#![allow(unused)]
fn main() {
// nexus/nexusd/src/logging.rs
use tracing_subscriber::{fmt, EnvFilter};
pub fn init() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
fmt()
.with_env_filter(filter)
.init();
}
}
Uses RUST_LOG env var when set, defaults to info. systemd captures stdout to journald automatically — no special journald integration needed.
Step 2: Commit
git add nexus/nexusd/src/logging.rs
git commit -m "feat(nexusd): add tracing-based logging with env filter"
Task 6: CLI Arguments and main() Wiring
Files:
- Modify:
nexus/nexusd/src/main.rs
Step 1: Wire everything together
// nexus/nexusd/src/main.rs
use clap::Parser;
use nexus_lib::config::{self, Config};
use tracing::{error, info};
mod api;
mod logging;
mod server;
#[derive(Parser)]
#[command(name = "nexusd", about = "WorkFort Nexus daemon")]
struct Cli {
/// Path to configuration file
/// [default: $XDG_CONFIG_HOME/nexus/nexus.yaml]
#[arg(long)]
config: Option<String>,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
logging::init();
let config_path = cli.config
.map(std::path::PathBuf::from)
.unwrap_or_else(config::default_config_path);
let config = match Config::load(&config_path) {
Ok(config) => {
info!(config_path = %config_path.display(), "loaded configuration");
config
}
Err(e) if e.is_not_found() => {
info!("no config file found, using defaults");
Config::default()
}
Err(e) => {
error!(error = %e, path = %config_path.display(), "invalid configuration file");
std::process::exit(1);
}
};
info!("nexusd starting");
if let Err(e) = server::run(&config).await {
error!(error = %e, "daemon failed");
std::process::exit(1);
}
}
Step 2: Verify build
Run: cd nexus && cargo build
Expected: Compiles with no errors.
Step 3: Verify --help
Run: cd nexus && cargo run -p nexusd -- --help
Expected:
WorkFort Nexus daemon
Usage: nexusd [OPTIONS]
Options:
--config <CONFIG> Path to configuration file [default: $XDG_CONFIG_HOME/nexus/nexus.yaml]
-h, --help Print help
Step 4: Quick manual smoke test
Run: cd nexus && cargo run -p nexusd
Expected: Daemon starts, logs “HTTP API ready” with listen=127.0.0.1:9600. In another terminal:
Run: curl -s http://127.0.0.1:9600/v1/health | python -m json.tool
Expected:
{
"status": "ok"
}
Kill the daemon with Ctrl-C. Expected: logs “received SIGINT, shutting down” and “nexusd stopped”, then exits cleanly.
Step 5: Commit
git add nexus/nexusd/
git commit -m "feat(nexusd): wire CLI args, config loading, logging, and server into main"
Task 7: Systemd Unit File
Files:
- Create:
nexus/dist/nexus.service
Step 1: Write the unit file
# nexus/dist/nexus.service
[Unit]
Description=WorkFort Nexus Daemon
[Service]
Type=exec
ExecStart=%h/.cargo/bin/nexusd
Restart=on-failure
RestartSec=5
Environment=RUST_LOG=info
[Install]
WantedBy=default.target
Notes:
Type=execwaits for the binary to launch successfully, catching missing binary errors (better thanType=simple).%hexpands to the user’s home directory. Binary path will be adjusted once packaging is set up.StandardOutput=journalandSyslogIdentifierare omitted as they are systemd defaults.
Step 2: Test with systemd
# Install the unit file
mkdir -p ~/.config/systemd/user
cp nexus/dist/nexus.service ~/.config/systemd/user/
systemctl --user daemon-reload
# First, build and install the binary somewhere on PATH
cd nexus && cargo build --release
cp target/release/nexusd ~/.cargo/bin/
# Start and verify
systemctl --user start nexus
systemctl --user status nexus
curl -s http://127.0.0.1:9600/v1/health
# Check logs
journalctl --user -u nexus -n 20
# Stop
systemctl --user stop nexus
Expected: Service starts, health endpoint responds, logs appear in journald, service stops cleanly on stop.
Step 3: Commit
git add nexus/dist/
git commit -m "feat(nexusd): add systemd user service unit file"
Task 8: Integration Test
Files:
- Create:
nexus/nexusd/tests/daemon.rs - Modify:
nexus/nexusd/Cargo.toml(add dev-dependencies)
Step 1: Add dev-dependencies
Add to nexus/nexusd/Cargo.toml:
Merge into the existing [dev-dependencies] section:
[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
reqwest = { version = "0.13", features = ["json"] }
nix = { version = "0.30", features = ["signal"] }
serde_json = "1"
Step 2: Write the integration test
#![allow(unused)]
fn main() {
// nexus/nexusd/tests/daemon.rs
use std::process::{Command, Child};
use std::time::Duration;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
fn start_daemon() -> Child {
let binary = env!("CARGO_BIN_EXE_nexusd");
Command::new(binary)
.env("RUST_LOG", "info")
.spawn()
.expect("failed to start nexusd")
}
fn stop_daemon(child: &Child) {
signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGTERM)
.expect("failed to send SIGTERM");
}
#[tokio::test]
async fn daemon_starts_serves_health_and_stops() {
let mut child = start_daemon();
// Wait for the daemon to be ready
let client = reqwest::Client::new();
let mut ready = false;
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(100)).await;
if client.get("http://127.0.0.1:9600/v1/health")
.send()
.await
.is_ok()
{
ready = true;
break;
}
}
assert!(ready, "daemon did not become ready within 5 seconds");
// Verify health endpoint
let resp = client.get("http://127.0.0.1:9600/v1/health")
.send()
.await
.expect("health request failed");
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["status"], "ok");
// Graceful shutdown
stop_daemon(&child);
let status = child.wait().expect("failed to wait on daemon");
assert!(status.success(), "daemon exited with non-zero status: {}", status);
}
}
Step 3: Run the integration test
Run: cd nexus && cargo test -p nexusd --test daemon
Expected: PASS — daemon starts, health endpoint returns {"status":"ok"}, SIGTERM causes clean exit with code 0.
Note: This test uses a hardcoded port (9600). If the port is in use, the test will fail. For now this is acceptable — a single integration test doesn’t need port randomization. Address this when adding more integration tests.
Step 4: Commit
git add nexus/nexusd/
git commit -m "test(nexusd): add integration test for daemon lifecycle"
Verification Checklist
After all tasks are complete, verify the following:
-
cargo buildsucceeds with no warnings -
cargo test --workspace— all tests pass -
cargo run -p nexusd -- --help— prints usage -
curl localhost:9600/v1/health— returns{"status":"ok"} -
systemctl --user start nexus— daemon starts -
journalctl --user -u nexus— shows structured log output -
systemctl --user stop nexus— daemon stops cleanly (exit 0) - Sending
SIGTERMto the process causes graceful shutdown - Sending
SIGINT(Ctrl-C) to the process causes graceful shutdown - No config file present — daemon starts with defaults
- Config file present — daemon uses configured values