pub mod help;
pub mod remote;
pub mod util;
use anyhow::Result;
pub use help::*;
pub use remote::*;
const SUBCOMMANDS: &[&str] = &[
"setup", "secure", "unsecure", "status", "provider", "clis", "help", "web", "remote", "acp",
"mcp", "update", "version", "evolve", "plugin",
];
pub struct Flags {
pub model: Option<String>,
pub yolo: bool,
pub watch: bool,
pub r#continue: bool,
pub resume: bool,
pub resume_session_id: Option<String>,
pub ext: Vec<String>,
pub debounce: Option<u64>,
pub dir: Option<String>,
pub watch_dir: Option<String>,
pub watch_agent: Option<String>,
pub json_metrics: bool,
}
pub fn parse_flags(args: &[String]) -> Flags {
let mut flags = Flags {
model: None,
yolo: false,
watch: false,
r#continue: false,
resume: false,
resume_session_id: None,
ext: Vec::new(),
debounce: None,
dir: None,
watch_dir: None,
watch_agent: None,
json_metrics: false,
};
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--model" | "-m" => {
flags.model = args.get(i + 1).cloned();
i += 2;
}
"--yolo" | "-y" => {
flags.yolo = true;
i += 1;
}
"--watch" | "-w" => {
flags.watch = true;
i += 1;
}
"--continue" | "-c" => {
flags.r#continue = true;
i += 1;
}
"--resume" | "-r" => {
if let Some(next) = args.get(i + 1)
&& !next.starts_with('-')
{
flags.resume_session_id = Some(next.clone());
i += 2;
continue;
}
flags.resume = true;
i += 1;
}
"--ext" | "-e" => {
if let Some(exts) = args.get(i + 1) {
flags.ext = exts.split(',').map(|s| s.trim().to_string()).collect();
}
i += 2;
}
"--debounce" | "-D" => {
flags.debounce = args.get(i + 1).and_then(|v| v.parse().ok());
i += 2;
}
"--dir" | "-d" => {
flags.dir = args.get(i + 1).cloned();
i += 2;
}
"--cwd" => {
flags.watch_dir = args.get(i + 1).cloned();
i += 2;
}
"--agent" | "-a" => {
flags.watch_agent = args.get(i + 1).cloned();
i += 2;
}
"--json-metrics" => {
flags.json_metrics = true;
i += 1;
}
_ => {
i += 1;
}
}
}
flags
}
pub fn extract_prompt(args: &[String]) -> Option<String> {
let mut i = 1;
while i < args.len() {
let arg = args[i].as_str();
if SUBCOMMANDS.contains(&arg) {
return None;
}
if arg.starts_with('-') {
if matches!(
arg,
"--model" | "--ext" | "--debounce" | "--dir" | "-d" | "--cwd" | "--agent" | "-a"
) {
i += 2;
} else if matches!(arg, "--resume" | "-r") {
if args.get(i + 1).is_some_and(|n| !n.starts_with('-')) {
i += 2;
} else {
i += 1;
}
} else {
i += 1;
}
continue;
}
return Some(args[i].clone());
}
None
}
pub fn is_help_arg(arg: Option<&String>) -> bool {
arg.map(|s| matches!(s.as_str(), "help" | "--help" | "-h"))
.unwrap_or(false)
}
pub async fn cmd_web(args: &[String]) -> Result<()> {
#[cfg(not(feature = "web"))]
{
let _ = args;
eprintln!("❌ Web server requires the `web` feature.");
eprintln!(" Rebuild with: cargo build --features web");
std::process::exit(1);
}
#[cfg(feature = "web")]
{
use std::net::SocketAddr;
let config = match crate::config::Config::load() {
Ok(c) => c,
Err(e) => {
eprintln!("❌ {e}");
std::process::exit(1);
}
};
let client = crate::api::provider::OpenAiCompatibleProvider::from_config(&config)?;
let mut host = config.web.host.clone();
let mut port = config.web.port;
let mut username = config.web.username.clone();
let mut cors: Vec<String> = config.web.cors_origins.clone();
let mut password = config.web.password.clone();
let mut i = 2; while i < args.len() {
match args[i].as_str() {
"--host" | "-h" => {
host = args.get(i + 1).cloned().unwrap_or(host);
i += 2;
}
"--port" | "-p" => {
port = args.get(i + 1).and_then(|v| v.parse().ok()).unwrap_or(port);
i += 2;
}
"--username" | "-u" => {
username = args.get(i + 1).cloned().unwrap_or(username);
i += 2;
}
"--cors" => {
if let Some(origins) = args.get(i + 1) {
cors.extend(origins.split(',').map(|s| s.trim().to_string()));
}
i += 2;
}
"--password" | "-P" => {
password = args.get(i + 1).cloned();
i += 2;
}
_ => {
i += 1;
}
}
}
let bind: SocketAddr = format!("{host}:{port}")
.parse()
.map_err(|e| anyhow::anyhow!("Invalid bind address {host}:{port}: {e}"))?;
let (event_bus, _) = tokio::sync::broadcast::channel::<crate::server::WebEvent>(4096);
let working_dir = std::env::current_dir()?.to_string_lossy().to_string();
let web_cfg = crate::config::WebConfig {
host,
port,
username,
password,
cors_origins: cors,
};
let url = format!("http://{bind}");
let auth_msg = if web_cfg.password.is_some() {
"🔒 Password authentication enabled"
} else {
"⚠️ No password set; server is unsecured."
};
eprintln!();
eprintln!(" \x1b[2;37m · ✦ . ˚\x1b[0m");
eprintln!(" \x1b[2;37m . ✧ · ˚ ·\x1b[0m");
eprintln!(" \x1b[2;37m · . ✧ .\x1b[0m");
eprintln!(" \x1b[1;97m █▀▀ █▀▀█ █ █ █▀▀ ▀█▀\x1b[0m");
eprintln!(" \x1b[1;97m █ █ █ █ █ █▀▀ █\x1b[0m");
eprintln!(" \x1b[1;97m ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀\x1b[0m");
eprintln!(" \x1b[2;37m ░▀▀ ░▀▀▀ ░▀▀ ░▀▀ ░▀▀ ░▀\x1b[0m");
eprintln!(" \x1b[2;37m ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\x1b[0m");
eprintln!();
eprintln!(" Web interface: {url}");
eprintln!(" {auth_msg}");
if web_cfg.password.is_none() {
eprintln!(" \x1b[2;37mSet credentials: collet secure --web\x1b[0m");
}
eprintln!();
eprintln!(" Press Ctrl+C to stop.");
eprintln!();
let handle =
crate::server::start(config, client, event_bus, bind, working_dir, web_cfg).await?;
let _ = util::open_url(&url);
tokio::signal::ctrl_c().await.ok();
eprintln!("\n🛑 Shutting down...");
handle.abort();
Ok(())
}
}
pub async fn cmd_remote(args: &[String]) -> Result<()> {
let sub_args: Vec<String> = args
.iter()
.skip_while(|a| a.as_str() != "remote")
.skip(1) .cloned()
.collect();
let sub = sub_args.first().map(|s| s.as_str());
match sub {
Some("help") | Some("--help") | Some("-h") => {
print_remote_usage();
Ok(())
}
None | Some("start") => {
let foreground = sub_args.iter().any(|a| a == "--fg" || a == "--foreground");
if foreground {
remote_start().await
} else {
remote_start_daemon()
}
}
Some("add") => {
let platform = sub_args.get(1).map(|s| s.as_str());
remote_add(platform)
}
Some("rm") | Some("remove") => {
let platform = sub_args.get(1).map(|s| s.as_str());
remote_rm(platform)
}
Some("ls") | Some("list") => remote_ls(),
Some("stop") => remote_stop(),
Some("restart") => remote_restart(),
Some("enable") => remote_enable(),
Some("disable") => remote_disable(),
Some("logs") | Some("log") => {
let follow = sub_args.iter().any(|a| a == "-f" || a == "--follow");
remote_logs(follow)
}
Some("status") => remote_status(),
Some(other) => {
eprintln!("Unknown remote subcommand: {other}");
eprintln!();
print_remote_usage();
std::process::exit(1);
}
}
}
pub async fn cmd_acp(args: &[String]) -> Result<()> {
let sub = args
.iter()
.skip_while(|a| a.as_str() != "acp")
.nth(1)
.map(|s| s.as_str());
match sub {
Some("help") | Some("--help") | Some("-h") => {
print_acp_usage();
Ok(())
}
Some("serve") => {
let config = match crate::config::Config::load() {
Ok(c) => c,
Err(e) => {
eprintln!("❌ {e}");
std::process::exit(1);
}
};
let client = crate::api::provider::OpenAiCompatibleProvider::from_config(&config)?;
crate::acp::server::run_acp_server(config, client).await
}
_ => {
print_acp_usage();
Ok(())
}
}
}
pub fn cmd_mcp(args: &[String]) -> Result<()> {
let sub = args.first().map(|s| s.as_str());
match sub {
Some("help") | Some("-h") | Some("--help") => {
print_mcp_usage();
Ok(())
}
_ => {
let working_dir = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.to_string_lossy()
.to_string();
let output = crate::commands::handle_mcp_command(args, &working_dir);
for line in output.lines() {
let line = line.trim_start_matches('#').trim_start_matches('*').trim();
if !line.is_empty() {
eprintln!("{line}");
}
}
Ok(())
}
}
}
pub async fn cmd_update() -> Result<()> {
eprintln!("Checking for updates...");
let current = env!("CARGO_PKG_VERSION");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let resp = client
.get("https://crates.io/api/v1/crates/collet")
.header("User-Agent", format!("collet/{current}"))
.send()
.await;
match resp {
Ok(r) if r.status().is_success() => {
let body: serde_json::Value = r.json().await?;
if let Some(latest) = body["crate"]["max_stable_version"].as_str() {
if latest == current {
eprintln!("collet {current} is already the latest version.");
} else {
eprintln!("Current version: {current}");
eprintln!("Latest version: {latest}");
eprintln!();
eprintln!("Update with:");
eprintln!(" cargo install collet");
}
} else {
eprintln!("collet {current} (could not determine latest version)");
}
}
Ok(r) => {
eprintln!(
"collet {current} (version check failed: HTTP {})",
r.status()
);
}
Err(e) => {
eprintln!("collet {current} (version check failed: {e})");
}
}
Ok(())
}