use std::collections::HashSet;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use serde::Serialize;
use tempfile::NamedTempFile;
use crate::error::CliError;
use crate::output::{self, OutputFormat, OutputSpec};
struct EmbeddedSkill {
name: &'static str,
body: &'static str,
path_in_repo: &'static str,
}
struct EmbeddedResource {
filename: &'static str,
body: &'static str,
}
fn resources_for(name: &str) -> &'static [EmbeddedResource] {
match name {
"stint-start" => STINT_START_RESOURCES,
_ => &[],
}
}
const STINT_START_RESOURCES: &[EmbeddedResource] = &[EmbeddedResource {
filename: "AGENTS-EXECUTION-DAG.md",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/stint-start/AGENTS-EXECUTION-DAG.md"
)),
}];
const SKILLS: &[EmbeddedSkill] = &[
EmbeddedSkill {
name: "orchestratectl-overview",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/orchestratectl-overview/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/orchestratectl-overview/SKILL.template.md",
},
EmbeddedSkill {
name: "octl-run-overview",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/octl-run-overview/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/octl-run-overview/SKILL.template.md",
},
EmbeddedSkill {
name: "octl-spawn-spinoff",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/octl-spawn-spinoff/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/octl-spawn-spinoff/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-spinoff",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-spinoff/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-spinoff/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-code",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/worktree-code/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/worktree-code/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-merge",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/worktree-merge/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/worktree-merge/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-orchestrated",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-orchestrated/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-orchestrated/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-research",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-research/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-research/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-make-skill",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-make-skill/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-make-skill/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-bugfix",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/worktree-bugfix/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/worktree-bugfix/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-technical-decision",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-technical-decision/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-technical-decision/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-bug-analysis",
body: include_str!(concat!(
env!("OUT_DIR"),
"/skills/worktree-bug-analysis/SKILL.md"
)),
path_in_repo: "crates/octl-cli/skills/worktree-bug-analysis/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/worktree/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/worktree/SKILL.template.md",
},
EmbeddedSkill {
name: "worktree-status",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/worktree-status/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/worktree-status/SKILL.template.md",
},
EmbeddedSkill {
name: "stint-start",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/stint-start/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/stint-start/SKILL.template.md",
},
EmbeddedSkill {
name: "stint-handoff",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/stint-handoff/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/stint-handoff/SKILL.template.md",
},
EmbeddedSkill {
name: "fan-out",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/fan-out/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/fan-out/SKILL.template.md",
},
EmbeddedSkill {
name: "orchestrate",
body: include_str!(concat!(env!("OUT_DIR"), "/skills/orchestrate/SKILL.md")),
path_in_repo: "crates/octl-cli/skills/orchestrate/SKILL.template.md",
},
];
const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
const SKILL_SCHEMA_VERSION: u32 = 1;
const MANAGED_MARKER_FILENAME: &str = ".orchestratectl-managed";
#[derive(Debug, Serialize)]
pub struct SkillCatalogEntry {
pub name: &'static str,
pub cli_version: String,
pub schema_version: u32,
}
pub fn catalog() -> Vec<SkillCatalogEntry> {
SKILLS
.iter()
.map(|s| SkillCatalogEntry {
name: s.name,
cli_version: parse_frontmatter_field(s.body, "cli_version")
.unwrap_or_else(|| CLI_VERSION.to_string()),
schema_version: parse_frontmatter_field(s.body, "schema_version")
.and_then(|v| v.parse().ok())
.unwrap_or(SKILL_SCHEMA_VERSION),
})
.collect()
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum AgentTarget {
Claude,
Codex,
All,
}
pub fn bundled_skill_names() -> Vec<&'static str> {
SKILLS.iter().map(|s| s.name).collect()
}
pub fn binary_cli_version() -> &'static str {
CLI_VERSION
}
pub fn claude_default_path(name: &str) -> Option<PathBuf> {
default_path("claude", name).ok()
}
pub fn read_on_disk_cli_version(path: &Path) -> Option<String> {
let body = fs::read_to_string(path).ok()?;
parse_frontmatter_field(&body, "cli_version")
}
pub fn cli_version_of(body: &str) -> Option<String> {
parse_frontmatter_field(body, "cli_version")
}
pub struct CompanionSource {
pub filename: &'static str,
pub bundled_body: &'static str,
}
pub fn companion_sources(name: &str) -> Vec<CompanionSource> {
resources_for(name)
.iter()
.map(|r| CompanionSource {
filename: r.filename,
bundled_body: r.body,
})
.collect()
}
#[derive(Serialize)]
struct SkillSummary {
name: &'static str,
description: String,
}
#[derive(Serialize)]
struct ListPayload {
skills: Vec<SkillSummary>,
}
#[derive(Serialize)]
struct InstallPayload {
installed: Vec<InstalledFile>,
pruned: Vec<String>,
}
#[derive(Serialize)]
struct InstalledFile {
name: &'static str,
agent: &'static str,
path: String,
}
pub fn cmd_list(spec: &OutputSpec, warnings: &[String]) -> Result<(), CliError> {
let skills: Vec<SkillSummary> = SKILLS
.iter()
.map(|s| SkillSummary {
name: s.name,
description: parse_description(s.body).unwrap_or_default(),
})
.collect();
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
output::emit_envelope(&ListPayload { skills }, spec, warnings)?;
}
OutputFormat::Text => {
for s in &skills {
println!("{}\t{}", s.name, s.description);
}
output::emit_text_warnings(warnings);
}
}
Ok(())
}
pub fn cmd_show(name: &str, spec: &OutputSpec, warnings: &[String]) -> Result<(), CliError> {
let skill = lookup(name)?;
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
#[derive(Serialize)]
struct ShowPayload<'a> {
name: &'a str,
content: &'a str,
}
output::emit_envelope(
&ShowPayload {
name: skill.name,
content: skill.body,
},
spec,
warnings,
)?;
}
OutputFormat::Text => {
print!("{}", skill.body);
if !skill.body.ends_with('\n') {
println!();
}
output::emit_text_warnings(warnings);
}
}
Ok(())
}
pub fn cmd_print(name: &str, spec: &OutputSpec, warnings: &[String]) -> Result<(), CliError> {
let skill = lookup(name)?;
let cli_version = parse_frontmatter_field(skill.body, "cli_version")
.unwrap_or_else(|| CLI_VERSION.to_string());
let schema_version_skill = parse_frontmatter_field(skill.body, "schema_version")
.and_then(|v| v.parse().ok())
.unwrap_or(SKILL_SCHEMA_VERSION);
match spec.format {
OutputFormat::Json => {
#[derive(Serialize)]
struct PrintPayload<'a> {
schema_version: u32,
name: &'a str,
cli_version: &'a str,
schema_version_skill: u32,
content: &'a str,
path_in_repo: &'a str,
}
output::emit_envelope(
&PrintPayload {
schema_version: SKILL_SCHEMA_VERSION,
name: skill.name,
cli_version: &cli_version,
schema_version_skill,
content: skill.body,
path_in_repo: skill.path_in_repo,
},
spec,
warnings,
)?;
}
OutputFormat::Text | OutputFormat::Jsonl => {
use std::io::Write as _;
let mut out = std::io::stdout().lock();
out.write_all(skill.body.as_bytes())
.map_err(|e| CliError::system("io_error", format!("write stdout: {e}")))?;
out.flush()
.map_err(|e| CliError::system("io_error", format!("flush stdout: {e}")))?;
output::emit_text_warnings(warnings);
}
}
Ok(())
}
pub fn cmd_install(
name: Option<&str>,
agent: AgentTarget,
dest: Option<PathBuf>,
force: bool,
spec: &OutputSpec,
warnings: &[String],
) -> Result<(), CliError> {
let skills: Vec<&'static EmbeddedSkill> = match name {
Some(n) => vec![lookup(n)?],
None => SKILLS.iter().collect(),
};
if dest.is_some() && matches!(agent, AgentTarget::All) {
return Err(CliError::user(
"invalid_arguments",
"--dest cannot be combined with --agent all",
));
}
if dest.is_some() && skills.len() > 1 {
return Err(CliError::user(
"invalid_arguments",
"--dest requires a skill name; omit --dest to install all skills",
));
}
let mut plan: Vec<PlanItem> = Vec::new();
let mut mark_dirs: Vec<(&'static str, PathBuf)> = Vec::new();
for skill in &skills {
let targets: Vec<(&'static str, PathBuf)> = match (&agent, dest.as_ref()) {
(AgentTarget::Claude, Some(p)) => vec![("claude", p.clone())],
(AgentTarget::Codex, Some(p)) => vec![("codex", p.clone())],
(AgentTarget::Claude, None) => vec![("claude", default_path("claude", skill.name)?)],
(AgentTarget::Codex, None) => vec![("codex", default_path("codex", skill.name)?)],
(AgentTarget::All, _) => vec![
("claude", default_path("claude", skill.name)?),
("codex", default_path("codex", skill.name)?),
],
};
for (agent_name, path) in targets {
if agent_name == "claude" {
if dest.is_none() {
if let Some(parent) = path.parent() {
mark_dirs.push((skill.name, parent.to_path_buf()));
}
}
for resource in resources_for(skill.name) {
plan.push(PlanItem {
name: resource.filename,
agent: agent_name,
path: sibling_path(&path, resource.filename),
content: resource.body,
});
}
}
plan.push(PlanItem {
name: skill.name,
agent: agent_name,
path,
content: skill.body,
});
}
}
let preflight_result = preflight(&plan, force)?;
let mut all_warnings: Vec<String> = warnings.to_vec();
all_warnings.extend(preflight_result.warnings);
let mut installed = Vec::with_capacity(plan.len());
for item in plan {
let allow_overwrite = preflight_result.overwrite_allowed.contains(&item.path);
write_atomic(&item.path, item.content, allow_overwrite)?;
installed.push(InstalledFile {
name: item.name,
agent: item.agent,
path: item.path.display().to_string(),
});
}
mark_dirs.sort();
mark_dirs.dedup();
for (skill_name, dir) in &mark_dirs {
write_marker(&dir.join(MANAGED_MARKER_FILENAME), skill_name)?;
}
let mut pruned: Vec<String> = Vec::new();
let prune_eligible = name.is_none()
&& dest.is_none()
&& force
&& matches!(agent, AgentTarget::Claude | AgentTarget::All);
if prune_eligible {
if let Some(root) = claude_skills_root() {
let registered: HashSet<&str> = SKILLS.iter().map(|s| s.name).collect();
for (orphan_name, orphan_path) in managed_orphan_dirs(&root, ®istered) {
match fs::remove_dir_all(&orphan_path) {
Ok(()) => {
all_warnings.push(format!(
"skill_pruned: removed de-registered managed skill '{orphan_name}' at {}",
orphan_path.display()
));
pruned.push(orphan_name);
}
Err(e) => {
all_warnings.push(format!(
"skill_prune_failed: could not remove de-registered skill '{orphan_name}' at {}: {e}",
orphan_path.display()
));
}
}
}
}
}
let payload = InstallPayload { installed, pruned };
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
output::emit_envelope(&payload, spec, &all_warnings)?;
}
OutputFormat::Text => {
for f in &payload.installed {
println!("installed {} ({}) -> {}", f.name, f.agent, f.path);
}
for name in &payload.pruned {
println!("pruned {name} (de-registered)");
}
output::emit_text_warnings(&all_warnings);
}
}
Ok(())
}
fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
let av = semver::Version::parse(a).ok()?;
let bv = semver::Version::parse(b).ok()?;
Some(av.cmp(&bv))
}
struct PreflightResult {
warnings: Vec<String>,
overwrite_allowed: HashSet<PathBuf>,
}
struct PlanItem {
name: &'static str,
agent: &'static str,
path: PathBuf,
content: &'static str,
}
fn sibling_path(skill_path: &Path, filename: &str) -> PathBuf {
match skill_path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.join(filename),
_ => PathBuf::from(filename),
}
}
fn preflight(plan: &[PlanItem], force: bool) -> Result<PreflightResult, CliError> {
use std::cmp::Ordering;
let mut seen: HashSet<&Path> = HashSet::new();
let mut warnings: Vec<String> = Vec::new();
let mut overwrite_allowed: HashSet<PathBuf> = HashSet::new();
for PlanItem { name, path, .. } in plan {
if !seen.insert(path.as_path()) {
return Err(CliError::user(
"duplicate_destination",
format!("destination appears more than once: {}", path.display()),
)
.with_invalid_value(path.display().to_string()));
}
if path.is_dir() {
return Err(CliError::user(
"invalid_dest",
format!("destination is a directory: {}", path.display()),
)
.with_invalid_value(path.display().to_string()));
}
if !path.exists() {
continue;
}
let on_disk_raw = fs::read_to_string(path)
.ok()
.and_then(|s| parse_frontmatter_field(&s, "cli_version"));
let drift = on_disk_raw
.as_deref()
.and_then(|v| compare_versions(v, CLI_VERSION).map(|ord| (v, ord)));
match drift {
Some((v, Ordering::Less)) => {
overwrite_allowed.insert(path.clone());
warnings.push(format!(
"skill_version_drift: {name} on disk is {v}; binary ships {CLI_VERSION}; overwriting"
));
}
Some((v, Ordering::Greater)) => {
if !force {
return Err(CliError::system(
"skill_version_too_new",
format!(
"{}: on-disk skill is cli_version {} but binary is {}; pass --force to overwrite anyway",
path.display(),
v,
CLI_VERSION
),
)
.with_invalid_value(path.display().to_string()));
}
overwrite_allowed.insert(path.clone());
warnings.push(format!(
"skill_version_drift: {name} on disk is {v} (newer than binary {CLI_VERSION}); --force overwriting"
));
}
Some((_, Ordering::Equal)) | None => {
if !force {
return Err(CliError::system(
"refused_overwrite",
format!(
"{} already exists; pass --force to overwrite",
path.display()
),
)
.with_invalid_value(path.display().to_string()));
}
overwrite_allowed.insert(path.clone());
}
}
}
Ok(PreflightResult {
warnings,
overwrite_allowed,
})
}
fn lookup(name: &str) -> Result<&'static EmbeddedSkill, CliError> {
SKILLS.iter().find(|s| s.name == name).ok_or_else(|| {
let available: Vec<&str> = SKILLS.iter().map(|s| s.name).collect();
CliError::user(
"skill_not_found",
format!(
"no skill named '{}'; available: {}",
name,
available.join(", ")
),
)
.with_invalid_value(name.to_string())
.with_expected(serde_json::json!({ "one_of": available }))
})
}
fn default_path(agent: &str, name: &str) -> Result<PathBuf, CliError> {
let home = std::env::var("HOME").map_err(|_| {
CliError::system(
"home_unset",
"HOME is not set; cannot resolve default install path (pass --dest)",
)
})?;
let base = PathBuf::from(home);
Ok(match agent {
"claude" => base.join(".claude/skills").join(name).join("SKILL.md"),
"codex" => base.join(".codex/prompts").join(format!("{name}.md")),
other => {
return Err(CliError::user(
"invalid_agent",
format!("unknown agent '{other}'"),
))
}
})
}
pub fn claude_skills_root() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
Some(PathBuf::from(home).join(".claude/skills"))
}
fn is_managed_skill_dir(dir: &Path) -> bool {
let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) else {
return false;
};
let marker = dir.join(MANAGED_MARKER_FILENAME);
let Ok(meta) = fs::symlink_metadata(&marker) else {
return false;
};
if !meta.file_type().is_file() {
return false;
}
let Ok(content) = fs::read_to_string(&marker) else {
return false;
};
let mut has_stamp = false;
let mut name_matches = false;
for line in content.lines() {
let line = line.trim();
if line == "managed-by: orchestratectl" {
has_stamp = true;
} else if let Some(rest) = line.strip_prefix("skill_name:") {
name_matches = rest.trim() == dir_name;
}
}
has_stamp && name_matches
}
fn write_marker(path: &Path, skill_name: &str) -> Result<(), CliError> {
if let Ok(meta) = fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
fs::remove_file(path).map_err(|e| {
CliError::system(
"marker_write_failed",
format!(
"could not clear stale marker symlink {}: {}",
path.display(),
e
),
)
})?;
}
}
fs::write(
path,
format!(
"managed-by: orchestratectl\ncli_version: {CLI_VERSION}\nskill_name: {skill_name}\n"
),
)
.map_err(|e| {
CliError::system(
"marker_write_failed",
format!(
"could not write provenance marker {}: {}",
path.display(),
e
),
)
})
}
fn managed_orphan_dirs(skills_root: &Path, registered: &HashSet<&str>) -> Vec<(String, PathBuf)> {
let Ok(entries) = fs::read_dir(skills_root) else {
return Vec::new();
};
let mut orphans: Vec<(String, PathBuf)> = Vec::new();
for entry in entries {
let Ok(entry) = entry else { continue };
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_dir() || file_type.is_symlink() {
continue;
}
let path = entry.path();
let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if registered
.iter()
.any(|r| *r == dir_name || r.eq_ignore_ascii_case(dir_name))
{
continue;
}
if is_managed_skill_dir(&path) {
orphans.push((dir_name.to_string(), path.clone()));
}
}
orphans.sort();
orphans
}
pub fn managed_orphans() -> Vec<(String, PathBuf)> {
let Some(root) = claude_skills_root() else {
return Vec::new();
};
let registered: HashSet<&str> = SKILLS.iter().map(|s| s.name).collect();
managed_orphan_dirs(&root, ®istered)
}
fn normalized_parent(path: &Path) -> Option<&Path> {
match path.parent() {
Some(p) if p.as_os_str().is_empty() => Some(Path::new(".")),
Some(p) => Some(p),
None => None,
}
}
fn write_atomic(path: &Path, content: &str, force: bool) -> Result<(), CliError> {
let parent = normalized_parent(path).ok_or_else(|| {
CliError::user(
"invalid_dest",
format!("destination has no parent directory: {}", path.display()),
)
})?;
fs::create_dir_all(parent).map_err(|e| {
CliError::system(
"create_dir_failed",
format!("could not create {}: {}", parent.display(), e),
)
})?;
let mut tmp = NamedTempFile::new_in(parent).map_err(|e| {
CliError::system(
"tempfile_failed",
format!("could not create tempfile in {}: {}", parent.display(), e),
)
})?;
tmp.write_all(content.as_bytes())
.map_err(|e| CliError::system("write_failed", format!("could not write tempfile: {e}")))?;
tmp.as_file_mut()
.sync_all()
.map_err(|e| CliError::system("fsync_failed", format!("could not fsync tempfile: {e}")))?;
let persist_result = if force {
tmp.persist(path).map(|_| ())
} else {
tmp.persist_noclobber(path).map(|_| ())
};
persist_result.map_err(|e| {
let kind = e.error.kind();
if !force && kind == std::io::ErrorKind::AlreadyExists {
CliError::system(
"refused_overwrite",
format!(
"{} already exists; pass --force to overwrite",
path.display()
),
)
.with_invalid_value(path.display().to_string())
} else {
CliError::system(
"rename_failed",
format!("could not rename into place {}: {}", path.display(), e),
)
}
})
}
fn parse_description(body: &str) -> Option<String> {
parse_frontmatter_field(body, "description")
}
fn parse_frontmatter_field(body: &str, field: &str) -> Option<String> {
let body = body.strip_prefix('\u{feff}').unwrap_or(body);
let mut lines = body.lines();
if lines.next()?.trim_end() != "---" {
return None;
}
for line in lines {
if line.trim_end() == "---" {
return None;
}
let Some((key, value)) = line.split_once(':') else {
continue;
};
if key.trim() == field {
let v = value.trim();
let v = v
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| v.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(v);
return Some(v.to_string());
}
}
None
}
#[cfg(test)]
fn parse_name(body: &str) -> Option<String> {
parse_frontmatter_field(body, "name")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_embedded_skill_has_a_description_and_matching_name() {
for s in SKILLS {
let d = parse_description(s.body)
.unwrap_or_else(|| panic!("skill {} missing description", s.name));
assert!(!d.is_empty(), "skill {} has empty description", s.name);
let n = parse_name(s.body).unwrap_or_else(|| panic!("skill {} missing name", s.name));
assert_eq!(
n, s.name,
"catalog name {:?} does not match frontmatter name {:?}",
s.name, n
);
let cli = parse_frontmatter_field(s.body, "cli_version")
.unwrap_or_else(|| panic!("skill {} missing cli_version", s.name));
assert_eq!(
cli, CLI_VERSION,
"skill {} cli_version {:?} does not match binary {:?}",
s.name, cli, CLI_VERSION
);
let schema = parse_frontmatter_field(s.body, "schema_version")
.unwrap_or_else(|| panic!("skill {} missing schema_version", s.name));
let parsed: u32 = schema.parse().unwrap_or_else(|_| {
panic!(
"skill {} has unparseable schema_version {:?}",
s.name, schema
)
});
assert_eq!(
parsed, SKILL_SCHEMA_VERSION,
"skill {} schema_version {} != {}",
s.name, parsed, SKILL_SCHEMA_VERSION
);
assert!(
!s.body.contains("{{CLI_VERSION}}"),
"skill {} still contains unrendered {{{{CLI_VERSION}}}} placeholder",
s.name
);
}
}
#[test]
fn every_companion_resource_is_rendered_and_version_pinned() {
for name in SKILLS.iter().map(|s| s.name) {
for r in resources_for(name) {
assert!(
!r.body.contains("{{CLI_VERSION}}"),
"companion {} for skill {} still contains an unrendered {{{{CLI_VERSION}}}} placeholder",
r.filename,
name
);
if let Some(v) = parse_frontmatter_field(r.body, "cli_version") {
assert_eq!(
v, CLI_VERSION,
"companion {} for skill {} declares cli_version {:?}, binary is {:?}",
r.filename, name, v, CLI_VERSION
);
}
}
}
}
#[test]
fn compare_versions_handles_semver_ordering() {
use std::cmp::Ordering;
assert_eq!(
compare_versions("1.0.0-alpha", "1.0.0"),
Some(Ordering::Less)
);
assert_eq!(
compare_versions("1.0.0", "1.0.0-alpha"),
Some(Ordering::Greater)
);
assert_eq!(compare_versions("1.10.0", "1.9.0"), Some(Ordering::Greater));
assert_eq!(compare_versions("0.0.1", "0.0.1"), Some(Ordering::Equal));
assert_eq!(compare_versions("banana", "1.0.0"), None);
assert_eq!(compare_versions("1.0.0", "1.x"), None);
assert_eq!(compare_versions("{{CLI_VERSION}}", "1.0.0"), None);
}
#[test]
fn parse_description_extracts_value() {
let body = "---\nname: foo\ndescription: a short blurb\nversion: 1\n---\n\n# body\n";
assert_eq!(parse_description(body).as_deref(), Some("a short blurb"));
}
#[test]
fn parse_description_handles_crlf() {
let body = "---\r\nname: foo\r\ndescription: blurb\r\n---\r\n";
assert_eq!(parse_description(body).as_deref(), Some("blurb"));
}
#[test]
fn parse_description_strips_quotes() {
let body = "---\ndescription: \"quoted blurb\"\n---\n";
assert_eq!(parse_description(body).as_deref(), Some("quoted blurb"));
}
#[test]
fn parse_description_returns_none_without_frontmatter() {
assert_eq!(parse_description("# just a heading\n"), None);
}
#[test]
fn parse_description_returns_none_when_field_absent() {
let body = "---\nname: foo\nversion: 1\n---\n";
assert_eq!(parse_description(body), None);
}
}