use clap::{Parser, Subcommand};
const HELP_TEMPLATE: &str = "\
tsk: work with a clear context
tsk keeps the missions, tasks, decisions and questions of a repo in a ledger that every
session and robot reads and writes. The ledger is a Dolt database that tsk publishes to
the git remote of the repo. tsk works per repo.
{usage-heading} {usage}
Start:
1. Install tsk: brew install jimbarritt/tap/tsk, or download a release archive.
2. Run tsk doctor to check the environment.
3. In the repo that will use tsk, run tsk install-plugin claude-cli (or copilot-cli).
4. Optional: install the swe plugin, which checks Software English, in the same step:
tsk install-plugin claude-cli --plugin tsk --plugin swe
This installs at project scope, for this repo only. claude plugin install on its own
defaults to user scope, which applies to every repo on the machine.
Set up and maintain:
install-plugin Install and update the tsk plugin through a coding agent's CLI
doctor Check the environment and print the fix for each fault
self-update Upgrade tsk to the latest release
config Read and write the user config and the repo config
nexus Attach the nexus and list the repos it indexes
Daily work:
mission Create, change, show and list missions
task Add, change, block and list the tasks of a mission
decision Record and list the decisions of a mission
question Ask, answer and list the questions of a mission
thread Start, pause, resume and stop the thread this session works on
actor Register this session as an actor and list the actors
Ledger:
ledger Fetch, push and inspect the ledger; read and write its documents
events Append to and read the external event queue
Guides:
guide Print guides for agents, such as importing an existing plan
Run tsk <command> --help for the detail of a command. The documents are in docs/index.md
in https://github.com/jimbarritt/tsk.
{options}
";
#[derive(Parser)]
#[command(
name = "tsk",
about = "tsk: work with a clear context",
version = env!("CARGO_PKG_VERSION"),
arg_required_else_help = true,
help_template = HELP_TEMPLATE
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
#[command(
about = "Start, pause, resume, detach, stop and list ledger threads, and resolve this session's or worktree's binding"
)]
Thread {
#[command(subcommand)]
action: thread::ThreadCommands,
},
#[command(
about = "Register this session or worktree as an actor (a human or a robot, by name), print its actor, and list the actors"
)]
Actor {
#[command(subcommand)]
action: actor::ActorCommands,
},
#[command(about = "Fetch, locate and push the ledger branch")]
Ledger {
#[command(subcommand)]
action: ledger::LedgerCommands,
},
#[command(
about = "Create, change and list mission records in the local ledger store; tsk ledger push publishes"
)]
Mission {
#[command(subcommand)]
action: mission::MissionCommands,
},
#[command(
about = "Add, change, block and list the tasks of a mission in the local ledger store; tsk ledger push publishes"
)]
Task {
#[command(subcommand)]
action: mission::records::TaskCommands,
},
#[command(
about = "Record and list the decisions of a mission in the local ledger store; tsk ledger push publishes"
)]
Decision {
#[command(subcommand)]
action: mission::records::DecisionCommands,
},
#[command(
about = "Ask, answer and list the questions of a mission in the local ledger store; tsk ledger push publishes"
)]
Question {
#[command(subcommand)]
action: mission::records::QuestionCommands,
},
#[command(
about = "Read and write the user config (attach the nexus) and the repo config (choose the ledger refs)"
)]
Config {
#[command(subcommand)]
action: config::ConfigCommands,
},
#[command(about = "Append to, read and advance the ledger's external event queue")]
Events {
#[command(subcommand)]
action: events::EventsCommands,
},
#[command(about = "Attach the nexus and list the territories and repos it indexes")]
Nexus {
#[command(subcommand)]
action: nexus_cmd::NexusCommands,
},
#[command(
about = "Add the plugin marketplace, then install and update the tsk plugin through a coding agent's CLI"
)]
InstallPlugin(install_plugin::InstallPluginArgs),
#[command(
about = "Check the environment and print what is wrong, with the fix for each: the tsk files on PATH, dolt, jq, git and the origin remote, the ledger refs, the plugin version, the Xcode licence on macOS and the Dolt server. Exit 0 when every check passes, 1 when any fails"
)]
Doctor,
#[command(
name = "self-update",
about = "Upgrade tsk to the latest release, by the method it was installed with (Homebrew, Cargo or a release binary)",
long_about = "Upgrade tsk to the latest release, by the method it was installed with (Homebrew, Cargo or a release binary).\n\nA command that stops with `unsupported ledger version` because the ledger is newer than this tsk runs this update itself, then runs the command again. `tsk thread session-start` and `tsk thread guard` never do. TSK_AUTO_UPDATE=never or TSK_NO_UPDATE_CHECK=1 switches it off."
)]
SelfUpdate(self_update::SelfUpdateArgs),
#[command(about = "Print guides for agents, such as importing an existing plan")]
Guide {
#[command(subcommand)]
action: guide::GuideCommands,
},
}
#[cfg(unix)]
fn restore_sigpipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn restore_sigpipe() {}
fn main() {
restore_sigpipe();
let cli = Cli::parse();
let hook = is_hook_command(&cli);
match run_cli(cli) {
Ok(0) => {}
Ok(code) => std::process::exit(code),
Err(e) => {
let extra = auto_update::handle(&e, hook);
eprintln!("{}", e);
if let Some(line) = extra {
eprintln!("{}", line);
}
std::process::exit(1);
}
}
}
fn is_hook_command(cli: &Cli) -> bool {
matches!(
cli.command,
Some(Commands::Thread {
action: thread::ThreadCommands::SessionStart { .. } | thread::ThreadCommands::Guard
}) | Some(Commands::Ledger {
action: ledger::LedgerCommands::BackgroundPushRun { .. }
| ledger::LedgerCommands::Push {
background: true,
..
}
})
)
}
fn run_cli(cli: Cli) -> Result<i32, String> {
match cli.command {
Some(Commands::Thread { action }) => thread::run(action),
Some(Commands::Actor { action }) => actor::run(action),
Some(Commands::Ledger { action }) => ledger::run(action).map(|()| 0),
Some(Commands::Events { action }) => events::run(action),
Some(Commands::Mission { action }) => mission::run(action).map(|()| 0),
Some(Commands::Task { action }) => mission::records::run_task(action).map(|()| 0),
Some(Commands::Decision { action }) => mission::records::run_decision(action).map(|()| 0),
Some(Commands::Question { action }) => mission::records::run_question(action).map(|()| 0),
Some(Commands::Config { action }) => config::run(action).map(|()| 0),
Some(Commands::Nexus { action }) => nexus_cmd::run(action).map(|()| 0),
Some(Commands::InstallPlugin(args)) => install_plugin::run(args).map(|()| 0),
Some(Commands::Doctor) => doctor::run(),
Some(Commands::SelfUpdate(args)) => self_update::run(args).map(|()| {
if let Some(note) = doctor::shadow_note(&doctor::tsk_on_path(std::env::var_os("PATH")))
{
eprintln!("{}", note);
}
0
}),
Some(Commands::Guide { action }) => guide::run(action).map(|()| 0),
None => Ok(0),
}
}
mod config;
mod doctor;
mod gitenv;
mod ledger;
mod mission;
mod thread;
mod actor;
mod events;
mod install_plugin;
mod guide;
mod self_update;
mod auto_update;
mod nexus_cmd;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn actor_subcommands_parse() {
assert!(matches!(
Cli::try_parse_from(["tsk", "actor", "register", "alice", "--kind", "robot"])
.unwrap()
.command,
Some(Commands::Actor {
action: actor::ActorCommands::Register { name, kind }
}) if name == "alice" && kind == "robot"
));
assert!(Cli::try_parse_from(["tsk", "actor", "register", "alice"]).is_err());
assert!(
Cli::try_parse_from(["tsk", "actor", "register", "alice", "--kind", "cat"]).is_err()
);
assert!(matches!(
Cli::try_parse_from(["tsk", "actor", "name"])
.unwrap()
.command,
Some(Commands::Actor {
action: actor::ActorCommands::Name
})
));
assert!(Cli::try_parse_from(["tsk", "actor", "name", "alice"]).is_err());
assert!(matches!(
Cli::try_parse_from(["tsk", "actor", "list", "--json"])
.unwrap()
.command,
Some(Commands::Actor {
action: actor::ActorCommands::List {
json: true,
porcelain: false
}
})
));
assert!(Cli::try_parse_from(["tsk", "actor"]).is_err());
}
#[test]
fn thread_subcommands_parse() {
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "start", "M-X"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Start { mission_id, briefing_path: None } })
if mission_id.as_deref() == Some("M-X")
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "start"])
.unwrap()
.command,
Some(Commands::Thread {
action: thread::ThreadCommands::Start {
mission_id: None,
briefing_path: None
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "set", "mission", "M-X"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Set { field: thread::SetCommands::Mission { mission_id, thread_id: None, none: false } } })
if mission_id.as_deref() == Some("M-X")
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "set", "mission", "M-X", "1234abcd"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Set { field: thread::SetCommands::Mission { mission_id, thread_id: Some(thread), none: false } } })
if mission_id.as_deref() == Some("M-X") && thread == "1234abcd"
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "set", "mission", "--none"])
.unwrap()
.command,
Some(Commands::Thread {
action: thread::ThreadCommands::Set {
field: thread::SetCommands::Mission {
mission_id: None,
thread_id: None,
none: true
}
}
})
));
assert!(Cli::try_parse_from(["tsk", "thread", "set", "mission"]).is_err());
assert!(Cli::try_parse_from(["tsk", "thread", "set"]).is_err());
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "start", "M-X", "missions/M-X.md"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Start { mission_id, briefing_path: Some(path) } })
if mission_id.as_deref() == Some("M-X") && path == "missions/M-X.md"
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "pause", "1234abcd", "", "-next: tests"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Pause { task_id, whats_next, legacy_whats_next: None, .. } })
if task_id.is_empty() && whats_next.as_deref() == Some("-next: tests")
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "pause", "1234abcd", "m.md", "T-01", "-next: tests"]).unwrap().command,
Some(Commands::Thread { action: thread::ThreadCommands::Pause { task_id, whats_next, legacy_whats_next: Some(last), .. } })
if task_id == "m.md" && whats_next.as_deref() == Some("T-01") && last == "-next: tests"
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "skip"])
.unwrap()
.command,
Some(Commands::Thread {
action: thread::ThreadCommands::Skip
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "stop"])
.unwrap()
.command,
Some(Commands::Thread {
action: thread::ThreadCommands::Stop { thread_id: None }
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "thread", "binding", "--no-fetch"])
.unwrap()
.command,
Some(Commands::Thread {
action: thread::ThreadCommands::Binding { no_fetch: true }
})
));
for args in [
vec!["tsk", "thread", "resume", "1234abcd"],
vec!["tsk", "thread", "detach"],
vec!["tsk", "thread", "list"],
vec!["tsk", "thread", "guard"],
vec!["tsk", "thread", "prune-bindings"],
vec!["tsk", "thread", "prune-bindings", "--dry-run"],
vec!["tsk", "thread", "prune-bindings", "--older-than", "7"],
vec!["tsk", "thread", "session-start"],
vec!["tsk", "thread", "session-start", "--format", "copilot"],
] {
assert!(Cli::try_parse_from(&args).is_ok(), "{:?}", args);
}
assert!(
Cli::try_parse_from(["tsk", "thread", "session-start", "--format", "gemini"]).is_err()
);
assert!(Cli::try_parse_from(["tsk", "thread", "prune-bindings", "--older-than"]).is_err());
assert!(
Cli::try_parse_from(["tsk", "thread", "prune-bindings", "--older-than", "x"]).is_err()
);
assert!(Cli::try_parse_from(["tsk", "thread", "pause", "1234abcd"]).is_err());
assert!(Cli::try_parse_from(["tsk", "thread", "resume"]).is_err());
}
#[test]
fn removed_daemon_commands_do_not_parse() {
assert!(Cli::try_parse_from(["tsk", "task", "list"]).is_err());
assert!(Cli::try_parse_from(["tsk", "context"]).is_err());
assert!(Cli::try_parse_from(["tsk", "where"]).is_err());
assert!(Cli::try_parse_from(["tsk", "thread", "create", "a", "PRIO", "d"]).is_err());
assert!(Cli::try_parse_from(["tsk", "thread", "switch-to", "1"]).is_err());
}
#[test]
fn ledger_subcommands_parse() {
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "fetch"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Fetch
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "path"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Path
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "push", "Record a change"]).unwrap().command,
Some(Commands::Ledger { action: ledger::LedgerCommands::Push { message, background: false } }) if message.as_deref() == Some("Record a change")
));
assert!(Cli::try_parse_from(["tsk", "ledger", "push"]).is_err());
assert!(Cli::try_parse_from(["tsk", "ledger"]).is_err());
assert!(Cli::try_parse_from(["tsk", "ledger", "fetch", "extra"]).is_err());
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "status"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Status {
porcelain: false,
local: false
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "discard"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Discard {
yes: false,
porcelain: false
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "discard", "--yes"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Discard {
yes: true,
porcelain: false
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "status", "--porcelain"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Status {
porcelain: true,
local: false
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "ledger", "status", "--local"])
.unwrap()
.command,
Some(Commands::Ledger {
action: ledger::LedgerCommands::Status {
porcelain: false,
local: true
}
})
));
assert!(Cli::try_parse_from(["tsk", "ledger", "discard", "--porcelain"]).is_ok());
assert!(Cli::try_parse_from(["tsk", "ledger", "status", "extra"]).is_err());
}
#[test]
fn mission_subcommands_parse() {
for args in [
vec!["tsk", "mission", "create", "M-X", "A name"],
vec![
"tsk",
"mission",
"create",
"M-X",
"A name",
"--category",
"administrative",
"--parent",
"M-Y",
],
vec!["tsk", "mission", "set", "M-X", "purpose", "-"],
vec!["tsk", "mission", "block", "M-X", "M-Y"],
vec!["tsk", "mission", "unblock", "M-X", "M-Y"],
vec!["tsk", "mission", "objective", "add", "M-X", "-dash outcome"],
vec!["tsk", "mission", "objective", "done", "M-X", "1"],
vec!["tsk", "mission", "objective", "undone", "M-X", "1"],
vec!["tsk", "mission", "objective", "remove", "M-X", "1"],
vec!["tsk", "mission", "objective", "list", "M-X"],
vec![
"tsk", "mission", "link", "M-X", "a/b.md", "--role", "report",
],
vec!["tsk", "mission", "unlink", "M-X", "a/b.md"],
vec!["tsk", "mission", "list"],
vec!["tsk", "mission", "list", "M-X", "--json"],
] {
assert!(Cli::try_parse_from(&args).is_ok(), "{:?}", args);
}
assert!(Cli::try_parse_from(["tsk", "mission", "show", "M-X"]).is_ok());
assert!(Cli::try_parse_from(["tsk", "mission", "show"]).is_err());
assert!(Cli::try_parse_from(["tsk", "mission", "link", "M-X", "a.md"]).is_err());
}
#[test]
fn task_decision_and_question_subcommands_parse() {
for args in [
vec!["tsk", "task", "add", "M-X", "Do the thing"],
vec!["tsk", "task", "add", "M-X", "-dash description"],
vec!["tsk", "task", "set", "M-X", "T-01", "status", "done"],
vec!["tsk", "task", "block", "M-X", "T-01", "T-02"],
vec!["tsk", "task", "unblock", "M-X", "T-01", "T-02"],
vec!["tsk", "task", "list", "M-X"],
vec!["tsk", "decision", "add", "M-X", "Use Dolt", "--by", "jim"],
vec![
"tsk", "decision", "add", "M-X", "Use Dolt", "--by", "jim", "--task", "T-01",
],
vec![
"tsk",
"decision",
"add",
"M-X",
"Use Dolt",
"--by",
"jim",
"--on",
"2026-02-27",
],
vec!["tsk", "decision", "list", "M-X"],
vec!["tsk", "question", "add", "M-X", "Why?"],
vec![
"tsk",
"question",
"add",
"M-X",
"Why?",
"--on",
"2026-02-27",
],
vec![
"tsk", "question", "answer", "M-X", "Q-01", "Because", "--by", "jim",
],
vec![
"tsk",
"question",
"answer",
"M-X",
"Q-01",
"Because",
"--by",
"jim",
"--on",
"2026-03-01",
],
vec!["tsk", "question", "list"],
vec!["tsk", "question", "list", "M-X"],
vec!["tsk", "mission", "list", "--porcelain"],
vec!["tsk", "mission", "list", "M-X", "--porcelain"],
vec!["tsk", "task", "list", "M-X", "--porcelain"],
vec!["tsk", "decision", "list", "M-X", "--porcelain"],
vec![
"tsk",
"decision",
"list",
"--adr",
"docs/a.md",
"--porcelain",
],
vec!["tsk", "question", "list", "--porcelain"],
] {
assert!(Cli::try_parse_from(&args).is_ok(), "{:?}", args);
}
assert!(Cli::try_parse_from(["tsk", "decision", "add", "M-X", "No who"]).is_err());
assert!(Cli::try_parse_from(["tsk", "task", "show", "M-X", "T-01"]).is_err());
assert!(Cli::try_parse_from(["tsk", "task", "list"]).is_err());
}
#[test]
fn events_subcommands_parse() {
assert!(matches!(
Cli::try_parse_from(["tsk", "events", "append", "github", "dependabot_alert", "created", "o/r", "p.json"]).unwrap().command,
Some(Commands::Events { action: events::EventsCommands::Append { source, event_type, action, repo, payload_file } })
if source == "github" && event_type == "dependabot_alert" && action == "created" && repo == "o/r" && payload_file == std::path::Path::new("p.json")
));
assert!(matches!(
Cli::try_parse_from(["tsk", "events", "append-batch"])
.unwrap()
.command,
Some(Commands::Events {
action: events::EventsCommands::AppendBatch {
source: None,
action: None
}
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "events", "append-batch", "--source", "github", "--action", "polled"]).unwrap().command,
Some(Commands::Events { action: events::EventsCommands::AppendBatch { source: Some(source), action: Some(action) } })
if source == "github" && action == "polled"
));
assert!(Cli::try_parse_from(["tsk", "events", "append-batch", "github"]).is_err());
assert!(matches!(
Cli::try_parse_from(["tsk", "events", "read-new"])
.unwrap()
.command,
Some(Commands::Events {
action: events::EventsCommands::ReadNew
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "events", "advance-watermark", "-1"]).unwrap().command,
Some(Commands::Events { action: events::EventsCommands::AdvanceWatermark { count } }) if count == "-1"
));
assert!(
Cli::try_parse_from(["tsk", "events", "append", "github", "t", "a", "o/r"]).is_err()
);
assert!(Cli::try_parse_from(["tsk", "events", "advance-watermark"]).is_err());
assert!(Cli::try_parse_from(["tsk", "events", "read-new", "extra"]).is_err());
}
#[test]
fn install_plugin_parses_with_defaults_and_options() {
match Cli::try_parse_from(["tsk", "install-plugin", "claude-cli"])
.unwrap()
.command
{
Some(Commands::InstallPlugin(args)) => {
assert_eq!(args.target, install_plugin::Target::ClaudeCli);
assert_eq!(args.scope, None);
assert_eq!(args.plugins, vec!["tsk".to_string()]);
assert_eq!(args.marketplace_source(), "jimbarritt/claude-plugins");
assert_eq!(args.marketplace_name(), "jimbarritt-claude-plugins");
}
_ => panic!("install-plugin did not parse"),
}
match Cli::try_parse_from([
"tsk",
"install-plugin",
"claude-cli",
"--scope",
"local",
"--plugin",
"swe",
"--plugin",
"tsk",
])
.unwrap()
.command
{
Some(Commands::InstallPlugin(args)) => {
assert_eq!(args.scope, Some(install_plugin::Scope::Local));
assert_eq!(args.plugins, vec!["swe".to_string(), "tsk".to_string()]);
}
_ => panic!("install-plugin did not parse"),
}
match Cli::try_parse_from(["tsk", "install-plugin", "copilot-cli"])
.unwrap()
.command
{
Some(Commands::InstallPlugin(args)) => {
assert_eq!(args.target, install_plugin::Target::CopilotCli);
assert_eq!(args.marketplace_source(), "jimbarritt/tsk");
assert_eq!(args.marketplace_name(), "tsk");
}
_ => panic!("install-plugin did not parse"),
}
let missing = match Cli::try_parse_from(["tsk", "install-plugin"]) {
Err(e) => e,
Ok(_) => panic!("install-plugin parsed with no target"),
};
assert_eq!(
missing.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
let text = missing.to_string();
assert!(
text.contains("claude-cli") && text.contains("copilot-cli"),
"{}",
text
);
assert!(text.contains("tsk install-plugin claude-cli"), "{}", text);
assert!(Cli::try_parse_from(["tsk", "install-plugin", "codex"]).is_err());
assert!(
Cli::try_parse_from(["tsk", "install-plugin", "claude-cli", "--scope", "team"])
.is_err()
);
}
#[test]
fn self_update_parses_with_and_without_check() {
assert!(matches!(
Cli::try_parse_from(["tsk", "self-update"]).unwrap().command,
Some(Commands::SelfUpdate(self_update::SelfUpdateArgs {
check: false
}))
));
assert!(matches!(
Cli::try_parse_from(["tsk", "self-update", "--check"])
.unwrap()
.command,
Some(Commands::SelfUpdate(self_update::SelfUpdateArgs {
check: true
}))
));
assert!(Cli::try_parse_from(["tsk", "self-update", "extra"]).is_err());
}
#[test]
fn nexus_subcommands_parse() {
assert!(matches!(
Cli::try_parse_from(["tsk", "nexus", "add", "https://example.test/o/nexus"]).unwrap().command,
Some(Commands::Nexus { action: nexus_cmd::NexusCommands::Add { url } })
if url == "https://example.test/o/nexus"
));
assert!(matches!(
Cli::try_parse_from(["tsk", "nexus", "list"])
.unwrap()
.command,
Some(Commands::Nexus {
action: nexus_cmd::NexusCommands::List { json: false }
})
));
assert!(matches!(
Cli::try_parse_from(["tsk", "nexus", "list", "--json"])
.unwrap()
.command,
Some(Commands::Nexus {
action: nexus_cmd::NexusCommands::List { json: true }
})
));
match Cli::try_parse_from(["tsk", "nexus", "register-repo"])
.unwrap()
.command
{
Some(Commands::Nexus {
action:
nexus_cmd::NexusCommands::RegisterRepo {
id,
territory,
territory_name,
ledger,
ledger_refs,
local,
},
}) => {
assert_eq!(id, None);
assert_eq!(territory, None);
assert_eq!(territory_name, None);
assert_eq!(ledger, nexus_cmd::LedgerChoice::Nexus);
assert_eq!(ledger_refs, None);
assert!(!local);
}
_ => panic!("register-repo did not parse"),
}
assert!(Cli::try_parse_from([
"tsk",
"nexus",
"register-repo",
"--id",
"x",
"--territory",
"work",
"--territory-name",
"Work",
"--ledger",
"repo",
"--local",
])
.is_ok());
assert!(
Cli::try_parse_from(["tsk", "nexus", "register-repo", "--territory-name", "Work"])
.is_err()
);
assert!(
Cli::try_parse_from(["tsk", "nexus", "register-repo", "--ledger", "cloud"]).is_err()
);
assert!(Cli::try_parse_from(["tsk", "nexus", "add"]).is_err());
assert!(Cli::try_parse_from(["tsk", "nexus"]).is_err());
}
#[test]
fn ledger_refs_subcommands_parse() {
for (args, mode) in [
(vec!["tsk", "config", "ledger-refs"], None),
(
vec!["tsk", "config", "ledger-refs", "custom"],
Some("custom"),
),
(
vec!["tsk", "config", "ledger-refs", "branches"],
Some("branches"),
),
] {
match Cli::try_parse_from(args).unwrap().command {
Some(Commands::Config {
action: config::ConfigCommands::LedgerRefs { mode: found },
}) => assert_eq!(found.as_deref(), mode),
_ => panic!("ledger-refs did not parse"),
}
}
assert!(Cli::try_parse_from(["tsk", "config", "ledger-refs", "tags"]).is_err());
match Cli::try_parse_from(["tsk", "config", "ledger-server", "off"])
.unwrap()
.command
{
Some(Commands::Config {
action: config::ConfigCommands::LedgerServer { mode },
}) => assert_eq!(mode.as_deref(), Some("off")),
_ => panic!("ledger-server did not parse"),
}
assert!(Cli::try_parse_from(["tsk", "config", "ledger-server", "maybe"]).is_err());
match Cli::try_parse_from(["tsk", "nexus", "register-repo", "--ledger-refs", "branches"])
.unwrap()
.command
{
Some(Commands::Nexus {
action: nexus_cmd::NexusCommands::RegisterRepo { ledger_refs, .. },
}) => assert_eq!(ledger_refs.as_deref(), Some("branches")),
_ => panic!("register-repo did not parse"),
}
assert!(
Cli::try_parse_from(["tsk", "nexus", "register-repo", "--ledger-refs", "tags"])
.is_err()
);
}
#[test]
fn the_top_level_help_names_every_command() {
use clap::CommandFactory;
let help = Cli::command().render_help().to_string();
for command in Cli::command().get_subcommands() {
let name = command.get_name();
if name == "help" {
continue;
}
assert!(
help.contains(&format!("\n {}", name)),
"tsk --help does not name {}:\n{}",
name,
help
);
}
assert!(help.contains("tsk install-plugin claude-cli --plugin tsk --plugin swe"));
assert!(help.contains("user scope"));
}
}