← LOGBOOK LOG-456
WORKING · SOFTWARE ·
RUSTHANDOFFCARGOWORKSPACECLIARCHITECTURENETWORKING

HandOff: Cargo Workspace and CLI Boundary

A two-crate Rust workspace keeps command-line parsing and presentation separate from the networking core that future interfaces will share.

HandOff separates interface policy from networking behavior. The command-line crate interprets user input and displays outcomes; the core crate owns operations that must behave identically when invoked from the CLI or a future GPUI application.

HandOff workspace
├── handoff-cli
│   ├── read arguments
│   ├── validate the target port
│   └── display messages

└── handoff-core
    ├── check target reachability
    └── create a LAN listener

The dependency points inward:

handoff-cli ──depends on──> handoff-core
handoff-gpui ──will depend on──> handoff-core

handoff-core does not know which interface requested an operation. This is the architectural constraint that keeps proxying, LAN discovery, port selection, and lifecycle management out of presentation code.

A Virtual Cargo Workspace

The root Cargo.toml coordinates two packages:

[workspace]
resolver = "3"
members = [
  "crates/handoff-core",
  "crates/handoff-cli",
]

The manifest is virtual because it has [workspace] but no [package]. The root is therefore not a third Rust package and has no src/main.rs or src/lib.rs. It selects member crates and workspace behavior.

Cargo manages the members together. They share the root Cargo.lock and the root target/ build directory, while retaining separate manifests, dependencies, and public APIs.

HandOff/
├── Cargo.toml
├── Cargo.lock
└── crates/
    ├── handoff-core/
    │   ├── Cargo.toml
    │   └── src/lib.rs
    └── handoff-cli/
        ├── Cargo.toml
        └── src/main.rs

resolver = "3" is declared explicitly because a virtual workspace has no root package edition from which Cargo could infer a resolver. Both member packages use the Rust 2024 edition.

Workspace commands operate across both members:

cargo check --workspace
cargo test --workspace
cargo fmt --all --check

A single member remains selectable with -p:

cargo run -p handoff-cli -- 3000
cargo test -p handoff-core

The Dependency Boundary

The CLI declares the local library as a path dependency:

[dependencies]
handoff-core = { path = "../handoff-core" }

Cargo package names may contain hyphens, but Rust identifiers cannot. The package handoff-core is imported in source as handoff_core:

use handoff_core::is_reachable;

The import works only because the core marks the function public:

pub fn is_reachable(port: u16) -> bool {
    // networking operation
}

Items are private to their module by default. pub makes this function part of the library boundary, so changing its name, arguments, return type, or semantics affects every interface built on the core.

The dependency direction prevents the inverse coupling. handoff-core cannot print CLI messages or read process arguments without explicitly depending on interface concerns. Its operations return values; callers decide how those values should appear.

Reading One Positional Argument

The current command accepts one target port:

handoff 3000

std::env::args returns an iterator of owned String values. Index zero is the executable path, so index one is the first user argument:

use std::env;

let port = env::args().nth(1);

The return type is Option<String> because the iterator might not contain that item:

handoff 3000 → Some("3000")
handoff      → None

Option makes missing input part of the type instead of representing it with an empty string or a sentinel number. The outer match chooses the CLI behavior:

match port {
    Some(value) => {
        // validate and use the argument
    }
    None => {
        println!("Usage: handoff <port>");
    }
}

Port Validation

The parser isolates text validation from networking:

fn parse_port(value: &str) -> Result<u16, String> {
    let port = match value.parse::<u16>() {
        Ok(port) => port,
        Err(_) => {
            return Err(String::from("Port must be between 1 and 65535"));
        }
    };

    if port == 0 {
        return Err(String::from("Port must be between 1 and 65535"));
    }

    Ok(port)
}

The input is &str, so the function borrows the argument without taking ownership of its String. parse::<u16>() accepts decimal values representable by an unsigned 16-bit integer. Nonnumeric text, negative numbers, and values above 65535 become Err automatically. The explicit zero check narrows the CLI’s target-port domain to 1..=65535.

"3000"  → Ok(3000)
"0"     → Err(...)
"hello" → Err(...)
"65536" → Err(...)

The function returns Result rather than printing because validation is independent of presentation. The CLI decides to send the error to standard error:

match parse_port(&value) {
    Ok(port) => {
        // ask the core about the target
    }
    Err(message) => {
        eprintln!("{message}");
    }
}

The nested matches describe the current decision tree completely:

argument present?
├── no  → print usage
└── yes → valid target port?
          ├── no  → print validation error
          └── yes → call handoff-core

Argument parsing can later move to a dedicated CLI parser when flags such as --port appear. The important boundary is already present: parsing produces a typed port before the networking core receives it.

Parser Tests

The parser’s type makes its cases deterministic and independent of the network:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_valid_port() {
        assert_eq!(parse_port("3000"), Ok(3000));
    }

    #[test]
    fn rejects_zero() {
        assert!(parse_port("0").is_err());
    }

    #[test]
    fn rejects_non_number() {
        assert!(parse_port("hello").is_err());
    }

    #[test]
    fn rejects_number_above_u16_range() {
        assert!(parse_port("65536").is_err());
    }
}

#[cfg(test)] compiles the module only for test builds. #[test] registers each function with the test harness. These four cases establish the current port invariant without starting HandOff or choosing a real service port.

The workspace now has a stable direction of change. Interface crates translate user intent into typed inputs. handoff-core performs networking and returns observable results. The current CLI message may still say Sharing, but reachability is the only completed behavior at this stage; no request forwarding has begun.

Sources