use std::collections::BTreeSet;
use callisto_model::{ApplyPermit, CommandRunner, Ecosystem, InitDiff, InitReport, SCHEMA_VERSION};
use crate::config::raw::RawConfig;
use crate::error::{ConfigError, GraphError};
use crate::resolver::DependencyResolver;
use crate::Workspace;
#[derive(Clone, Debug, Default)]
pub struct InitOptions {
pub yes: bool,
}
fn io_err(e: std::io::Error) -> GraphError {
GraphError::Command(callisto_model::CommandError::Io {
program: "fs".to_string(),
message: e.to_string(),
})
}
fn discovered_ecosystems<D: DependencyResolver>(graph: &D) -> BTreeSet<Ecosystem> {
graph
.packages()
.flat_map(|p| p.canonical_manifests())
.map(|m| m.ecosystem())
.collect()
}
fn recorded_ecosystems(content: &str) -> BTreeSet<Ecosystem> {
toml::from_str::<RawConfig>(content)
.ok()
.and_then(|raw| raw.init)
.and_then(|init| init.ecosystems)
.unwrap_or_default()
.iter()
.filter_map(|s| Ecosystem::from_prefix(s))
.collect()
}
fn render_ecosystems_array(ecosystems: &BTreeSet<Ecosystem>) -> String {
let items: Vec<String> = ecosystems.iter().map(|e| format!("\"{}\"", e.prefix())).collect();
format!("[{}]", items.join(", "))
}
fn set_recorded_ecosystems(content: &str, ecosystems: &BTreeSet<Ecosystem>) -> Result<String, GraphError> {
let mut doc: toml_edit::DocumentMut = content.parse().map_err(|e: toml_edit::TomlError| {
GraphError::Config(ConfigError::ParseToml {
path: "callisto.toml".into(),
message: e.to_string(),
})
})?;
if doc.get("init").and_then(|i| i.as_table()).is_none() {
doc["init"] = toml_edit::Item::Table(toml_edit::Table::new());
}
let mut array = toml_edit::Array::new();
for e in ecosystems {
array.push(e.prefix());
}
doc["init"]["ecosystems"] = toml_edit::value(array);
Ok(doc.to_string())
}
pub fn init<R: CommandRunner, D: DependencyResolver>(
ws: &Workspace<'_, R, D>,
opts: &InitOptions,
permit: Option<&ApplyPermit>,
) -> Result<InitReport, GraphError> {
let config_path = ws.root.join("callisto.toml");
let discovered = discovered_ecosystems(&ws.graph);
let mut initialized = false;
let mut diff = InitDiff::default();
if !config_path.exists() {
let content = format!(
"# callisto configuration\n\n[changesets]\ndir = \".changeset\"\n\n\
[cascade]\nmode = \"out-of-range\"\nbump-severity = \"patch\"\npeer-escalation = true\npreserve-npm-ranges = true\n\n\
[init]\necosystems = {}\n",
render_ecosystems_array(&discovered)
);
if let Some(permit) = permit {
callisto_manifests::atomic::atomic_write(&config_path, &content, permit).map_err(io_err)?;
}
initialized = true;
} else {
let existing = std::fs::read_to_string(&config_path).map_err(|e| {
GraphError::Config(ConfigError::Read {
path: config_path.clone(),
message: e.to_string(),
})
})?;
let recorded = recorded_ecosystems(&existing);
let new_ecosystems: Vec<Ecosystem> = discovered.difference(&recorded).copied().collect();
if !new_ecosystems.is_empty() {
diff.new_ecosystems = new_ecosystems;
if opts.yes {
let reconciled: BTreeSet<Ecosystem> = recorded.union(&discovered).copied().collect();
let updated = set_recorded_ecosystems(&existing, &reconciled)?;
if let Some(permit) = permit {
callisto_manifests::atomic::atomic_write(&config_path, &updated, permit).map_err(io_err)?;
}
diff.applied = true;
}
}
}
let cs_dir = ws.root.join(".changeset");
if let Some(permit) = permit {
if !cs_dir.exists() {
std::fs::create_dir_all(&cs_dir).map_err(io_err)?;
}
let readme_path = cs_dir.join("README.md");
if !readme_path.exists() {
let readme_content = r#"# Changesets
This directory contains markdown changeset files generated by Callisto CLI (`callisto add`).
Each changeset describes a package version bump and associated release summary.
"#;
callisto_manifests::atomic::atomic_write(&readme_path, readme_content, permit).map_err(io_err)?;
}
}
Ok(InitReport {
schema_version: SCHEMA_VERSION,
initialized,
config_path,
diff,
diagnostics: Vec::new(),
})
}