use crate::repl::{self, runner::Runner};
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use crabhub::manifest::Manifest;
use std::path::{Path, PathBuf};
use wcore::Setup;
#[derive(Args, Debug)]
pub struct Hub {
#[arg(long)]
pub branch: Option<String>,
#[arg(long)]
pub path: Option<PathBuf>,
#[command(subcommand)]
pub command: HubCommand,
}
#[derive(Subcommand, Debug)]
pub enum HubCommand {
Install(HubInstall),
Uninstall(HubPackage),
Test(HubTest),
}
#[derive(Args, Debug)]
pub struct HubTest {
pub path: PathBuf,
}
#[derive(Args, Debug)]
pub struct HubInstall {
pub package: String,
#[arg(long)]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct HubPackage {
pub package: String,
}
impl Hub {
pub async fn run(self, runner: &mut Runner) -> Result<()> {
if let HubCommand::Test(t) = self.command {
return test_manifest(&t.path);
}
let (pkg, force, is_install) = match self.command {
HubCommand::Install(p) => (p.package, p.force, true),
HubCommand::Uninstall(p) => (p.package, false, false),
HubCommand::Test(_) => unreachable!(),
};
let on_step = |msg: &str| println!(" {msg}");
if is_install {
let result = crabhub::package::install(
&pkg,
self.branch.as_deref(),
self.path.as_deref(),
force,
on_step,
)
.await?;
println!("Done: {pkg}");
let _ = runner.reload().await;
println!("Daemon reloaded.");
let config_dir = &*wcore::paths::CONFIG_DIR;
let (manifest, mut warnings) = wcore::resolve_manifests(config_dir);
warnings.extend(wcore::check_skill_conflicts(&manifest.skill_dirs));
for w in &warnings {
tracing::warn!("{w}");
}
for (name, mcp) in &manifest.mcps {
if mcp.auth
&& !wcore::paths::TOKENS_DIR
.join(format!("{name}.json"))
.exists()
{
println!("MCP '{name}' requires authentication.");
}
}
if let Some(Setup::Prompt { ref prompt }) = result.setup {
let prompt_text = if prompt.ends_with(".md") {
let repo_dir = result
.repo_dir
.as_ref()
.context("prompt setup requires a repository but none was cloned")?;
let raw = std::fs::read_to_string(repo_dir.join(prompt))
.with_context(|| format!("failed to read setup prompt: {}", prompt))?;
raw.replace("<REPO_DIR>", &repo_dir.display().to_string())
} else {
prompt.clone()
};
println!("Running setup…");
let conn_info = runner.conn_info().clone();
let os_user = std::env::var("USER").unwrap_or_else(|_| "user".into());
let stream = runner.stream(
wcore::paths::DEFAULT_AGENT,
&prompt_text,
result.repo_dir.as_deref(),
false,
None,
Some(os_user),
);
repl::stream_to_terminal(stream, &conn_info).await?;
println!();
}
println!("Configure env vars in config.toml [env] section if needed.");
} else {
crabhub::package::uninstall(&pkg, on_step).await?;
println!("Done: {pkg}");
let _ = runner.reload().await;
println!("Daemon reloaded.");
}
Ok(())
}
}
fn test_manifest(path: &Path) -> Result<()> {
let content =
std::fs::read_to_string(path).with_context(|| format!("cannot read {}", path.display()))?;
let manifest: Manifest =
toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))?;
println!("ok {}", manifest.package.name);
Ok(())
}