use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
mod hash_trip;
#[derive(Parser)]
#[command(
name = "apm",
about = "Agent Project Manager",
version,
help_template = "\
Agent Project Manager — a git-native ticket system for human+AI teams.
{usage-heading} {usage}
Setup:
init Initialize apm in the current repository
agents Print agent instructions
Ticket management:
new Create a new ticket
list List tickets
show Show a ticket
set Set a field on a ticket
spec Read or write individual spec sections
close Force-close a ticket from any state
assign Assign a ticket to an owner
Workflow:
review Review a ticket and transition state (supervisor)
next Return the highest-priority actionable ticket
start Claim a ticket and provision its worktree (agent)
state Transition a ticket's state (low-level)
work Orchestrate workers: dispatch in a loop
sync Sync with remote (poll events, detect merges)
Epics:
epic Manage epics
refresh-epic Pull default-branch updates into an epic branch
Maintenance:
worktrees List or remove permanent git worktrees
clean Remove worktrees and branches for closed tickets
workers List and manage running worker processes
validate Validate config and ticket integrity
archive Move closed ticket files to the archive directory
version Print version and build type
Server:
register Generate a one-time password for device registration
sessions List active sessions
revoke Revoke sessions
{options}
",
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum EpicCommand {
New {
title: String,
},
Close {
id: String,
},
List,
Show {
id: String,
#[arg(long)]
no_aggressive: bool,
},
Set {
id: String,
field: String,
value: String,
},
}
#[derive(Subcommand)]
enum Command {
#[command(long_about = "Initialize apm in the current repository.
Creates the .apm/ directory containing:
* config.toml — project config
* workflow.toml — state-machine definition
* ticket.toml — ticket template
* agents.md — agent onboarding instructions
* apm.spec-writer.md — spec-writer agent manual
* apm.worker.md — worker agent manual
Also installs git hooks (.git/hooks/post-merge, post-checkout) so apm can
detect branch merges automatically.
Unless --no-claude is passed, adds apm commands to .claude/settings.json
so that Claude Code's allow list does not prompt for every apm call.
Use --migrate if you have an existing root-level apm.toml and apm.agents.md
that need to be moved into .apm/.")]
Init {
#[arg(long)]
no_claude: bool,
#[arg(long)]
migrate: bool,
#[arg(long)]
with_docker: bool,
#[arg(long)]
quiet: bool,
},
#[command(long_about = "List tickets (read-only query).
All filter flags are combinable. By default, tickets in terminal states
(closed, etc.) are hidden; pass --all to include them.
Examples:
apm list # all non-closed tickets
apm list --state ready # only tickets awaiting an agent
apm list --unassigned # no agent assigned yet
apm list --actionable agent # tickets an agent can act on now
apm list --all # everything including closed
apm list --mine # only your tickets
apm list --author alice # only tickets by alice")]
List {
#[arg(long)]
state: Option<String>,
#[arg(long)]
unassigned: bool,
#[arg(long)]
all: bool,
#[arg(long, value_name = "ACTOR")]
actionable: Option<String>,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
mine: bool,
#[arg(long, value_name = "USERNAME", conflicts_with = "mine")]
author: Option<String>,
#[arg(long, value_name = "USERNAME", conflicts_with = "mine")]
owner: Option<String>,
},
#[command(long_about = "Show the full content of a ticket.
Reads the ticket file directly from its branch blob in the git object store,
so the working tree does not need to be checked out on that branch.
By default, `apm show` fetches the latest remote state first. Pass
--no-aggressive to skip the fetch (faster for scripts or offline use).
The ticket ID can be supplied as:
* a plain integer (e.g. 42 → pads to 0042)
* a 4+ char hex prefix (e.g. 00ab)
* the full 8-char hex ID")]
Show {
#[arg(value_name = "ID")]
id: String,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
edit: bool,
},
#[command(long_about = "Create a new ticket and its branch.
Creates a ticket Markdown file on a new branch (ticket/<id>-<slug>) and
opens $EDITOR so you can fill in the spec immediately.
Agents must always pass --no-edit to skip the interactive editor:
apm new --no-edit \"Short title\"
Use --side-note during implementation to capture an out-of-scope observation
without interrupting the current ticket:
apm new --side-note \"Spotted issue\" --context \"What was observed\"
After creating a ticket the typical next step is:
apm state <id> in_design # claim the spec for writing")]
New {
#[arg(value_name = "TITLE")]
title: String,
#[arg(long)]
no_edit: bool,
#[arg(long)]
side_note: bool,
#[arg(long)]
context: Option<String>,
#[arg(long)]
context_section: Option<String>,
#[arg(long)]
no_aggressive: bool,
#[arg(long, value_name = "NAME")]
section: Vec<String>,
#[arg(long, value_name = "TEXT")]
set: Vec<String>,
#[arg(long, value_name = "ID")]
epic: Option<String>,
#[arg(long, value_name = "IDS")]
depends_on: Vec<String>,
},
#[command(long_about = "Transition a ticket to a new state.
Valid target states depend on the ticket's current state. The allowed
transitions are defined in .apm/apm.toml under [[workflow.states]].
Illegal transitions are rejected with an error.
Run `apm show <id>` first to check the current state, then choose a
target from the edges listed for that state in apm.toml.
Use --force to bypass the transition rules (escape hatch for stuck tickets).
The target state must still exist in the config; document-level validations
(spec completeness, unchecked criteria) are still enforced.
Examples:
apm state 42 in_design # claim a new ticket for spec writing
apm state 42 specd # submit spec for supervisor review
apm state 42 implemented # mark implementation done (open PR first)
apm state 42 new --force # reset a stuck in_design ticket
apm state 42 ready --force # reset a stuck in_progress ticket")]
State {
#[arg(value_name = "ID")]
id: String,
#[arg(value_name = "STATE")]
state: String,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
force: bool,
},
#[command(long_about = "Set a metadata field on a ticket.
Valid field names:
priority — integer; higher = picked first by `apm next`
effort — integer 1-10; implementation scale estimate
risk — integer 1-10; technical risk estimate
title — short human-readable summary
agent — name of the assigned agent (use \"-\" to clear)
branch — override the ticket's branch name (use \"-\" to clear)
depends_on — comma-separated list of blocker IDs (use \"-\" to clear)
Examples:
apm set 42 priority 5
apm set 42 agent alice
apm set 42 agent - # clear agent field
apm set 42 depends_on abc123 # single blocker
apm set 42 depends_on \"abc123,def456\" # multiple blockers
apm set 42 depends_on - # clear depends_on")]
Set {
#[arg(value_name = "ID")]
id: String,
#[arg(value_name = "FIELD")]
field: String,
#[arg(value_name = "VALUE")]
value: String,
#[arg(long)]
no_aggressive: bool,
},
#[command(long_about = "Claim a ticket and provision its permanent worktree.
Sets the ticket's agent field to $APM_AGENT_NAME and transitions state to
in_progress, then provisions (or reuses) a permanent git worktree for the
ticket branch. Prints the worktree path so the caller can cd into it or use
`git -C <path>` for all subsequent git operations.
Without --spawn, the command only claims the ticket and sets up the worktree.
No worker process is launched — the engineer works in the worktree manually.
Use this when you want to implement the ticket yourself.
With --spawn, a background Claude Code subprocess is launched in the worktree.
The subprocess receives the project allow list by default; add -P to also pass
--dangerously-skip-permissions. Worker output is written to
.apm-worker.log in the worktree directory.
--next auto-selects the highest-priority actionable ticket; mutually
exclusive with an explicit ID.
Examples:
apm start 42 # claim ticket 42 for manual work
apm start --next # claim the top-priority ticket for manual work
apm start --spawn 42 # hand ticket 42 to a background agent
apm start --spawn --next -P # background agent, skip permissions")]
Start {
id: Option<String>,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
spawn: bool,
#[arg(long, short = 'P')]
skip_permissions: bool,
#[arg(long)]
next: bool,
},
#[command(long_about = "Return the highest-priority ticket actionable right now.
Considers only tickets in states that the current actor can act on (agent
by default). Selects by priority descending, then by id ascending as a
tiebreaker.
Returns nothing (exit 0, empty output) when there is no actionable ticket.
--json outputs the result as a JSON object — useful in agent startup loops:
apm next --json # {\"id\": \"0042\", \"title\": \"...\", \"state\": \"ready\", ...}
Typical agent startup sequence:
apm sync
apm next --json # check for work
apm start --next # claim and provision in one step")]
Next {
#[arg(long)]
json: bool,
#[arg(long)]
no_aggressive: bool,
},
#[command(long_about = "Fetch from remote and reconcile the local ticket cache.
What sync does:
1. git fetch (unless --offline)
2. Detects ticket branches that have been merged into main
3. For each merged branch, closes the ticket immediately; use --auto-close
to skip the confirmation prompt in CI
4. Updates the local branch cache
Run sync at the start of each agent session to ensure local state reflects
what has happened on the remote since last time.
Examples:
apm sync # interactive, fetch from remote
apm sync --offline # re-process local branches only
apm sync --auto-close # close all merged tickets silently
apm sync --quiet # suppress non-error output")]
Sync {
#[arg(long)]
offline: bool,
#[arg(long)]
quiet: bool,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
auto_close: bool,
#[arg(long)]
push_default: bool,
#[arg(long)]
push_refs: bool,
},
#[command(long_about = "Set the owner field on any ticket, regardless of its current state.
Use this to assign a ticket to a user or agent, or to clear the owner field.
Ownership gates dispatcher pickup: `apm work`, `apm start --next`, and the UI
dispatch loop only pick up tickets whose owner matches the current user's identity.
Tickets with no owner are never auto-dispatched. Assign a ticket before running
the dispatch loop.
Examples:
apm assign 42 alice # assign ticket 42 to alice
apm assign 42 - # clear the owner field")]
Assign {
#[arg(value_name = "ID")]
id: String,
#[arg(value_name = "USERNAME")]
username: String,
#[arg(long)]
no_aggressive: bool,
#[arg(long)]
force: bool,
},
#[command(long_about = "Manage permanent git worktrees for ticket branches.
APM uses permanent worktrees (in the apm--worktrees/ sibling directory by
default) so that agents can work on a ticket branch without disturbing the
main working tree. These worktrees survive `apm sync` and are reused across
sessions.
Examples:
apm worktrees # list all known worktrees
apm worktrees --remove 42 # remove the worktree for ticket 42")]
Worktrees {
#[arg(long, value_name = "ID")]
remove: Option<String>,
},
#[command(long_about = "Supervisor command: review and edit a ticket spec, then transition state.
Opens $EDITOR on the ticket file so the supervisor can read the spec, leave
feedback in amendment-request boxes, or update acceptance criteria. After
the editor closes, prompts for a state transition unless --to is supplied.
Common review flows:
apm review 42 --to specd # approve spec as-is
apm review 42 --to ammend # request changes (fill in amendment boxes first)
apm review 42 --to ready # approve and queue for implementation
apm review 42 --to implemented # accept implementation
--to skips the interactive prompt — useful in scripts or when the transition
is already decided before opening the editor.")]
Review {
#[arg(value_name = "ID")]
id: String,
#[arg(long, value_name = "STATE")]
to: Option<String>,
#[arg(long)]
no_aggressive: bool,
},
#[command(long_about = "Validate apm.toml correctness and full ticket integrity.
Config checks:
* .apm/ TOML files parse without errors
* All state transitions reference states that exist in the config
* completion 'pr' or 'merge' requires [git_host] with a provider
* instruction file paths exist on disk
Ticket integrity checks:
* Every ticket's branch field matches its actual branch name
* dependency-rule violations per completion strategy
* Unknown state values (state not in config.workflow.states)
* ID/filename mismatches
* in_progress/implemented tickets missing a branch field
* Branches already merged into the default branch with ticket still open
* Missing ## Spec or ## History sections
* Document-structure validation errors
* in_design/in_progress tickets whose worktree directory is absent
--fix repairs branch-field mismatches automatically and closes tickets
whose branch has already been merged. Missing worktrees are reported but
never auto-recreated.
--json outputs the full results as JSON — useful in CI pipelines:
apm validate --json | jq '.errors'
--config-only skips all per-ticket and filesystem checks, including
merged-branch and worktree checks.")]
Validate {
#[arg(long)]
fix: bool,
#[arg(long)]
json: bool,
#[arg(long)]
config_only: bool,
#[arg(long)]
no_aggressive: bool,
},
#[command(name = "_hook", hide = true)]
Hook {
#[arg(value_name = "HOOK")]
hook_name: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
_extra: Vec<String>,
},
#[command(long_about = "Print the contents of the instructions file configured under [agents] instructions in .apm/apm.toml.
Useful for onboarding a new agent subprocess: pipe or paste the output into
the agent's context so it knows the workflow, branch conventions, and shell
discipline rules without needing file-system access to the repo.
Example:
apm agents | pbcopy # copy to clipboard
apm agents > /tmp/agents.md # write to a temp file for injection")]
Agents,
#[command(long_about = "Orchestration loop: repeatedly dispatch agents until no work remains.
Calls `apm start --next --spawn` in a loop, launching one Claude subprocess
per actionable ticket, until `apm next` returns null (no more tickets).
--dry-run prints the ticket IDs that would be started without actually
spawning any subprocesses — useful to preview the work queue.
-P passes --dangerously-skip-permissions to every spawned worker.
--daemon keeps the process alive after the queue is exhausted, polling at
--interval seconds (default 30) and dispatching new workers as slots open
or tickets become actionable. Ctrl-C stops the daemon; already-running
workers continue independently.
Example:
apm work --dry-run # preview
apm work # run with normal permissions
apm work -P # run with skipped permissions
apm work --daemon # run forever, poll every 30s
apm work --daemon --interval 60 # poll every 60s")]
Work {
#[arg(long, short = 'P')]
skip_permissions: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, short = 'd')]
daemon: bool,
#[arg(long, default_value = "30")]
interval: u64,
#[arg(long, value_name = "EPIC_ID")]
epic: Option<String>,
},
#[command(long_about = "Move an existing ticket into (or out of) an epic.
Rebases the ticket's branch onto the target epic's branch tip, then updates
the ticket's `epic` and `target_branch` frontmatter fields in place. The
ticket keeps its original ID and branch name; only the branch base changes.
Move into an epic:
apm move <ticket> <epic_id>
Move between epics:
apm move <ticket> <epic_id_2>
Remove from epic (rebase onto main):
apm move <ticket> -
Both <ticket> and <epic_id> accept 4–8 char hex prefixes. Use \"-\" as the
second argument to detach from any current epic.")]
Move {
#[arg(value_name = "TICKET")]
ticket: String,
#[arg(value_name = "EPIC")]
target: String,
},
#[command(long_about = "Force-close a ticket from any state (supervisor only).
Bypasses the normal state machine and closes the ticket immediately,
regardless of current state. An optional reason is appended to the ticket's
## History section for the record.
This is an escape hatch for tickets that are abandoned, duplicated, or
otherwise need to be removed from the active queue without following the
normal flow. Prefer the standard `apm state <id> closed` transition when
the ticket has been properly resolved.
Example:
apm close 42 --reason \"duplicate of #38\"")]
Close {
id: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
no_aggressive: bool,
},
#[command(long_about = "Move terminal-state ticket files from tickets/ to the configured archive_dir.
Requires `archive_dir` under the [tickets] section of .apm/config.toml:
[tickets]
archive_dir = \"archive/tickets\"
Examples:
apm archive # archive all closed tickets
apm archive --dry-run # preview which files would be moved
apm archive --older-than 30d # archive only tickets updated >30 days ago
apm archive --older-than 2026-01-01 # ISO date threshold")]
Archive {
#[arg(long)]
dry_run: bool,
#[arg(long, value_name = "THRESHOLD")]
older_than: Option<String>,
},
#[command(long_about = "Remove worktrees (and optionally branches) for terminal-state tickets.
Default (no flags): removes worktrees only. Branches are never touched
without --branches. APM only ever deletes branches matching `ticket/*`
or `epic/*` — never any other branch.
apm clean # remove worktrees only
apm clean --dry-run # preview worktree removals
apm clean --branches # also delete local + remote ticket/* branches
apm clean --branches --older-than 30d # only tickets last updated >30d ago
apm clean --branches --older-than 2026-01-01 # ISO date threshold
apm clean --untracked # also remove untracked non-temp files
apm clean --force # bypass merge/divergence checks
Known temp files (.apm-worker.pid, .apm-worker.log, pr-body.md, body.md,
ac.txt) are always removed automatically without needing --untracked.")]
Clean {
#[arg(long)]
dry_run: bool,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
force: bool,
#[arg(long)]
branches: bool,
#[arg(long, value_name = "THRESHOLD")]
older_than: Option<String>,
#[arg(long)]
untracked: bool,
#[arg(long)]
epics: bool,
},
Workers {
#[arg(long, value_name = "ID")]
log: Option<String>,
#[arg(long, value_name = "ID")]
kill: Option<String>,
},
Epic {
#[command(subcommand)]
command: EpicCommand,
},
RefreshEpic {
id: String,
},
#[command(long_about = "Read or write individual sections of a ticket's spec.
--section alone reads the named section and prints it to stdout:
apm spec 42 --section Problem
--section combined with --set writes new content to that section (use \"-\"
to read the new content from stdin):
apm spec 42 --section Approach --set \"New approach text\"
echo \"text\" | apm spec 42 --section Approach --set -
--check validates that all required sections defined in apm.toml are
present and non-empty:
apm spec 42 --check
--mark checks off the first unchecked item in --section whose text contains
the given substring:
apm spec 42 --section \"Acceptance criteria\" --mark \"output is JSON\"")]
Spec {
#[arg(value_name = "ID")]
id: String,
#[arg(long)]
section: Option<String>,
#[arg(long, allow_hyphen_values = true)]
set: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with = "set")]
set_file: Option<String>,
#[arg(long)]
check: bool,
#[arg(long)]
mark: Option<String>,
#[arg(long, allow_hyphen_values = true, conflicts_with_all = ["set", "set_file", "append_file", "add_task"])]
append: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with_all = ["set", "set_file", "append", "add_task"])]
append_file: Option<String>,
#[arg(long, conflicts_with_all = ["set", "set_file", "append", "append_file"])]
add_task: Option<String>,
#[arg(long)]
no_aggressive: bool,
},
Register {
username: Option<String>,
},
Sessions,
Revoke {
#[arg(value_name = "USERNAME")]
username: Option<String>,
#[arg(long, value_name = "HINT")]
device: Option<String>,
#[arg(long, conflicts_with = "device")]
all: bool,
},
Version,
}
pub fn repo_root() -> Result<PathBuf> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.context("git not found")?;
if !output.status.success() {
anyhow::bail!("not inside a git repository");
}
Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim()))
}
fn main() -> Result<()> {
let cli = Cli::parse();
let root = repo_root()?;
if let Ok(ref config) = apm_core::config::Config::load(&root) {
if config.logging.enabled {
let log_path = apm_core::logger::resolve_log_path(
&config.project.name,
config.logging.file.as_deref(),
);
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let agent = std::env::var("APM_AGENT_NAME").unwrap_or_else(|_| "apm".to_string());
apm_core::logger::init(&root, &log_path, &agent);
}
}
let args: Vec<String> = std::env::args().skip(1).collect();
apm_core::logger::log("cmd", &args.join(" "));
if !hash_trip::is_exempt_command(&cli.command) {
match hash_trip::run(&root)? {
hash_trip::HashTripOutcome::Clean | hash_trip::HashTripOutcome::PassedAndRefreshed => {}
hash_trip::HashTripOutcome::Failed(issues) => {
for (subject, msg) in &issues {
#[allow(clippy::print_stderr)]
{ eprintln!(" {}: {}", subject, msg); }
}
if hash_trip::is_read_only_command(&cli.command) {
#[allow(clippy::print_stderr)]
{ eprintln!("warning: config has changed and apm validate is failing."); }
#[allow(clippy::print_stderr)]
{ eprintln!("Run apm validate to see details and fix the issues."); }
} else {
#[allow(clippy::print_stderr)]
{ eprintln!("error: config has changed and validation is failing."); }
#[allow(clippy::print_stderr)]
{ eprintln!("Mutating commands are blocked. Run apm validate to fix."); }
std::process::exit(2);
}
}
}
}
match cli.command {
Command::Init { no_claude, migrate, with_docker, quiet } => cmd::init::run(&root, no_claude, migrate, with_docker, quiet),
Command::List { state, unassigned, all, actionable, no_aggressive, mine, author, owner } => cmd::list::run(&root, state, unassigned, all, actionable, no_aggressive, mine, author, owner),
Command::New { title, no_edit, side_note, context, context_section, no_aggressive, section, set, epic, depends_on } => cmd::new::run(&root, title, no_edit, side_note, context, context_section, no_aggressive, section, set, epic, depends_on),
Command::Show { id, no_aggressive, edit } => cmd::show::run(&root, &id, no_aggressive, edit),
Command::State { id, state, no_aggressive, force } => cmd::state::run(&root, &id, state, no_aggressive, force),
Command::Set { id, field, value, no_aggressive } => cmd::set::run(&root, &id, field, value, no_aggressive),
Command::Next { json, no_aggressive } => cmd::next::run(&root, json, no_aggressive),
Command::Start { id, no_aggressive, spawn, skip_permissions, next } => {
match (next, id) {
(true, Some(_)) => anyhow::bail!("--next and an explicit ID are mutually exclusive"),
(true, None) => cmd::start::run_next(&root, no_aggressive, spawn, skip_permissions),
(false, Some(id)) => {
let agent_name = apm_core::config::resolve_caller_name();
cmd::start::run(&root, &id, no_aggressive, spawn, skip_permissions, &agent_name)
}
(false, None) => anyhow::bail!("provide a ticket ID or use --next"),
}
}
Command::Sync { offline, quiet, no_aggressive, auto_close, push_default, push_refs } => cmd::sync::run(&root, offline, quiet, no_aggressive, auto_close, push_default, push_refs),
Command::Assign { id, username, no_aggressive, force } => cmd::assign::run(&root, &id, &username, no_aggressive, force),
Command::Worktrees { remove } => cmd::worktrees::run(&root, remove.as_deref()),
Command::Review { id, to, no_aggressive } => cmd::review::run(&root, &id, to, no_aggressive),
Command::Validate { fix, json, config_only, no_aggressive } => cmd::validate::run(&root, fix, json, config_only, no_aggressive),
Command::Hook { hook_name, .. } => { cmd::hook::run(&root, &hook_name); Ok(()) }
Command::Agents => cmd::agents::run(&root),
Command::Work { skip_permissions, dry_run, daemon, interval, epic } => cmd::work::run(&root, skip_permissions, dry_run, daemon, interval, epic),
Command::Move { ticket, target } => cmd::move_ticket::run(&root, &ticket, &target),
Command::Close { id, reason, no_aggressive } => cmd::close::run(&root, &id, reason, no_aggressive),
Command::Archive { dry_run, older_than } => cmd::archive::run(&root, dry_run, older_than),
Command::Clean { dry_run, yes, force, branches, older_than, untracked, epics } => cmd::clean::run(&root, dry_run, yes, force, branches, older_than, untracked, epics),
Command::Spec { id, section, set, set_file, check, mark, append, append_file, add_task, no_aggressive } => cmd::spec::run(&root, &id, section, set, set_file, check, mark, append, append_file, add_task, no_aggressive),
Command::Workers { log, kill } => cmd::workers::run(&root, log.as_deref(), kill.as_deref()),
Command::Epic { command: EpicCommand::New { title } } => cmd::epic::run_new(&root, title),
Command::Epic { command: EpicCommand::Close { id } } => cmd::epic::run_close(&root, &id),
Command::Epic { command: EpicCommand::List } => cmd::epic::run_list(&root),
Command::Epic { command: EpicCommand::Show { id, no_aggressive } } => cmd::epic::run_show(&root, &id, no_aggressive),
Command::Epic { command: EpicCommand::Set { id, field, value } } => cmd::epic::run_set(&root, &id, &field, &value),
Command::RefreshEpic { id } => cmd::epic::run_refresh_epic(&root, &id),
Command::Register { username } => {
let inferred = username.is_none();
let config = apm_core::config::Config::load(&root)?;
let username = username.unwrap_or_else(|| {
apm_core::config::try_github_username(&config.git_host)
.expect("could not detect GitHub username; pass one explicitly")
});
cmd::register::run(&root, &username, inferred)
}
Command::Sessions => cmd::sessions::run(&root),
Command::Revoke { username, device, all } => {
if !all && username.is_none() {
eprintln!("error: provide a username or use --all");
std::process::exit(1);
}
cmd::revoke::run(&root, username.as_deref(), device.as_deref(), all)
}
Command::Version => { cmd::version::run(); Ok(()) }
}
}
use apm::cmd;