Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli(profile): align with current Radicle structure; safer atomic copy
anon committed 4 months ago
commit 49b6cd29ee53c608a458b02978dd959fd92d3cce
parent 829b7cba1802cdcc6a1648e7894c9a7e21b0cf37
4 files changed +664 -565
modified crates/radicle-cli/src/commands.rs
@@ -1,64 +1,33 @@
-
#[path = "commands/auth.rs"]
-
pub mod rad_auth;
-
#[path = "commands/block.rs"]
-
pub mod rad_block;
-
#[path = "commands/checkout.rs"]
-
pub mod rad_checkout;
-
#[path = "commands/clean.rs"]
-
pub mod rad_clean;
-
#[path = "commands/clone.rs"]
-
pub mod rad_clone;
-
#[path = "commands/cob.rs"]
-
pub mod rad_cob;
-
#[path = "commands/config.rs"]
-
pub mod rad_config;
-
#[path = "commands/debug.rs"]
-
pub mod rad_debug;
-
#[path = "commands/diff.rs"]
-
pub mod rad_diff;
-
#[path = "commands/follow.rs"]
-
pub mod rad_follow;
-
#[path = "commands/fork.rs"]
-
pub mod rad_fork;
-
#[path = "commands/help.rs"]
-
pub mod rad_help;
-
#[path = "commands/id.rs"]
-
pub mod rad_id;
-
#[path = "commands/inbox.rs"]
-
pub mod rad_inbox;
-
#[path = "commands/init.rs"]
-
pub mod rad_init;
-
#[path = "commands/inspect.rs"]
-
pub mod rad_inspect;
-
#[path = "commands/issue.rs"]
-
pub mod rad_issue;
-
#[path = "commands/ls.rs"]
-
pub mod rad_ls;
-
#[path = "commands/node.rs"]
-
pub mod rad_node;
-
#[path = "commands/patch.rs"]
-
pub mod rad_patch;
-
#[path = "commands/path.rs"]
-
pub mod rad_path;
-
#[path = "commands/publish.rs"]
-
pub mod rad_publish;
-
#[path = "commands/remote.rs"]
-
pub mod rad_remote;
-
#[path = "commands/seed.rs"]
-
pub mod rad_seed;
-
#[path = "commands/self.rs"]
-
pub mod rad_self;
-
#[path = "commands/stats.rs"]
-
pub mod rad_stats;
-
#[path = "commands/sync.rs"]
-
pub mod rad_sync;
-
#[path = "commands/unblock.rs"]
-
pub mod rad_unblock;
-
#[path = "commands/unfollow.rs"]
-
pub mod rad_unfollow;
-
#[path = "commands/unseed.rs"]
-
pub mod rad_unseed;
-
#[path = "commands/watch.rs"]
-
pub mod rad_watch;
+
pub mod auth;
+
pub mod block;
+
pub mod checkout;
+
pub mod clean;
+
pub mod clone;
+
pub mod cob;
+
pub mod config;
+
pub mod debug;
+
pub mod diff;
+
pub mod follow;
+
pub mod fork;
+
pub mod id;
+
pub mod inbox;
+
pub mod init;
+
pub mod inspect;
+
pub mod issue;
+
pub mod ls;
+
pub mod node;
+
pub mod patch;
+
pub mod path;
+
pub mod publish;
+
pub mod remote;
+
pub mod seed;
+
pub mod stats;
+
pub mod sync;
+
pub mod unblock;
+
pub mod unfollow;
+
pub mod unseed;
+
pub mod watch;
#[path = "commands/profile.rs"]
pub mod rad_profile;
+
#[path = "commands/self.rs"]
+
pub mod rad_self;
modified crates/radicle-cli/src/commands/profile.rs
@@ -1,75 +1,23 @@
+
#[path = "profile/args.rs"]
+
mod args;
+
pub use args::Args;
+

use std::env;
-
use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
+
use std::process;

-
use lexopt::prelude::*;
-
use lexopt::Parser;
use thiserror::Error;

use crate::terminal as term;
-
use crate::terminal::args::{Args, Help};
-

