use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use crate::adapter::{AdapterKind, AgentAdapter};
use crate::config;
use crate::display;
use crate::error::Result;
use crate::lockfile;
use crate::manifest::CapabilityType;
use crate::resolver::Scope;
use super::add::add_targets_from_installed_dir;
use super::home_dir;
const TUFF_GUIDE_CONTENT: &str = include_str!("../../assets/tuff-cli-guide.md");
fn detected_harnesses(repo_root: &Path) -> Vec<AdapterKind> {
AdapterKind::all()
.into_iter()
.filter(|adapter| {
!matches!(adapter, AdapterKind::OpenAgents | AdapterKind::Codex)
&& adapter.supports(CapabilityType::Skill)
&& adapter.detect(repo_root)
})
.collect()
}
pub fn cmd_init(repo_root: &Path, global: bool) -> Result<()> {
if global {
display::print_init_banner();
let home = home_dir()?;
let lock_path = crate::paths::global_lockfile(&home);
lockfile::init_lockfile_at(&lock_path)?;
let _ = config::read_global_config(&home)?;
println!("initialized global Tuff state");
} else {
display::print_init_banner();
let detected = detected_harnesses(repo_root);
let lock_path = lockfile::init_lockfile(repo_root)?;
let had_config = crate::paths::project_config(repo_root).exists();
let mut config = config::read_config(repo_root)?;
if !config.agents.iter().any(|agent| agent == "open-agents") {
config.agents.push("open-agents".to_string());
}
for adapter in &detected {
if !config.agents.iter().any(|agent| agent == adapter.id()) {
config.agents.push(adapter.id().to_string());
}
}
let lock = lockfile::read_lockfile_at(&lock_path)?;
let targets: BTreeSet<String> = lock
.capabilities
.values()
.flat_map(|entry| entry.targets.keys().cloned())
.collect();
for target in &targets {
if !config.agents.iter().any(|agent| agent == target) {
config.agents.push(target.clone());
}
}
if !had_config && !targets.is_empty() && !targets.contains(&config.default_agent) {
config.default_agent = targets
.iter()
.next()
.cloned()
.unwrap_or_else(|| config.default_agent.clone());
}
config::write_config(repo_root, &config)?;
for dir in &["skills", "tools", "hooks"] {
let path = repo_root.join(".agents").join(dir);
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
}
println!(
"initialized {}",
lockfile::relative_or_absolute_fs(&lock_path, repo_root)
);
println!("scaffolded .agents/ — place your capabilities here:");
let guide_path = repo_root
.join(".agents")
.join("skills")
.join("tuff-cli-guide");
if !guide_path.exists() {
std::fs::create_dir_all(&guide_path)?;
std::fs::write(guide_path.join("SKILL.md"), TUFF_GUIDE_CONTENT)?;
let baseline_tree_hash = crate::cache::hash_tree(&guide_path)?;
crate::cache::populate(&home_dir()?, &baseline_tree_hash, &guide_path)?;
let mut lf = lockfile::require_lockfile(repo_root).unwrap_or(lockfile::Lockfile {
version: lockfile::LOCKFILE_VERSION,
capabilities: BTreeMap::new(),
});
let mut targets = BTreeMap::new();
targets.insert(
"open-agents".to_string(),
lockfile::TargetLockEntry {
managed_hooks: Vec::new(),
managed_mcp_entry: None,
managed_permissions: Vec::new(),
unenforced_rules: Vec::new(),
ownership: lockfile::TargetOwnership::Generated,
sha256: baseline_tree_hash,
installed_path: ".agents/skills/tuff-cli-guide".into(),
},
);
lf.capabilities.insert(
"tuff-cli-guide".into(),
lockfile::CapabilityLockEntry {
capability_type: CapabilityType::Skill,
version: "0.1.0".into(),
version_scheme: lockfile::VersionScheme::Declared,
description: "Guide for using Tuff CLI commands inside this repository.".into(),
source: lockfile::CapabilitySource::local(""),
targets,
implementation: None,
parameters: None,
server: None,
},
);
lockfile::write_lockfile(repo_root, &lf)?;
let extra: Vec<String> = detected
.iter()
.map(|adapter| adapter.id().to_string())
.collect();
if !extra.is_empty() {
add_targets_from_installed_dir(
repo_root,
Scope::Project,
&guide_path,
&extra,
true,
)?;
}
}
}
Ok(())
}