pub mod templates;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use crate::cli::{SkillArgs, SkillCommand, SkillTarget};
use templates::Flavor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Generate,
Install,
}
impl Mode {
fn label(self) -> &'static str {
match self {
Mode::Generate => "generate",
Mode::Install => "install",
}
}
}
struct Artifact {
path: PathBuf,
content: Content,
}
enum Content {
Text(String),
AgentsBlock(String),
}
pub fn execute(command: SkillCommand, root: &clap::Command) -> Result<()> {
let (mode, args) = match command {
SkillCommand::Generate(args) => (Mode::Generate, args),
SkillCommand::Install(args) => (Mode::Install, args),
};
let targets = expand_targets(&args.target);
let command_list = templates::command_list(root);
let mut artifacts = Vec::new();
for target in targets {
artifacts.extend(plan_target(target, mode, &args, &command_list)?);
}
let mut written = Vec::new();
for artifact in &artifacts {
write_artifact(artifact, args.force)?;
written.push(artifact.path.display().to_string());
}
render_summary(args.json, mode, &written);
Ok(())
}
fn expand_targets(requested: &[SkillTarget]) -> Vec<SkillTarget> {
let mut targets = Vec::new();
let push = |target: SkillTarget, targets: &mut Vec<SkillTarget>| {
if !targets.contains(&target) {
targets.push(target);
}
};
for &target in requested {
match target {
SkillTarget::All => {
push(SkillTarget::ClaudeCode, &mut targets);
push(SkillTarget::Codex, &mut targets);
push(SkillTarget::Pi, &mut targets);
push(SkillTarget::OpenClaw, &mut targets);
push(SkillTarget::AgentsMd, &mut targets);
}
other => push(other, &mut targets),
}
}
targets
}
fn plan_target(
target: SkillTarget,
mode: Mode,
args: &SkillArgs,
command_list: &str,
) -> Result<Vec<Artifact>> {
match target {
SkillTarget::ClaudeCode => {
let base = base_dir(mode, args, ".claude", ".claude")?;
Ok(vec![Artifact {
path: base.join("skills").join("claudex").join("SKILL.md"),
content: Content::Text(templates::skill_md(Flavor::ClaudeCode, command_list)),
}])
}
SkillTarget::Codex => {
let base = base_dir(mode, args, ".agents", ".agents")?;
Ok(vec![Artifact {
path: base.join("skills").join("claudex").join("SKILL.md"),
content: Content::Text(templates::skill_md(Flavor::Codex, command_list)),
}])
}
SkillTarget::Pi => {
let base = base_dir(mode, args, ".pi", ".pi")?;
Ok(vec![Artifact {
path: base.join("skills").join("claudex").join("SKILL.md"),
content: Content::Text(templates::skill_md(Flavor::Pi, command_list)),
}])
}
SkillTarget::OpenClaw => {
let base = openclaw_base_dir(mode, args)?;
Ok(vec![Artifact {
path: base.join("skills").join("claudex").join("SKILL.md"),
content: Content::Text(templates::skill_md(Flavor::OpenClaw, command_list)),
}])
}
SkillTarget::AgentsMd => {
let base = base_dir(mode, args, "", ".codex")?;
Ok(vec![Artifact {
path: base.join("AGENTS.md"),
content: Content::AgentsBlock(templates::agents_block(command_list)),
}])
}
SkillTarget::Plugin => {
let base = bundle_base(mode, args).join("claudex");
Ok(vec![
Artifact {
path: base.join(".claude-plugin").join("plugin.json"),
content: Content::Text(templates::plugin_json()),
},
Artifact {
path: base.join("skills").join("claudex").join("SKILL.md"),
content: Content::Text(templates::skill_md(Flavor::ClaudeCode, command_list)),
},
])
}
SkillTarget::All => unreachable!("expand_targets removes All"),
}
}
fn base_dir(
mode: Mode,
args: &SkillArgs,
project_prefix: &str,
global_prefix: &str,
) -> Result<PathBuf> {
match mode {
Mode::Generate => {
let root = args
.dir
.clone()
.unwrap_or_else(|| PathBuf::from("claudex-skills"));
Ok(join_prefix(root, project_prefix))
}
Mode::Install if args.global => {
let root = match &args.dir {
Some(dir) => dir.clone(),
None => home_dir()?,
};
Ok(join_prefix(root, global_prefix))
}
Mode::Install => {
let root = args.dir.clone().unwrap_or_else(|| PathBuf::from("."));
Ok(join_prefix(root, project_prefix))
}
}
}
fn bundle_base(mode: Mode, args: &SkillArgs) -> PathBuf {
let default = match mode {
Mode::Generate => PathBuf::from("claudex-skills"),
Mode::Install => PathBuf::from("."),
};
args.dir.clone().unwrap_or(default)
}
fn openclaw_base_dir(mode: Mode, args: &SkillArgs) -> Result<PathBuf> {
match mode {
Mode::Generate => Ok(args
.dir
.clone()
.unwrap_or_else(|| PathBuf::from("claudex-skills"))),
Mode::Install if args.global => {
if let Some(dir) = &args.dir {
return Ok(dir.clone());
}
if let Ok(state_dir) = std::env::var("OPENCLAW_STATE_DIR")
&& !state_dir.trim().is_empty()
{
return expand_home(state_dir.trim());
}
Ok(home_dir()?.join(".openclaw"))
}
Mode::Install => Ok(args.dir.clone().unwrap_or_else(|| PathBuf::from("."))),
}
}
fn join_prefix(root: PathBuf, prefix: &str) -> PathBuf {
if prefix.is_empty() {
root
} else {
root.join(prefix)
}
}
fn home_dir() -> Result<PathBuf> {
dirs::home_dir().context("could not determine your home directory for --global install")
}
fn expand_home(value: &str) -> Result<PathBuf> {
if let Some(rest) = value.strip_prefix("~/") {
Ok(home_dir()?.join(rest))
} else if value == "~" {
home_dir()
} else {
Ok(PathBuf::from(value))
}
}
fn write_artifact(artifact: &Artifact, force: bool) -> Result<()> {
if let Some(parent) = artifact.path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)
.with_context(|| format!("could not create {}", parent.display()))?;
}
match &artifact.content {
Content::Text(text) => write_new_file(&artifact.path, text.as_bytes(), force),
Content::AgentsBlock(block) => splice_agents(&artifact.path, block),
}
}
fn write_new_file(path: &Path, bytes: &[u8], force: bool) -> Result<()> {
if path.exists() && !force {
bail!(
"{} already exists (use --force to overwrite)",
path.display()
);
}
fs::write(path, bytes).with_context(|| format!("could not write {}", path.display()))
}
fn splice_agents(path: &Path, block: &str) -> Result<()> {
let existing = fs::read_to_string(path).unwrap_or_default();
let updated = match (
existing.find(templates::AGENTS_START),
existing.find(templates::AGENTS_END),
) {
(Some(start), Some(end)) if end > start => {
let end = end + templates::AGENTS_END.len();
format!("{}{}{}", &existing[..start], block, &existing[end..])
}
_ if existing.trim().is_empty() => format!("{block}\n"),
_ => format!("{}\n\n{block}\n", existing.trim_end()),
};
fs::write(path, updated).with_context(|| format!("could not write {}", path.display()))
}
fn render_summary(json: bool, mode: Mode, written: &[String]) {
let hint = (mode == Mode::Generate).then(install_hint);
if json {
let mut payload = json!({ "mode": mode.label(), "written": written });
if let Some(hint) = &hint {
payload["hint"] = Value::String(hint.clone());
}
println!(
"{}",
serde_json::to_string_pretty(&payload).unwrap_or_default()
);
return;
}
println!("Wrote {} file(s) ({}):", written.len(), mode.label());
for path in written {
println!(" {path}");
}
if let Some(hint) = &hint {
println!();
println!("{hint}");
}
}
fn install_hint() -> String {
"These files are for review. Activate the skill with `claudex skills install` \
(add --global for user-level, or --target to choose a harness), or copy them \
into your harness configuration directories manually."
.to_string()
}