-
pub const HELP: Help = Help {
-
    name: "profile",
-
    description: "Manage Radicle CLI profiles (config.json and keys/* per user)",
-
    version: env!("RADICLE_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad profile new <name> [--from <name>] [--force]
-
    rad profile switch <name|default> [--print-env]
-
    rad profile list
-
    rad profile current
-
    rad profile remove <name> [-y]
-

-
Options
-

-
    --from <name>     Create from existing profile (for: new)
-
    --force, -f       Overwrite existing profile or reserved name (for: new)
-
    --print-env       Print shell export for RAD_PROFILE (for: switch)
-
    --yes, -y         Confirm removal (for: remove)
-
    --help            Print help
-

-
Notes
-

-
    • 'default' is the pseudo-profile representing the root (~/.radicle).
-
      'rad profile switch default' persists the current root into the active profile,
-
      clears the root, and unsets the active profile marker.
-
"#,
-
};
-

-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-
enum OperationName {
-
    New,
-
    Switch,
-
    List,
-
    Current,
-
    Remove,
-
}
-

-
#[derive(Debug)]
-
struct Operation {
-
    name: OperationName,
-
    target: Option<String>,
-
    from: Option<String>,
-
    force: bool,
-
    print_env: bool,
-
    yes: bool,
-
}
-

-
#[derive(Debug)]
-
pub struct Options(Operation);

#[derive(Error, Debug)]
pub enum ProfileError {
    #[error("home directory not available")]
    Home,
-
    #[error("profiles root not found: {0}")]
-
    ProfilesRoot(PathBuf),
-
    #[error("active profile marker error: {0}")]
-
    ActiveMarker(PathBuf),
+
    #[error("invalid profile name: {0}")]
+
    InvalidName(String),
    #[error("profile not found: {0}")]
    NotFound(String),
    #[error("profile already exists: {0}")]
@@ -80,6 +28,12 @@ pub enum ProfileError {
    RemoveDefault,
    #[error("confirmation required, use -y/--yes")]
    ConfirmationRequired,
+
    #[error("active profile marker error: {0}")]
+
    ActiveMarker(PathBuf),
+
    #[error("profile operation locked: {0}")]
+
    Locked(PathBuf),
+
    #[error("unsupported file type: {0}")]
+
    UnsupportedFileType(PathBuf),
    #[error("io error: {0}")]
    Io(#[from] io::Error),
    #[error("invalid state: {0}")]
@@ -88,142 +42,104 @@ pub enum ProfileError {

type Result<T> = std::result::Result<T, ProfileError>;

-
impl Args for Options {
-
    fn from_args(v: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
-
        let mut p = Parser::from_args(v);
-
        let mut op = None;
-
        let mut target = None;
-
        let mut from = None;
-
        let mut force = false;
-
        let mut print_env = false;
-
        let mut yes = false;
-

-
        while let Some(arg) = p.next()? {
-
            match arg {
-
                Value(val) if op.is_none() => {
-
                    let s = term::args::string(&val);
-
                    op = Some(match s.as_str() {
-
                        "new" => OperationName::New,
-
                        "switch" => OperationName::Switch,
-
                        "list" => OperationName::List,
-
                        "current" => OperationName::Current,
-
                        "remove" => OperationName::Remove,
-
                        other => anyhow::bail!("unknown subcommand: {other}"),
-
                    });
-
                }
-
                Value(val) => {
-
                    target = Some(term::args::string(&val));
-
                }
-
                Long("from") => {
-
                    from = Some(term::args::string(&p.value()?));
-
                }
-
                Long("force") | Short('f') => {
-
                    force = true;
-
                }
-
                Long("print-env") => {
-
                    print_env = true;
-
                }
-
                Long("yes") | Short('y') => {
-
                    yes = true;
-
                }
-
                Long("help") | Short('h') => return Err(term::args::Error::Help.into()),
-
                _ => anyhow::bail!("unexpected argument"),
-
            }
-
        }
-

-
        let name = op.ok_or_else(|| anyhow::anyhow!("missing subcommand"))?;
-
        if matches!(name, OperationName::New | OperationName::Switch | OperationName::Remove)
-
            && target.is_none()
-
        {
-
            anyhow::bail!("missing <name>");
-
        }
-

-
        Ok((
-
            Options(Operation {
-
                name,
-
                target,
-
                from,
-
                force,
-
                print_env,
-
                yes,
-
            }),
-
            vec![],
-
        ))
-
    }
+
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
+
    run_inner(args, ctx).map_err(|e| anyhow::anyhow!(e))
}

-
pub fn run(opts: Options, ctx: impl term::Context) -> anyhow::Result<()> {
-
    run_inner(opts, ctx).map_err(|e| anyhow::anyhow!(e))
-
}
-

-
fn run_inner(opts: Options, ctx: impl term::Context) -> Result<()> {
+
fn run_inner(args: Args, ctx: impl term::Context) -> Result<()> {
    let home = resolve_home(ctx)?;
+
    let _lock = acquire_lock(&home)?;
+

+
    match args.op {
+
        args::Op::New { name, from, force } => {
+
            validate_profile_name(&name)?;

-
    match opts.0.name {
-
        OperationName::New => {
-
            let name = opts.0.target.as_ref().unwrap();
-
            if name == "default" && !opts.0.force {
+
            if name == "default" && !force {
                return Err(ProfileError::AlreadyExists("default".into()));
            }
-
            create_profile(&home, name, opts.0.from.as_deref(), opts.0.force)?;
-
            write_active(&home, name)?;
+

+
            if let Some(ref from_name) = from {
+
                validate_profile_name(from_name)?;
+
                ensure_profile_exists(&home, from_name)?;
+
            }
+

+
            create_profile(&home, &name, from.as_deref(), force)?;
+
            write_active(&home, &name)?;
            clear_root_for_auth(&home)?;
-
            term::success!("Profile {} created", term::format::tertiary(name));
+
            clear_node_fingerprint(&home)?;
+

+
            term::success!("Profile {} created", term::format::tertiary(&name));
        }
-
        OperationName::Switch => {
-
            let name = opts.0.target.as_ref().unwrap();
+

+
        args::Op::Switch { name, print_env } => {
+
            validate_profile_name(&name)?;
+

            if name == "default" {
                if let Some(current) = read_active(&home)? {
                    persist_root_into_profile(&home, &current)?;
                }
+

                clear_root_for_auth(&home)?;
+
                clear_node_fingerprint(&home)?;
                clear_active(&home)?;
-
                if opts.0.print_env {
+

+
                if print_env {
                    println!("unset RAD_PROFILE");
                } else {
                    term::success!("Switched to {}", term::format::tertiary("default"));
                }
            } else {
-
                let active = read_active(&home)?;
-
                if let Some(current) = active.as_deref() {
-
                    persist_root_into_profile(&home, current)?;
+
                ensure_profile_exists(&home, &name)?;
+

+
                if let Some(current) = read_active(&home)? {
+
                    persist_root_into_profile(&home, &current)?;
                }
-
                copy_profile_into_root(&home, name)?;
-
                write_active(&home, name)?;
-
                if opts.0.print_env {
+

+
                copy_profile_into_root(&home, &name)?;
+
                clear_node_fingerprint(&home)?;
+
                write_active(&home, &name)?;
+

+
                if print_env {
                    println!("export RAD_PROFILE={name}");
                } else {
-
                    term::success!("Switched to {}", term::format::tertiary(name));
+
                    term::success!("Switched to {}", term::format::tertiary(&name));
                }
            }
        }
-
        OperationName::List => {
+

+
        args::Op::List => {
            list_profiles(&home)?;
        }
-
        OperationName::Current => {
+

+
        args::Op::Current => {
            let cur = read_active(&home)?.unwrap_or_else(|| "default".to_string());
            println!("{cur}");
        }
-
        OperationName::Remove => {
-
            let name = opts.0.target.as_ref().unwrap();
+

+
        args::Op::Remove { name, yes } => {
+
            validate_profile_name(&name)?;
+

            if name == "default" {
                return Err(ProfileError::RemoveDefault);
            }
+

            let cur = read_active(&home)?;
-
            if cur.as_deref() == Some(name) {
-
                return Err(ProfileError::RemoveActive(name.clone()));
+
            if cur.as_deref() == Some(name.as_str()) {
+
                return Err(ProfileError::RemoveActive(name));
            }
-
            if !opts.0.yes {
+

+
            if !yes {
                return Err(ProfileError::ConfirmationRequired);
            }
-
            remove_profile(&home, name)?;
-
            term::success!("Profile {} removed", term::format::tertiary(name));
+

+
            remove_profile(&home, &name)?;
+
            term::success!("Profile {} removed", term::format::tertiary(&name));
        }
    }
+

    Ok(())
}

-

fn resolve_home(ctx: impl term::Context) -> Result<PathBuf> {
    if let Ok(p) = ctx.profile() {
        return Ok(p.home().path().to_path_buf());
@@ -237,6 +153,25 @@ fn resolve_home(ctx: impl term::Context) -> Result<PathBuf> {
    Err(ProfileError::Home)
}

+
fn validate_profile_name(name: &str) -> Result<()> {
+
    if name.is_empty() || name == "." || name == ".." {
+
        return Err(ProfileError::InvalidName(name.to_string()));
+
    }
+
    if name.len() > 64 {
+
        return Err(ProfileError::InvalidName(name.to_string()));
+
    }
+
    if name.contains('/') || name.contains('\\') || name.contains('\0') {
+
        return Err(ProfileError::InvalidName(name.to_string()));
+
    }
+
    if !name
+
        .bytes()
+
        .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
+
        {
+
            return Err(ProfileError::InvalidName(name.to_string()));
+
        }
+
        Ok(())
+
}
+

fn profiles_root(home: &Path) -> PathBuf {
    home.join("profiles")
}
@@ -245,6 +180,10 @@ fn active_file(home: &Path) -> PathBuf {
    home.join(".active_profile")
}

+
fn lock_file(home: &Path) -> PathBuf {
+
    home.join(".radicle_cli_profile.lock")
+
}
+

fn root_config(home: &Path) -> PathBuf {
    home.join("config.json")
}
@@ -253,6 +192,10 @@ fn root_keys(home: &Path) -> PathBuf {
    home.join("keys")
}

+
fn node_fingerprint(home: &Path) -> PathBuf {
+
    home.join("node").join("fingerprint")
+
}
+

fn profile_dir(home: &Path, name: &str) -> PathBuf {
    profiles_root(home).join(name)
}
@@ -265,34 +208,105 @@ fn profile_keys(home: &Path, name: &str) -> PathBuf {
    profile_dir(home, name).join("keys")
}

+
struct LockGuard {
+
    path: PathBuf,
+
}
+

+
impl Drop for LockGuard {
+
    fn drop(&mut self) {
+
        let _ = fs::remove_file(&self.path);
+
    }
+
}
+

+
fn acquire_lock(home: &Path) -> Result<LockGuard> {
+
    fs::create_dir_all(home)?;
+
    let path = lock_file(home);
+

+
    for _ in 0..2 {
+
        match fs::OpenOptions::new().write(true).create_new(true).open(&path) {
+
            Ok(mut f) => {
+
                use io::Write as _;
+
                let _ = writeln!(f, "{}", process::id());
+
                let _ = f.sync_all();
+
                return Ok(LockGuard { path });
+
            }
+
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
+
                if is_stale_lock(&path) {
+
                    let _ = fs::remove_file(&path);
+
                    continue;
+
                }
+
                return Err(ProfileError::Locked(path));
+
            }
+
            Err(e) => return Err(ProfileError::Io(e)),
+
        }
+
    }
+

+
    Err(ProfileError::Locked(path))
+
}
+

+
fn is_stale_lock(path: &Path) -> bool {
+
    let Ok(s) = fs::read_to_string(path) else { return false; };
+
    let Ok(pid) = s.trim().parse::<u32>() else { return false; };
+
    let proc_path = PathBuf::from("/proc").join(pid.to_string());
+
    !proc_path.exists()
+
}
+

fn ensure_profiles_root(home: &Path) -> Result<()> {
    fs::create_dir_all(profiles_root(home))?;
    Ok(())
}

+
fn ensure_profile_exists(home: &Path, name: &str) -> Result<()> {
+
    let dir = profile_dir(home, name);
+
    if !dir.exists() {
+
        return Err(ProfileError::NotFound(name.to_string()));
+
    }
+
    Ok(())
+
}
+

+
fn clear_node_fingerprint(home: &Path) -> Result<()> {
+
    remove_file_if_exists(&node_fingerprint(home))?;
+
    Ok(())
+
}
+

fn create_profile(home: &Path, name: &str, from: Option<&str>, force: bool) -> Result<()> {
    ensure_profiles_root(home)?;
+

    let dst_dir = profile_dir(home, name);
+

    if dst_dir.exists() && !force {
        return Err(ProfileError::AlreadyExists(name.to_string()));
    }
+

+
    if dst_dir.exists() && force {
+
        fs::remove_dir_all(&dst_dir)?;
+
    }
+

    fs::create_dir_all(&dst_dir)?;
    fs::create_dir_all(profile_keys(home, name))?;
+

    if let Some(src) = from {
+
        ensure_profile_exists(home, src)?;
+

        let src_cfg = profile_config(home, src);
        let src_keys = profile_keys(home, src);
+

        let dst_cfg = profile_config(home, name);
        let dst_keys = profile_keys(home, name);
+

        if src_cfg.exists() {
-
            if let Some(parent) = dst_cfg.parent() {
-
                fs::create_dir_all(parent)?;
-
            }
-
            fs::copy(src_cfg, dst_cfg)?;
+
            atomic_copy_file(&src_cfg, &dst_cfg)?;
+
        } else {
+
            remove_file_if_exists(&dst_cfg)?;
        }
+

        if src_keys.exists() {
-
            copy_dir(&src_keys, &dst_keys)?;
+
            atomic_copy_dir_replace(&src_keys, &dst_keys)?;
+
        } else {
+
            remove_dir_if_exists(&dst_keys)?;
        }
    }
+

    Ok(())
}

@@ -316,93 +330,87 @@ fn read_active(home: &Path) -> Result<Option<String>> {

fn write_active(home: &Path, name: &str) -> Result<()> {
    let path = active_file(home);
-
    if let Some(parent) = path.parent() {
-
        fs::create_dir_all(parent)?;
-
    }
-
    fs::write(path, format!("{name}\n"))?;
+
    atomic_write_text(&path, &format!("{name}\n"))?;
    Ok(())
}

fn clear_active(home: &Path) -> Result<()> {
-
    let path = active_file(home);
-
    if path.exists() {
-
        fs::remove_file(path)?;
-
    }
+
    remove_file_if_exists(&active_file(home))?;
    Ok(())
}

fn persist_root_into_profile(home: &Path, name: &str) -> Result<()> {
    let src_cfg = root_config(home);
    let src_keys = root_keys(home);
+

    let dst_cfg = profile_config(home, name);
    let dst_keys = profile_keys(home, name);

-
    if let Some(parent) = dst_cfg.parent() {
-
        fs::create_dir_all(parent)?;
-
    }
-
    fs::create_dir_all(&dst_keys)?;
+
    ensure_profiles_root(home)?;
+
    fs::create_dir_all(profile_dir(home, name))?;

    if src_cfg.exists() {
-
        fs::copy(src_cfg, dst_cfg)?;
+
        atomic_copy_file(&src_cfg, &dst_cfg)?;
+
    } else {
+
        remove_file_if_exists(&dst_cfg)?;
    }
+

    if src_keys.exists() {
-
        copy_dir(&src_keys, &dst_keys)?;
+
        atomic_copy_dir_replace(&src_keys, &dst_keys)?;
+
    } else {
+
        remove_dir_if_exists(&dst_keys)?;
    }
+

    Ok(())
}

fn copy_profile_into_root(home: &Path, name: &str) -> Result<()> {
    let src_cfg = profile_config(home, name);
    let src_keys = profile_keys(home, name);
+

    let dst_cfg = root_config(home);
    let dst_keys = root_keys(home);

-
    if dst_cfg.exists() {
-
        fs::remove_file(&dst_cfg)?;
-
    }
-
    if dst_keys.exists() {
-
        fs::remove_dir_all(&dst_keys)?;
-
    }
    if src_cfg.exists() {
-
        if let Some(parent) = dst_cfg.parent() {
-
            fs::create_dir_all(parent)?;
-
        }
-
        fs::copy(src_cfg, dst_cfg)?;
+
        atomic_copy_file(&src_cfg, &dst_cfg)?;
+
    } else {
+
        remove_file_if_exists(&dst_cfg)?;
    }
+

    if src_keys.exists() {
-
        fs::create_dir_all(&dst_keys)?;
-
        copy_dir(&src_keys, &dst_keys)?;
+
        atomic_copy_dir_replace(&src_keys, &dst_keys)?;
+
    } else {
+
        remove_dir_if_exists(&dst_keys)?;
    }
+

    Ok(())
}

fn clear_root_for_auth(home: &Path) -> Result<()> {
-
    let cfg = root_config(home);
-
    let keys = root_keys(home);
-
    if cfg.exists() {
-
        fs::remove_file(cfg)?;
-
    }
-
    if keys.exists() {
-
        fs::remove_dir_all(keys)?;
-
    }
+
    remove_file_if_exists(&root_config(home))?;
+
    remove_dir_if_exists(&root_keys(home))?;
+
    remove_file_if_exists(&node_fingerprint(home))?;
    Ok(())
}

fn list_profiles(home: &Path) -> Result<()> {
    ensure_profiles_root(home)?;
+

    let active = read_active(home)?;
    let mut names = Vec::<String>::new();
+

    for entry in fs::read_dir(profiles_root(home))? {
        let e = entry?;
        if e.file_type()?.is_dir() {
            if let Some(n) = e.file_name().to_str() {
-
                names.push(n.to_string());
+
                if !n.starts_with('.') {
+
                    names.push(n.to_string());
+
                }
            }
        }
    }
    names.sort();

-
    
    if active.is_none() {
        println!("* default");
    } else {
@@ -416,20 +424,201 @@ fn list_profiles(home: &Path) -> Result<()> {
            println!("  {n}");
        }
    }
+

    Ok(())
}

-
fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
+
fn remove_file_if_exists(path: &Path) -> Result<()> {
+
    match fs::remove_file(path) {
+
        Ok(()) => Ok(()),
+
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
+
        Err(e) => Err(ProfileError::Io(e)),
+
    }
+
}
+

+
fn remove_dir_if_exists(path: &Path) -> Result<()> {
+
    match fs::remove_dir_all(path) {
+
        Ok(()) => Ok(()),
+
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
+
        Err(e) => Err(ProfileError::Io(e)),
+
    }
+
}
+

+
fn best_effort_sync_parent_dir(path: &Path) {
+
    let Some(parent) = path.parent() else { return; };
+
    if let Ok(dir) = fs::File::open(parent) {
+
        let _ = dir.sync_all();
+
    }
+
}
+

+
fn unique_sibling(dst: &Path, tag: &str, attempt: u32) -> PathBuf {
+
    let pid = process::id();
+
    let name = dst.file_name().and_then(|s| s.to_str()).unwrap_or("tmp");
+
    let tmp = format!(".{name}.{tag}.{pid}.{attempt}");
+
    dst.with_file_name(tmp)
+
}
+

+
fn replace_file(dst: &Path, tmp: &Path) -> io::Result<()> {
+
    let mut backup = None;
+

+
    if dst.exists() {
+
        for i in 0..64 {
+
            let b = unique_sibling(dst, "old", i);
+
            if !b.exists() {
+
                fs::rename(dst, &b)?;
+
                backup = Some(b);
+
                break;
+
            }
+
        }
+
    }
+

+
    match fs::rename(tmp, dst) {
+
        Ok(()) => {
+
            best_effort_sync_parent_dir(dst);
+
            if let Some(b) = backup {
+
                let _ = fs::remove_file(b);
+
            }
+
            Ok(())
+
        }
+
        Err(e) => {
+
            if let Some(b) = backup {
+
                let _ = fs::rename(&b, dst);
+
            }
+
            let _ = fs::remove_file(tmp);
+
            Err(e)
+
        }
+
    }
+
}
+

+
fn atomic_write_text(dst: &Path, content: &str) -> Result<()> {
+
    let parent = dst.parent().ok_or(ProfileError::InvalidState("missing parent dir"))?;
+
    fs::create_dir_all(parent)?;
+

+
    let mut tmp_path = None;
+
    let mut tmp_file = None;
+

+
    for i in 0..64 {
+
        let p = unique_sibling(dst, "new", i);
+
        match fs::OpenOptions::new().create_new(true).write(true).open(&p) {
+
            Ok(f) => {
+
                tmp_path = Some(p);
+
                tmp_file = Some(f);
+
                break;
+
            }
+
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
+
            Err(e) => return Err(ProfileError::Io(e)),
+
        }
+
    }
+

+
    let Some(tmp) = tmp_path else { return Err(ProfileError::InvalidState("tmp path exhausted")); };
+
    let mut f = tmp_file.ok_or(ProfileError::InvalidState("tmp file missing"))?;
+

+
    use io::Write as _;
+
    f.write_all(content.as_bytes())?;
+
    f.sync_all()?;
+

+
    replace_file(dst, &tmp)?;
+
    Ok(())
+
}
+

+
fn atomic_copy_file(src: &Path, dst: &Path) -> Result<()> {
+
    let parent = dst.parent().ok_or(ProfileError::InvalidState("missing parent dir"))?;
+
    fs::create_dir_all(parent)?;
+

+
    let mut tmp = None;
+

+
    for i in 0..64 {
+
        let p = unique_sibling(dst, "copy", i);
+
        match fs::OpenOptions::new().create_new(true).write(true).open(&p) {
+
            Ok(mut out_f) => {
+
                let mut in_f = fs::File::open(src)?;
+
                io::copy(&mut in_f, &mut out_f)?;
+
                if let Ok(meta) = fs::metadata(src) {
+
                    let _ = fs::set_permissions(&p, meta.permissions());
+
                }
+
                out_f.sync_all()?;
+
                tmp = Some(p);
+
                break;
+
            }
+
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
+
            Err(e) => return Err(ProfileError::Io(e)),
+
        }
+
    }
+

+
    let Some(tmp) = tmp else { return Err(ProfileError::InvalidState("tmp path exhausted")); };
+
    replace_file(dst, &tmp)?;
+
    Ok(())
+
}
+

+
fn atomic_copy_dir_replace(src_dir: &Path, dst_dir: &Path) -> Result<()> {
+
    let parent = dst_dir
+
    .parent()
+
    .ok_or(ProfileError::InvalidState("missing parent dir"))?;
+
    fs::create_dir_all(parent)?;
+

+
    let mut staging = None;
+
    for i in 0..64 {
+
        let s = unique_sibling(dst_dir, "staging", i);
+
        if !s.exists() {
+
            fs::create_dir_all(&s)?;
+
            staging = Some(s);
+
            break;
+
        }
+
    }
+

+
    let Some(staging) = staging else { return Err(ProfileError::InvalidState("staging exhausted")); };
+
    copy_dir_recursive(src_dir, &staging)?;
+

+
    let mut backup = None;
+
    if dst_dir.exists() {
+
        for i in 0..64 {
+
            let b = unique_sibling(dst_dir, "old", i);
+
            if !b.exists() {
+
                fs::rename(dst_dir, &b)?;
+
                backup = Some(b);
+
                break;
+
            }
+
        }
+
    }
+

+
    match fs::rename(&staging, dst_dir) {
+
        Ok(()) => {
+
            best_effort_sync_parent_dir(dst_dir);
+
            if let Some(b) = backup {
+
                let _ = fs::remove_dir_all(&b);
+
            }
+
            Ok(())
+
        }
+
        Err(e) => {
+
            if let Some(b) = backup {
+
                let _ = fs::rename(&b, dst_dir);
+
            }
+
            let _ = fs::remove_dir_all(&staging);
+
            Err(ProfileError::Io(e))
+
        }
+
    }
+
}
+

+
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    fs::create_dir_all(dst)?;
    for entry in fs::read_dir(src)? {
        let e = entry?;
        let ty = e.file_type()?;
+
        let from = e.path();
        let to = dst.join(e.file_name());
+

        if ty.is_dir() {
-
            copy_dir(&e.path(), &to)?;
+
            copy_dir_recursive(&from, &to)?;
+
        } else if ty.is_file() {
+
            fs::copy(&from, &to)?;
+
            if let Ok(meta) = fs::metadata(&from) {
+
                let _ = fs::set_permissions(&to, meta.permissions());
+
            }
+
        } else if ty.is_symlink() {
+
            return Err(ProfileError::UnsupportedFileType(from));
        } else {
-
            fs::copy(e.path(), &to)?;
+
            return Err(ProfileError::UnsupportedFileType(from));
        }
    }
    Ok(())
-
}

\ No newline at end of file
+
}
added crates/radicle-cli/src/commands/profile/args.rs
@@ -0,0 +1,45 @@
+
use clap::{Args as ClapArgs, Subcommand};
+

+
#[derive(Debug, ClapArgs)]
+
#[command(
+
about = "Manage Radicle CLI profiles (config.json and keys/* per user)",
+
          long_about = r#"Manage Radicle CLI profiles (config.json and keys/* per user)
+

+
Notes
+
• 'default' is the pseudo-profile representing the root (~/.radicle).
+
'rad profile switch default' persists the current root into the active profile,
+
clears the root, and unsets the active profile marker.
+
"#,
+
arg_required_else_help = true
+
)]
+
pub struct Args {
+
    #[command(subcommand)]
+
    pub op: Op,
+
}
+

+
#[derive(Debug, Subcommand)]
+
pub enum Op {
+
    New {
+
        name: String,
+
        #[arg(long)]
+
        from: Option<String>,
+
        #[arg(long, short = 'f')]
+
        force: bool,
+
    },
+

+
    Switch {
+
        name: String,
+
        #[arg(long)]
+
        print_env: bool,
+
    },
+

+
    List,
+

+
    Current,
+

+
    Remove {
+
        name: String,
+
        #[arg(long, short = 'y')]
+
        yes: bool,
+
    },
+
}
modified crates/radicle-cli/src/main.rs
@@ -1,18 +1,35 @@
use std::ffi::OsString;
-
use std::io::{self, Write};
-
use std::{io::ErrorKind, iter, process};
+
use std::fmt::Display;
+
use std::io;
+
use std::io::Write;
+
use std::{io::ErrorKind, process};

use anyhow::anyhow;
+
use clap::builder::styling::AnsiColor;
+
use clap::builder::Styles;
+
use clap::{CommandFactory as _, Parser, Subcommand};

use radicle::version::Version;
use radicle_cli::commands::*;
use radicle_cli::terminal as term;

pub const NAME: &str = "rad";
+
pub const GIT_HEAD: &str = env!("GIT_HEAD");
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION");
+
pub const RADICLE_VERSION_LONG: &str =
+
    concat!(env!("RADICLE_VERSION"), " (", env!("GIT_HEAD"), ")");
pub const DESCRIPTION: &str = "Radicle command line interface";
-
pub const GIT_HEAD: &str = env!("GIT_HEAD");
+
pub const LONG_DESCRIPTION: &str = "
+
Radicle is a sovereign code forge built on Git.
+

+
See `rad <COMMAND> --help` to learn about a specific command.
+

+
Do you have feedback?
+
 - Chat <\x1b]8;;https://radicle.zulipchat.com\x1b\\radicle.zulipchat.com\x1b]8;;\x1b\\>
+
 - Mail <\x1b]8;;mailto:feedback@radicle.xyz\x1b\\feedback@radicle.xyz\x1b]8;;\x1b\\>
+
   (Messages are automatically posted to the public #feedback channel on Zulip.)\
+
";
pub const TIMESTAMP: &str = env!("SOURCE_DATE_EPOCH");
pub const VERSION: Version = Version {
    name: NAME,
@@ -20,12 +37,78 @@ pub const VERSION: Version = Version {
    commit: GIT_HEAD,
    timestamp: TIMESTAMP,
};
+
const STYLES: Styles = Styles::styled()
+
    .header(AnsiColor::Magenta.on_default().bold())
+
    .usage(AnsiColor::Magenta.on_default().bold())
+
    .placeholder(AnsiColor::Cyan.on_default());

-
#[derive(Debug)]
+
/// Radicle command line interface
+
#[derive(Parser, Debug)]
+
#[command(name = NAME)]
+
#[command(version = RADICLE_VERSION)]
+
#[command(long_version = RADICLE_VERSION_LONG)]
+
#[command(about = DESCRIPTION)]
+
#[command(long_about = LONG_DESCRIPTION)]
+
#[command(propagate_version = true)]
+
#[command(styles = STYLES)]
+
struct CliArgs {
+
    #[command(subcommand)]
+
    pub command: Command,
+
}
+

+
#[derive(Subcommand, Debug)]
enum Command {
-
    Other(Vec<OsString>),
-
    Help,
-
    Version { json: bool },
+
    Auth(auth::Args),
+
    Block(block::Args),
+
    Checkout(checkout::Args),
+
    Clean(clean::Args),
+
    Clone(clone::Args),
+
    #[command(hide = true)]
+
    Cob(cob::Args),
+
    Config(config::Args),
+
    Debug(debug::Args),
+
    Follow(follow::Args),
+
    Fork(fork::Args),
+
    Id(id::Args),
+
    Inbox(inbox::Args),
+
    Init(init::Args),
+
    #[command(alias = ".")]
+
    Inspect(inspect::Args),
+
    Issue(issue::Args),
+
    Ls(ls::Args),
+
    Node(node::Args),
+
    Patch(patch::Args),
+
    Path(path::Args),
+
    Publish(publish::Args),
+
    Remote(remote::Args),
+
    Seed(seed::Args),
+
    #[command(name = "self")]
+
    RadSelf(rad_self::Args),
+
    Stats(stats::Args),
+
    Sync(sync::Args),
+
    Unblock(unblock::Args),
+
    Unfollow(unfollow::Args),
+
    Unseed(unseed::Args),
+
    Watch(watch::Args),
+
    Profile(rad_profile::Args),
+

+

+
    /// Print the version information of the CLI
+
    Version {
+
        /// Print the version information in JSON format
+
        #[arg(long)]
+
        json: bool,
+
    },
+

+
    /// Print static completion information for a given shell
+
    #[command(hide = true)]
+
    Completion {
+
        /// The type of shell to output a static completion script for.
+
        shell: clap_complete::Shell,
+
    },
+

+
    #[command(external_subcommand)]
+
    External(Vec<OsString>),
}

fn main() {
@@ -44,326 +127,139 @@ fn main() {
    if let Err(e) = radicle::io::set_file_limit(4096) {
        log::warn!(target: "cli", "Unable to set open file limit: {e}");
    }
-
    match parse_args().map_err(Some).and_then(run) {
-
        Ok(_) => process::exit(0),
+
    let CliArgs { command } = CliArgs::parse();
+
    run(command, term::DefaultContext)
+
}
+

+
fn write_version(as_json: bool) -> anyhow::Result<()> {
+
    let mut stdout = io::stdout();
+
    if as_json {
+
        VERSION.write_json(&mut stdout)?;
+
        writeln!(&mut stdout)?;
+
        Ok(())
+
    } else {
+
        VERSION.write(&mut stdout)?;
+
        Ok(())
+
    }
+
}
+

+
fn run(command: Command, ctx: impl term::Context) -> ! {
+
    match run_command(command, ctx) {
+
        Ok(()) => process::exit(0),
        Err(err) => {
-
            if let Some(err) = err {
-
                term::error(format!("rad: {err}"));
-
            }
+
            term::fail(&err);
            process::exit(1);
        }
    }
}

-
fn parse_args() -> anyhow::Result<Command> {
-
    use lexopt::prelude::*;
-

-
    let mut parser = lexopt::Parser::from_env();
-
    let mut command = None;
-
    let mut json = false;
-

-
    while let Some(arg) = parser.next()? {
-
        match arg {
-
            Long("json") => {
-
                json = true;
-
            }
-
            Long("help") | Short('h') => {
-
                command = Some(Command::Help);
-
            }
-
            Long("version") => {
-
                command = Some(Command::Version { json: false });
-
            }
-
            Value(val) if command.is_none() => {
-
                if val == *"." {
-
                    command = Some(Command::Other(vec![OsString::from("inspect")]));
-
                } else if val == "version" {
-
                    command = Some(Command::Version { json: false });
-
                } else {
-
                    let args = iter::once(val)
-
                        .chain(iter::from_fn(|| parser.value().ok()))
-
                        .collect();
-

-
                    command = Some(Command::Other(args))
-
                }
-
            }
-
            _ => anyhow::bail!(arg.unexpected()),
-
        }
-
    }
-
    if let Some(Command::Version { json: j }) = &mut command {
-
        *j = json;
+
fn run_command(command: Command, ctx: impl term::Context) -> Result<(), anyhow::Error> {
+
    match command {
+
        Command::Auth(args) => auth::run(args, ctx),
+
        Command::Block(args) => block::run(args, ctx),
+
        Command::Checkout(args) => checkout::run(args, ctx),
+
        Command::Clean(args) => clean::run(args, ctx),
+
        Command::Clone(args) => clone::run(args, ctx),
+
        Command::Cob(args) => cob::run(args, ctx),
+
        Command::Config(args) => config::run(args, ctx),
+
        Command::Debug(args) => debug::run(args, ctx),
+
        Command::Follow(args) => follow::run(args, ctx),
+
        Command::Fork(args) => fork::run(args, ctx),
+
        Command::Id(args) => id::run(args, ctx),
+
        Command::Inbox(args) => inbox::run(args, ctx),
+
        Command::Init(args) => init::run(args, ctx),
+
        Command::Inspect(args) => inspect::run(args, ctx),
+
        Command::Issue(args) => issue::run(args, ctx),
+
        Command::Ls(args) => ls::run(args, ctx),
+
        Command::Node(args) => node::run(args, ctx),
+
        Command::Patch(args) => patch::run(args, ctx),
+
        Command::Path(args) => path::run(args, ctx),
+
        Command::Publish(args) => publish::run(args, ctx),
+
        Command::Remote(args) => remote::run(args, ctx),
+
        Command::Seed(args) => seed::run(args, ctx),
+
        Command::RadSelf(args) => rad_self::run(args, ctx),
+
        Command::Stats(args) => stats::run(args, ctx),
+
        Command::Sync(args) => sync::run(args, ctx),
+
        Command::Unblock(args) => unblock::run(args, ctx),
+
        Command::Unfollow(args) => unfollow::run(args, ctx),
+
        Command::Unseed(args) => unseed::run(args, ctx),
+
        Command::Watch(args) => watch::run(args, ctx),
+
        Command::Profile(args) => rad_profile::run(args, ctx),
+
        Command::Version { json } => write_version(json),
+
        Command::Completion { shell } => {
+
            print_completion(shell, &mut CliArgs::command());
+
            Ok(())
+
        }
+
        Command::External(args) => ExternalCommand::new(args).run(),
    }
-
    Ok(command.unwrap_or_else(|| Command::Other(vec![])))
}

-
fn print_help() -> anyhow::Result<()> {
-
    VERSION.write(&mut io::stdout())?;
-
    println!("{DESCRIPTION}");
-
    println!();
+
fn print_completion<G: clap_complete::Generator>(generator: G, cmd: &mut clap::Command) {
+
    clap_complete::generate(
+
        generator,
+
        cmd,
+
        cmd.get_name().to_string(),
+
        &mut io::stdout(),
+
    );
+
}

-
    rad_help::run(Default::default(), term::DefaultContext)
+
struct ExternalCommand {
+
    command: OsString,
+
    args: Vec<OsString>,
}

-
fn run(command: Command) -> Result<(), Option<anyhow::Error>> {
-
    match command {
-
        Command::Version { json } => {
-
            let mut stdout = io::stdout();
-
            if json {
-
                VERSION
-
                    .write_json(&mut stdout)
-
                    .map_err(|e| Some(e.into()))?;
-
                writeln!(&mut stdout).ok();
-
            } else {
-
                VERSION.write(&mut stdout).map_err(|e| Some(e.into()))?;
-
            }
-
        }
-
        Command::Help => {
-
            print_help()?;
-
        }
-
        Command::Other(args) => {
-
            let exe = args.first();
+
impl ExternalCommand {
+
    fn new(mut args: Vec<OsString>) -> Self {
+
        let command = args.remove(0);
+
        Self { command, args }
+
    }

-
            if let Some(Some(exe)) = exe.map(|s| s.to_str()) {
-
                run_other(exe, &args[1..])?;
-
            } else {
-
                print_help()?;
-
            }
-
        }
+
    fn is_diff(&self) -> bool {
+
        self.command == "diff"
    }

-
    Ok(())
-
}
+
    fn exe(&self) -> OsString {
+
        let mut exe = OsString::from(NAME);
+
        exe.push("-");
+
        exe.push(self.command.clone());
+
        exe
+
    }

-
fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>> {
-
    match exe {
-
        "auth" => {
-
            term::run_command_args::<rad_auth::Options, _>(
-
                rad_auth::HELP,
-
                rad_auth::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "block" => {
-
            term::run_command_args::<rad_block::Options, _>(
-
                rad_block::HELP,
-
                rad_block::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "checkout" => {
-
            term::run_command_args::<rad_checkout::Options, _>(
-
                rad_checkout::HELP,
-
                rad_checkout::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "clone" => {
-
            term::run_command_args::<rad_clone::Options, _>(
-
                rad_clone::HELP,
-
                rad_clone::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "cob" => {
-
            term::run_command_args::<rad_cob::Options, _>(
-
                rad_cob::HELP,
-
                rad_cob::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "config" => {
-
            term::run_command_args::<rad_config::Options, _>(
-
                rad_config::HELP,
-
                rad_config::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "diff" => {
-
            term::run_command_args::<rad_diff::Options, _>(
-
                rad_diff::HELP,
-
                rad_diff::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "debug" => {
-
            term::run_command_args::<rad_debug::Options, _>(
-
                rad_debug::HELP,
-
                rad_debug::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "follow" => {
-
            term::run_command_args::<rad_follow::Options, _>(
-
                rad_follow::HELP,
-
                rad_follow::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "fork" => {
-
            term::run_command_args::<rad_fork::Options, _>(
-
                rad_fork::HELP,
-
                rad_fork::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "help" => {
-
            term::run_command_args::<rad_help::Options, _>(
-
                rad_help::HELP,
-
                rad_help::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "id" => {
-
            term::run_command_args::<rad_id::Options, _>(rad_id::HELP, rad_id::run, args.to_vec());
-
        }
-
        "inbox" => term::run_command_args::<rad_inbox::Options, _>(
-
            rad_inbox::HELP,
-
            rad_inbox::run,
-
            args.to_vec(),
-
        ),
-
        "init" => {
-
            term::run_command_args::<rad_init::Options, _>(
-
                rad_init::HELP,
-
                rad_init::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "inspect" => {
-
            term::run_command_args::<rad_inspect::Options, _>(
-
                rad_inspect::HELP,
-
                rad_inspect::run,
-
                args.to_vec(),
-
            );
+
    fn display_exe(&self) -> impl Display {
+
        match self.exe().into_string() {
+
            Ok(exe) => exe,
+
            Err(exe) => format!("{exe:?}"),
        }
-
        "issue" => {
-
            term::run_command_args::<rad_issue::Options, _>(
-
                rad_issue::HELP,
-
                rad_issue::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "ls" => {
-
            term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
-
        }
-
        "node" => {
-
            term::run_command_args::<rad_node::Options, _>(
-
                rad_node::HELP,
-
                rad_node::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "patch" => {
-
            term::run_command_args::<rad_patch::Options, _>(
-
                rad_patch::HELP,
-
                rad_patch::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "path" => {
-
            term::run_command_args::<rad_path::Options, _>(
-
                rad_path::HELP,
-
                rad_path::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "publish" => {
-
            term::run_command_args::<rad_publish::Options, _>(
-
                rad_publish::HELP,
-
                rad_publish::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "clean" => {
-
            term::run_command_args::<rad_clean::Options, _>(
-
                rad_clean::HELP,
-
                rad_clean::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "self" => {
-
            term::run_command_args::<rad_self::Options, _>(
-
                rad_self::HELP,
-
                rad_self::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "sync" => {
-
            term::run_command_args::<rad_sync::Options, _>(
-
                rad_sync::HELP,
-
                rad_sync::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "seed" => {
-
            term::run_command_args::<rad_seed::Options, _>(
-
                rad_seed::HELP,
-
                rad_seed::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "unblock" => {
-
            term::run_command_args::<rad_unblock::Options, _>(
-
                rad_unblock::HELP,
-
                rad_unblock::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "unfollow" => {
-
            term::run_command_args::<rad_unfollow::Options, _>(
-
                rad_unfollow::HELP,
-
                rad_unfollow::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "unseed" => {
-
            term::run_command_args::<rad_unseed::Options, _>(
-
                rad_unseed::HELP,
-
                rad_unseed::run,
-
                args.to_vec(),
-
            );
-
        }
-
        "remote" => term::run_command_args::<rad_remote::Options, _>(
-
            rad_remote::HELP,
-
            rad_remote::run,
-
            args.to_vec(),
-
        ),
-
        "stats" => term::run_command_args::<rad_stats::Options, _>(
-
            rad_stats::HELP,
-
            rad_stats::run,
-
            args.to_vec(),
-
        ),
-
        "profile" => {
-
            term::run_command_args::<rad_profile::Options, _>(
-
                rad_profile::HELP,
-
                rad_profile::run,
-
                args.to_vec(),
-
            );
+
    }
+

+
    fn run(self) -> anyhow::Result<()> {
+
        // This command is deprecated and delegates to `git diff`.
+
        // Even before it was deprecated, it was not printed by
+
        // `rad -h`.
+
        //
+
        // Since it is external, `--help` will delegate to `git diff --help`.
+
        if self.is_diff() {
+
            return diff::run(self.args);
        }
-
        "watch" => term::run_command_args::<rad_watch::Options, _>(
-
            rad_watch::HELP,
-
            rad_watch::run,
-
            args.to_vec(),
-
        ),
-
        other => {
-
            let exe = format!("{NAME}-{exe}");
-
            let status = process::Command::new(exe).args(args).status();

-
            match status {
-
                Ok(status) => {
-
                    if !status.success() {
-
                        return Err(None);
-
                    }
+
        let status = process::Command::new(self.exe()).args(&self.args).status();
+
        match status {
+
            Ok(status) => {
+
                if !status.success() {
+
                    return Err(anyhow!("`{}` exited with an error.", self.display_exe()));
                }
-
                Err(err) => {
-
                    if let ErrorKind::NotFound = err.kind() {
-
                        return Err(Some(anyhow!(
-
                            "`{other}` is not a command. See `rad --help` for a list of commands.",
-
                        )));
-
                    } else {
-
                        return Err(Some(err.into()));
-
                    }
+
                Ok(())
+
            }
+
            Err(err) => {
+
                if let ErrorKind::NotFound = err.kind() {
+
                    Err(anyhow!(
+
                        "`{}` is not a known command. See `rad --help` for a list of commands.",
+
                        self.display_exe(),
+
                    ))
+
                } else {
+
                    Err(err.into())
                }
            }
        }
    }
-
    Ok(())
}