use crate::config::YamlConfig;
use crate::constants::section;
use rust_embed::RustEmbed;
use serde::Deserialize;
use std::borrow::Cow;
use std::fs;
#[derive(Debug, Clone)]
pub struct HelpTab {
pub name: String,
pub content: String,
}
#[derive(Deserialize)]
struct HelpFrontmatter {
name: String,
order: u32,
}
#[derive(Debug, RustEmbed)]
#[folder = "assets/"]
#[exclude = "remote/node_modules/*"]
#[exclude = "remote/dist/*"]
pub struct Assets;
pub fn load_help_tabs() -> Vec<HelpTab> {
let mut tabs: Vec<(u32, HelpTab)> = Vec::new();
for filename in Assets::iter() {
let filename = filename.as_ref();
if !filename.starts_with("help/") || !filename.ends_with(".md") {
continue;
}
let asset = match Assets::get(filename) {
Some(a) => a,
None => continue,
};
let content = String::from_utf8_lossy(&asset.data);
if let Some((fm_str, body)) = split_help_frontmatter(&content)
&& let Ok(fm) = serde_yaml::from_str::<HelpFrontmatter>(&fm_str)
{
tabs.push((
fm.order,
HelpTab {
name: fm.name,
content: body,
},
));
}
}
tabs.sort_by_key(|(order, _)| *order);
tabs.into_iter().map(|(_, tab)| tab).collect()
}
fn split_help_frontmatter(content: &str) -> Option<(String, String)> {
let trimmed = content.trim_start();
if !trimmed.starts_with("---") {
return None;
}
let after_first = &trimmed[3..];
let end = after_first.find("\n---")?;
let fm = after_first[..end].trim().to_string();
let body = after_first[end + 4..].trim_start().to_string();
Some((fm, body))
}
pub fn version_template() -> Cow<'static, str> {
Assets::get("version.md")
.map(|f| String::from_utf8_lossy(&f.data).into_owned().into())
.unwrap_or_else(|| Cow::Borrowed(""))
}
pub fn default_system_prompt() -> Cow<'static, str> {
Assets::get("system_prompt_default.md")
.map(|f| String::from_utf8_lossy(&f.data).into_owned().into())
.unwrap_or_else(|| Cow::Borrowed(""))
}
pub fn default_memory() -> Cow<'static, str> {
Assets::get("memory_default.md")
.map(|f| String::from_utf8_lossy(&f.data).into_owned().into())
.unwrap_or_else(|| Cow::Borrowed(""))
}
pub fn default_soul() -> Cow<'static, str> {
Assets::get("soul_default.md")
.map(|f| String::from_utf8_lossy(&f.data).into_owned().into())
.unwrap_or_else(|| Cow::Borrowed(""))
}
pub fn default_agent_md() -> Cow<'static, str> {
Assets::get("agent_md_default.md")
.map(|f| String::from_utf8_lossy(&f.data).into_owned().into())
.unwrap_or_else(|| Cow::Borrowed(""))
}
pub fn quotes_text() -> &'static str {
include_str!("../assets/quotes.txt")
}
pub fn install_default_skills(skills_dir: &std::path::Path) -> Result<(), std::io::Error> {
for filename in Assets::iter() {
let filename = filename.as_ref();
if !filename.starts_with("skills/") {
continue;
}
let rel_path = &filename["skills/".len()..];
if rel_path.is_empty() {
continue;
}
let dst_path = skills_dir.join(rel_path);
if dst_path.exists() {
continue;
}
let asset = Assets::get(filename).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("asset not found: {}", filename),
)
})?;
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dst_path, asset.data)?;
}
Ok(())
}
pub fn install_default_commands(commands_dir: &std::path::Path) -> Result<(), std::io::Error> {
for filename in Assets::iter() {
let filename = filename.as_ref();
if !filename.starts_with("commands/") {
continue;
}
let rel_path = &filename["commands/".len()..];
if rel_path.is_empty() {
continue;
}
let dst_path = commands_dir.join(rel_path);
if dst_path.exists() {
continue;
}
let asset = Assets::get(filename).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("asset not found: {}", filename),
)
})?;
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dst_path, asset.data)?;
}
Ok(())
}
#[derive(Deserialize)]
struct PresetEntry {
name: String,
file: String,
}
pub fn install_default_scripts(config: &mut YamlConfig) -> Result<(), Box<dyn std::error::Error>> {
let manifest_asset = Assets::get("presets/manifest.yaml")
.ok_or("presets/manifest.yaml not found in embedded assets")?;
let manifest_str = std::str::from_utf8(&manifest_asset.data)?;
let entries: Vec<PresetEntry> = serde_yaml::from_str(manifest_str)?;
let scripts_dir = YamlConfig::scripts_dir();
for entry in &entries {
let dst_path = scripts_dir.join(&entry.file);
if dst_path.exists() {
continue;
}
let asset_path = format!("presets/{}", entry.file);
let asset = Assets::get(&asset_path)
.ok_or_else(|| format!("preset script not found in embedded assets: {}", asset_path))?;
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dst_path, &asset.data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
fs::set_permissions(&dst_path, perms)?;
}
let path_str = dst_path.to_string_lossy().to_string();
config.set_property(section::PATH, &entry.name, &path_str);
config.set_property(section::SCRIPT, &entry.name, &path_str);
}
Ok(())
}