use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
mod cmd;
#[derive(Parser)]
#[command(
name = "teamctl",
version,
about = "Declarative CLI for persistent AI agent teams",
long_about = None,
)]
struct Cli {
#[arg(long, short = 'C')]
root: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Init {
name: Option<String>,
#[arg(long)]
template: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long)]
force: bool,
#[arg(long, short = 'y')]
yes: bool,
},
Validate,
Up,
Down,
Reload {
#[arg(long)]
dry_run: bool,
},
#[command(alias = "status")]
Ps,
Logs { target: String },
Tail {
target: String,
#[arg(short, long)]
follow: bool,
},
Mail {
target: Option<String>,
#[arg(long)]
all: bool,
},
Inspect { target: String },
Send { target: String, text: String },
#[command(alias = "pending")]
Approvals,
Approve {
id: i64,
#[arg(long)]
note: Option<String>,
},
Deny {
id: i64,
#[arg(long)]
note: Option<String>,
},
Bridge {
#[command(subcommand)]
action: BridgeAction,
},
Budget {
#[arg(long)]
project: Option<String>,
},
Gc,
Attach {
target: String,
#[arg(long)]
rw: bool,
},
Exec {
target: String,
#[arg(last = true, allow_hyphen_values = true, num_args = 1..)]
argv: Vec<String>,
},
Shell { target: String },
Env {
#[arg(long)]
doctor: bool,
},
Context {
#[command(subcommand)]
action: ContextAction,
},
#[command(name = "rl-watch")]
RlWatch {
target: String,
#[arg(last = true, allow_hyphen_values = true)]
runtime_command: Vec<String>,
},
}
#[derive(Subcommand)]
enum ContextAction {
Ls,
Current,
Use { name: String },
Add { name: String, path: PathBuf },
Rm { name: String },
}
#[derive(Subcommand)]
enum BridgeAction {
Open {
#[arg(long)]
from: String,
#[arg(long)]
to: String,
#[arg(long)]
topic: String,
#[arg(long, default_value_t = 120)]
ttl: u64,
},
Close {
id: i64,
},
#[command(alias = "list")]
Ls,
Log {
id: i64,
},
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("TEAMCTL_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
if let Command::Init {
name,
template,
project,
force,
yes,
} = cli.command
{
return cmd::init::run(name, template, project, force, yes);
}
if let Command::Context { action } = &cli.command {
return match action {
ContextAction::Ls => cmd::context::ls(),
ContextAction::Current => cmd::context::current(),
ContextAction::Use { name } => cmd::context::use_(name),
ContextAction::Add { name, path } => cmd::context::add(name, path),
ContextAction::Rm { name } => cmd::context::rm(name),
};
}
let (root, source) = resolve_root_with_source(cli.root)?;
let warns_on_override = matches!(
cli.command,
Command::Validate | Command::Ps | Command::Mail { .. } | Command::Inspect { .. }
);
if warns_on_override {
cmd::warn::maybe_warn_root_source(&source, &root);
}
match cli.command {
Command::Validate => cmd::validate::run(&root),
Command::Up => cmd::up::run(&root),
Command::Down => cmd::down::run(&root),
Command::Reload { dry_run } => cmd::reload::run(&root, dry_run),
Command::Ps => cmd::status::run(&root),
Command::Logs { target } => cmd::logs::run(&root, &target),
Command::Tail { target, follow } => cmd::tail::run(&root, &target, follow),
Command::Mail { target, all } => cmd::mail::run(&root, target.as_deref(), all),
Command::Inspect { target } => cmd::inspect::run(&root, &target),
Command::Send { target, text } => cmd::send::run(&root, &target, &text),
Command::Budget { project } => cmd::budget::run(&root, project.as_deref()),
Command::Gc => cmd::gc::run(&root),
Command::RlWatch {
target,
runtime_command,
} => cmd::rl_watch::run(&root, &target, &runtime_command),
Command::Approvals => cmd::approval::pending(&root),
Command::Approve { id, note } => cmd::approval::decide(&root, id, true, note.as_deref()),
Command::Deny { id, note } => cmd::approval::decide(&root, id, false, note.as_deref()),
Command::Bridge { action } => match action {
BridgeAction::Open {
from,
to,
topic,
ttl,
} => cmd::bridge::open(&root, &from, &to, &topic, ttl),
BridgeAction::Close { id } => cmd::bridge::close(&root, id),
BridgeAction::Ls => cmd::bridge::list(&root),
BridgeAction::Log { id } => cmd::bridge::log(&root, id),
},
Command::Attach { target, rw } => cmd::attach::run(&root, &target, rw),
Command::Exec { target, argv } => cmd::exec::run(&root, &target, &argv),
Command::Shell { target } => cmd::exec::shell(&root, &target),
Command::Env { doctor } => cmd::env::run(&root, doctor),
Command::Context { .. } => unreachable!("handled above"),
Command::Init { .. } => unreachable!("handled above"),
}
}
fn resolve_root_with_source(explicit: Option<PathBuf>) -> Result<(PathBuf, cmd::warn::RootSource)> {
use cmd::warn::RootSource;
if let Some(p) = explicit {
let canon = p
.canonicalize()
.with_context(|| format!("canonicalize --root {}", p.display()))?;
return Ok((canon, RootSource::CliFlag));
}
if let Some(raw) = std::env::var_os("TEAMCTL_ROOT") {
if !raw.is_empty() {
let p = PathBuf::from(raw);
let canon = p
.canonicalize()
.with_context(|| format!("canonicalize $TEAMCTL_ROOT {}", p.display()))?;
return Ok((canon, RootSource::Env));
}
}
let cwd = std::env::current_dir().context("get cwd")?;
let p = team_core::compose::Compose::discover(&cwd)?;
Ok((p, RootSource::WalkUp))
}