use std::path::Path;
use anyhow::{bail, Context, Result};
use crate::cli::Target;
use crate::generate::{render_targets, GeneratedFileOutput};
use crate::{config::Config, introspect, types::Intent};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixAction {
RegenPluginJson,
}
#[derive(Debug, Clone, Default)]
pub struct FixOutcome {
pub files_written: Vec<String>,
}
impl FixOutcome {
pub fn is_empty(&self) -> bool {
self.files_written.is_empty()
}
pub fn len(&self) -> usize {
self.files_written.len()
}
pub fn unique_sorted(&self) -> Vec<String> {
let v: Vec<String> = self
.files_written
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
v
}
}
pub fn apply(action: FixAction, root: &Path) -> Result<FixOutcome> {
match action {
FixAction::RegenPluginJson => apply_regen_plugin_json(root),
}
}
fn apply_regen_plugin_json(root: &Path) -> Result<FixOutcome> {
let profile = introspect::introspect(root).context("introspecting repo for --fix")?;
let Some(cfg) = Config::load(root)? else {
bail!(
"no skillpack.toml at {} — `--fix` can only repair init-managed\n\
distribution files; run `skillpack init` to seed it first.",
root.display()
);
};
let Some(intent): Option<Intent> = cfg.to_intent() else {
bail!(
"skillpack.toml at {} has no `[skill]` block — cannot recover intent for --fix",
root.display()
);
};
let files = render_targets(&profile, &intent, &[Target::Claude])
.context("rendering claude target for --fix")?;
let plugin_json = files
.iter()
.find(|f| f.rel_path.ends_with("plugin.json"))
.cloned()
.ok_or_else(|| {
anyhow::anyhow!(
"claude target render produced no plugin.json — fix prerequisites not met"
)
})?;
write_one(root, &plugin_json)?;
Ok(FixOutcome {
files_written: vec![plugin_json.rel_path],
})
}
fn write_one(root: &Path, file: &GeneratedFileOutput) -> Result<()> {
let p = root.join(&file.rel_path);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating parent dir for {}", p.display()))?;
}
std::fs::write(&p, &file.contents)
.with_context(|| format!("writing {} for --fix", p.display()))?;
Ok(())
}
pub fn action_for(check_id: &str) -> Option<FixAction> {
match check_id {
"discovery.plugin.version_drift" => Some(FixAction::RegenPluginJson),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_for_maps_known_drift() {
assert_eq!(
action_for("discovery.plugin.version_drift"),
Some(FixAction::RegenPluginJson)
);
assert_eq!(action_for("discovery.description"), None);
assert_eq!(action_for("invocation.help_present"), None);
}
#[test]
fn apply_match_is_exhaustive_over_enum() {
let action = FixAction::RegenPluginJson;
let _ = action_for("¬-real"); let _ = action;
}
#[test]
fn fixoutcome_unique_sorted_dedupes() {
let o = FixOutcome {
files_written: vec!["b.json".into(), "a.json".into(), "b.json".into()],
};
assert_eq!(
o.unique_sorted(),
vec!["a.json".to_string(), "b.json".to_string()]
);
}
}