use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::{info, warn};
use super::agent_spec::{default_role_agents, write_agent_to_dir, AgentBody, AgentSpec};
use super::hook_spec::{default_project_hooks, ProjectHookDefs};
use super::manifest::{AssetManifest, EntryKind};
pub async fn install_project_assets(project_dir: &Path, dry_run: bool) -> Result<InstallReport> {
let mut report = InstallReport::default();
let mut manifest = AssetManifest::new(project_dir);
let agents_dir = project_dir.join(".kimi").join("agents");
manifest.add_dir(&PathBuf::from(".kimi/agents"));
for agent in default_role_agents() {
let agent_dir = agents_dir.join(&agent.id);
let spec_path = agent_dir.join("agent.yaml");
let prompt_path = agent_dir.join("system.md");
if dry_run {
report.would_install.push(format!("agent/{}", agent.id));
} else {
let spec = AgentSpec {
version: 1,
agent: AgentBody {
name: agent.name.clone(),
extend: Some("default".to_string()),
system_prompt_path: "./system.md".to_string(),
system_prompt_args: None,
tools: agent.tools.clone(),
subagents: None,
},
};
if let Ok(yaml) = spec.to_yaml() {
if let Some(backup) = super::manifest::maybe_backup(&spec_path, &yaml).await {
report.backups_created.push(backup);
if let Some(last) = report.backups_created.last() {
manifest.add_backup(&spec_path, Path::new(last));
}
}
}
if let Some(backup) =
super::manifest::maybe_backup(&prompt_path, &agent.system_prompt).await
{
report.backups_created.push(backup);
if let Some(last) = report.backups_created.last() {
manifest.add_backup(&prompt_path, Path::new(last));
}
}
match write_agent_to_dir(&agent, &agent_dir).await {
Ok(()) => {
info!(agent = %agent.id, dir = %agent_dir.display(), "Installed agent");
report.agents_installed.push(agent.id.clone());
manifest
.add_file(
&PathBuf::from(format!(".kimi/agents/{}/agent.yaml", agent.id)),
EntryKind::AgentSpec,
)
.await;
manifest
.add_file(
&PathBuf::from(format!(".kimi/agents/{}/system.md", agent.id)),
EntryKind::AgentPrompt,
)
.await;
manifest.add_dir(&PathBuf::from(format!(".kimi/agents/{}", agent.id)));
}
Err(e) => {
warn!(agent = %agent.id, error = %e, "Failed to install agent");
report.errors.push(format!("agent {}: {}", agent.id, e));
}
}
}
}
let hooks_dir = project_dir.join(".kimi").join("hooks");
manifest.add_dir(&PathBuf::from(".kimi/hooks"));
let hook_defs = default_project_hooks();
for (filename, content) in &hook_defs.scripts {
let path = hooks_dir.join(filename);
if dry_run {
report.would_install.push(format!("hooks/{}", filename));
} else {
tokio::fs::create_dir_all(&hooks_dir).await?;
if let Some(backup) = super::manifest::maybe_backup(&path, content).await {
report.backups_created.push(backup);
if let Some(last) = report.backups_created.last() {
manifest.add_backup(&path, Path::new(last));
}
}
crate::runtime::atomic::atomic_write(&path, content.as_bytes()).await?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
let _ = tokio::fs::set_permissions(&path, perms).await;
}
info!(script = %filename, path = %path.display(), "Installed hook script");
report.hooks_installed.push(filename.clone());
manifest
.add_file(
&PathBuf::from(format!(".kimi/hooks/{}", filename)),
EntryKind::HookScript,
)
.await;
}
}
let hooks_toml = hooks_toml_reference(&hook_defs);
let hooks_toml_path = project_dir.join(".kimi").join("hooks.toml.example");
if dry_run {
report.would_install.push("hooks.toml.example".to_string());
} else {
if let Some(backup) = super::manifest::maybe_backup(&hooks_toml_path, &hooks_toml).await {
report.backups_created.push(backup);
if let Some(last) = report.backups_created.last() {
manifest.add_backup(&hooks_toml_path, Path::new(last));
}
}
crate::runtime::atomic::atomic_write(&hooks_toml_path, hooks_toml.as_bytes()).await?;
info!(path = %hooks_toml_path.display(), "Wrote hooks.toml.example");
manifest
.add_file(
&PathBuf::from(".kimi/hooks.toml.example"),
EntryKind::HookConfig,
)
.await;
}
let omk_skills_dir = crate::runtime::config::data_dir().join("skills");
let kimi_skills_dir = project_dir.join(".kimi").join("skills");
if omk_skills_dir.exists() {
if dry_run {
report.would_install.push("skills".to_string());
} else {
tokio::fs::create_dir_all(&kimi_skills_dir).await?;
info!(src = %omk_skills_dir.display(), dst = %kimi_skills_dir.display(), "Linked skills");
report.skills_linked = true;
manifest.add_dir(&PathBuf::from(".kimi/skills"));
}
}
if !dry_run {
if let Err(e) = manifest.save(project_dir).await {
warn!(error = %e, "Failed to save asset manifest");
}
}
Ok(report)
}
#[allow(dead_code)]
pub async fn install_user_assets(dry_run: bool) -> Result<InstallReport> {
let mut report = InstallReport::default();
let config_dir = dirs::config_dir()
.map(|d| d.join("kimi"))
.unwrap_or_else(|| PathBuf::from("~/.config/kimi"));
let agents_dir = config_dir.join("agents");
for agent in default_role_agents() {
let agent_dir = agents_dir.join(&agent.id);
let spec_path = agent_dir.join("agent.yaml");
let prompt_path = agent_dir.join("system.md");
if dry_run {
report
.would_install
.push(format!("user-agent/{}", agent.id));
} else {
let spec = AgentSpec {
version: 1,
agent: AgentBody {
name: agent.name.clone(),
extend: Some("default".to_string()),
system_prompt_path: "./system.md".to_string(),
system_prompt_args: None,
tools: agent.tools.clone(),
subagents: None,
},
};
if let Ok(yaml) = spec.to_yaml() {
if let Some(backup) = super::manifest::maybe_backup(&spec_path, &yaml).await {
report.backups_created.push(backup);
}
}
if let Some(backup) =
super::manifest::maybe_backup(&prompt_path, &agent.system_prompt).await
{
report.backups_created.push(backup);
}
match write_agent_to_dir(&agent, &agent_dir).await {
Ok(()) => {
info!(agent = %agent.id, dir = %agent_dir.display(), "Installed user agent");
report.agents_installed.push(agent.id.clone());
}
Err(e) => {
warn!(agent = %agent.id, error = %e, "Failed to install user agent");
report
.errors
.push(format!("user agent {}: {}", agent.id, e));
}
}
}
}
Ok(report)
}
fn hooks_toml_reference(defs: &ProjectHookDefs) -> String {
let mut toml = String::from(
"# Copy these hooks into your ~/.kimi/config.toml\n\
# or project .kimi/config.toml\n\n",
);
for hook in &defs.hooks {
toml.push_str("[[hooks]]\n");
toml.push_str(&format!("event = \"{:?}\"\n", hook.event));
toml.push_str(&format!("command = \"{}\"\n", hook.command));
if let Some(ref matcher) = hook.matcher {
toml.push_str(&format!("matcher = \"{}\"\n", matcher));
}
if let Some(timeout) = hook.timeout {
toml.push_str(&format!("timeout = {}\n", timeout));
}
toml.push('\n');
}
toml
}
#[derive(Debug, Clone, Default)]
pub struct InstallReport {
pub agents_installed: Vec<String>,
pub hooks_installed: Vec<String>,
pub skills_linked: bool,
pub errors: Vec<String>,
pub backups_created: Vec<String>,
pub would_install: Vec<String>,
}