use clap::{Parser, Subcommand, ValueEnum};
use crate::cmd;
use netsky_io::IoCommand;
#[derive(Parser, Debug)]
#[command(name = "netsky", version, about = "Netsky launcher")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Up {
#[arg(default_value_t = netsky_core::consts::DEFAULT_CLONE_COUNT)]
n: u32,
#[arg(long = "type", value_enum, default_value_t = AgentType::Codex)]
agent_type: AgentType,
},
Down,
Restart {
#[arg(default_value_t = netsky_core::consts::DEFAULT_CLONE_COUNT)]
n: u32,
#[arg(long)]
handoff: Option<String>,
},
Agent {
#[arg(value_name = "N")]
n: u32,
#[arg(long = "type", value_enum, default_value_t = AgentType::Codex)]
agent_type: AgentType,
#[arg(long)]
fresh: bool,
},
Codex {
#[arg(value_name = "N")]
n: u32,
#[arg(long)]
fresh: bool,
#[arg(long)]
prompt: Option<String>,
#[arg(long)]
drain: bool,
#[arg(long)]
model: Option<String>,
},
#[command(alias = "a")]
Attach {
#[arg(value_name = "TARGET")]
target: String,
},
Agentinfinity,
Agentinit {
#[arg(default_value = netsky_core::consts::AGENTINFINITY_NAME)]
session: String,
},
#[command(subcommand)]
Watchdog(WatchdogCommand),
#[command(subcommand)]
Tick(TickCommand),
#[command(subcommand)]
Launchd(LaunchdCommand),
Handoffs {
#[arg(value_name = "WHICH", default_value = "list")]
which: String,
#[arg(short = 'n', long, default_value_t = 50)]
limit: usize,
},
Escalate {
subject: String,
body: Option<String>,
},
Init {
#[arg(long)]
update: bool,
#[arg(long, value_name = "DIR")]
path: Option<std::path::PathBuf>,
},
Status,
Query {
#[arg(value_name = "SQL")]
sql: String,
},
Doctor {
#[arg(long)]
brief: bool,
#[arg(long)]
quiet: bool,
},
Morning {
#[arg(long)]
send: bool,
},
Quiet {
#[arg(value_name = "SECONDS")]
seconds: u64,
#[arg(long)]
reason: Option<String>,
},
Nap {
#[arg(value_name = "SECONDS")]
seconds: u64,
#[arg(long)]
reason: Option<String>,
},
Drill {
#[arg(value_name = "N")]
n: u8,
},
Test {
#[arg(default_value = "all")]
suite: String,
},
#[command(subcommand)]
Setup(SetupCommand),
#[command(subcommand)]
Hooks(HooksCommand),
#[command(subcommand)]
Channel(ChannelCommand),
#[command(subcommand)]
Io(IoCommand),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum AgentType {
Claude,
Codex,
}
#[derive(Subcommand, Debug)]
pub enum WatchdogCommand {
Tick,
}
#[derive(Subcommand, Debug)]
pub enum TickCommand {
Enable {
#[arg(value_name = "SECONDS")]
seconds: u64,
},
Disable,
Request,
Ticker,
TickerStart,
}
#[derive(Subcommand, Debug)]
pub enum LaunchdCommand {
Install,
Uninstall,
Status,
Reinstall,
}
#[derive(Subcommand, Debug)]
pub enum SetupCommand {
Email {
account: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum ChannelCommand {
Drain {
#[arg(value_name = "AGENT")]
agent: String,
},
Send {
#[arg(value_name = "TARGET")]
target: String,
#[arg(value_name = "TEXT")]
text: String,
#[arg(long)]
from: Option<String>,
},
Quarantine {
#[arg(value_name = "AGENT")]
agent: String,
#[arg(long)]
list: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum HooksCommand {
Install {
#[arg(long)]
force: bool,
},
Uninstall,
Status,
}
pub fn dispatch(cli: Cli) -> netsky_core::Result<()> {
if let Some(name) = command_requires_netsky_cwd(&cli.command) {
netsky_core::paths::require_netsky_cwd(name)?;
}
match cli.command {
None => {
use clap::CommandFactory;
Cli::command().print_help()?;
println!();
Ok(())
}
Some(Command::Up { n, agent_type }) => cmd::up::run(n, agent_type),
Some(Command::Down) => cmd::down::run(),
Some(Command::Restart { n, handoff }) => cmd::restart::run(n, handoff.as_deref()),
Some(Command::Agent {
n,
agent_type,
fresh,
}) => {
if n == 0 {
cmd::agent::run(n, fresh)
} else {
match agent_type {
AgentType::Claude => cmd::agent::run(n, fresh),
AgentType::Codex => cmd::agent::run_codex_resident(n, fresh),
}
}
}
Some(Command::Codex {
n,
fresh,
prompt,
drain,
model,
}) => cmd::codex_agent::run(n, prompt.as_deref(), drain, model.as_deref(), fresh),
Some(Command::Attach { target }) => cmd::attach::run(&target),
Some(Command::Agentinfinity) => cmd::agentinfinity::run(),
Some(Command::Agentinit { session }) => cmd::agentinit::run(&session),
Some(Command::Watchdog(WatchdogCommand::Tick)) => cmd::watchdog::tick(),
Some(Command::Tick(sub)) => cmd::tick::run(sub),
Some(Command::Launchd(sub)) => cmd::launchd::run(sub),
Some(Command::Handoffs { which, limit }) => cmd::handoffs::run(&which, limit),
Some(Command::Escalate { subject, body }) => cmd::escalate::run(&subject, body.as_deref()),
Some(Command::Init { update, path }) => cmd::init::run(path, update),
Some(Command::Status) => cmd::status::run(),
Some(Command::Query { sql }) => cmd::query::run(&sql),
Some(Command::Doctor { brief, quiet }) => cmd::doctor::run(brief, quiet),
Some(Command::Morning { send }) => cmd::morning::run(send),
Some(Command::Quiet { seconds, reason }) => cmd::quiet::run(seconds, reason.as_deref()),
Some(Command::Nap { seconds, reason }) => cmd::nap::run(seconds, reason.as_deref()),
Some(Command::Drill { n }) => cmd::drill::run(n),
Some(Command::Test { suite }) => cmd::test::run(&suite),
Some(Command::Setup(sub)) => cmd::setup::run(sub),
Some(Command::Hooks(sub)) => cmd::hooks::run(sub),
Some(Command::Channel(sub)) => cmd::channel::run(sub),
Some(Command::Io(sub)) => cmd::io::run(sub),
}
}
fn command_requires_netsky_cwd(cmd: &Option<Command>) -> Option<&'static str> {
match cmd {
Some(Command::Up { .. }) => Some("up"),
Some(Command::Down) => Some("down"),
Some(Command::Restart { .. }) => Some("restart"),
Some(Command::Agent { .. }) => Some("agent"),
Some(Command::Codex { .. }) => Some("codex"),
Some(Command::Drill { .. }) => Some("drill"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gated_commands_return_their_name() {
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Up {
n: 0,
agent_type: AgentType::Claude,
})),
Some("up")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Down)),
Some("down")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Restart {
n: 0,
handoff: None,
})),
Some("restart")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Agent {
n: 1,
agent_type: AgentType::Claude,
fresh: false,
})),
Some("agent")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Agent {
n: 1,
agent_type: AgentType::Codex,
fresh: false,
})),
Some("agent")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Codex {
n: 1,
fresh: false,
prompt: None,
drain: true,
model: None,
})),
Some("codex")
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Drill { n: 1 })),
Some("drill")
);
}
#[test]
fn ungated_commands_pass() {
assert_eq!(command_requires_netsky_cwd(&None), None);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Doctor {
brief: false,
quiet: true,
})),
None
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Morning { send: false })),
None
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Watchdog(WatchdogCommand::Tick))),
None
);
assert_eq!(
command_requires_netsky_cwd(&Some(Command::Escalate {
subject: "x".into(),
body: None,
})),
None
);
}
}