pub mod apply;
pub mod consent;
pub mod discover;
pub mod error;
pub mod extract;
pub mod parse;
pub mod types;
pub use apply::{apply, apply_and_save, plan};
pub use consent::ConsentStore;
pub use discover::{KNOWN_SOURCES, scan, scan_with_home, why_config_missing};
pub use error::{ImportError, Result};
pub use extract::extract;
pub use types::{
Extracted, FoundSource, ImportItem, ImportItemKind, ImportPlan, Product, SourceState,
};
pub fn preview(
sources: &[FoundSource],
config: &whycodes_config::Config,
force: bool,
) -> Result<(Vec<Extracted>, ImportPlan)> {
let mut extracted = Vec::new();
for src in sources {
match src.state {
SourceState::Approved => extracted.push(extract(src)?),
SourceState::New | SourceState::Denied | SourceState::Symlink => {}
}
}
let plan = plan(config, &extracted, force);
Ok((extracted, plan))
}
pub fn has_discoverable_sources(home: &std::path::Path, consent: &ConsentStore) -> bool {
scan_with_home(home, consent)
.iter()
.any(|s| s.state != SourceState::Symlink)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn preview_skips_unapproved() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".claude.json");
std::fs::write(&path, r#"{"mcpServers":{"fs":{"command":"npx"}}}"#).unwrap();
let src = FoundSource {
product: Product::Claude,
rel_path: ".claude.json",
path: path.clone(),
state: SourceState::New,
};
let cfg = whycodes_config::Config::default();
let (ex, plan) = preview(&[src], &cfg, false).unwrap();
assert!(ex.is_empty());
assert!(plan.is_empty());
let src = FoundSource {
product: Product::Claude,
rel_path: ".claude.json",
path,
state: SourceState::Approved,
};
let (ex, plan) = preview(&[src], &cfg, false).unwrap();
assert_eq!(ex.len(), 1);
assert_eq!(plan.mcp_add.len(), 1);
}
#[test]
fn preview_skips_denied_and_symlink_and_surfaces_extract_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".claude.json");
std::fs::write(&path, r#"{"mcpServers":{"fs":{"command":"npx"}}}"#).unwrap();
let denied = FoundSource {
product: Product::Claude,
rel_path: ".claude.json",
path: path.clone(),
state: SourceState::Denied,
};
let link = FoundSource {
product: Product::Claude,
rel_path: ".claude.json",
path: path.clone(),
state: SourceState::Symlink,
};
let cfg = whycodes_config::Config::default();
let (ex, plan) = preview(&[denied, link], &cfg, false).unwrap();
assert!(ex.is_empty());
assert!(plan.is_empty());
let missing = FoundSource {
product: Product::Claude,
rel_path: ".claude.json",
path: dir.path().join("missing.json"),
state: SourceState::Approved,
};
assert!(preview(&[missing], &cfg, false).is_err());
}
#[test]
fn has_discoverable_sources_true() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(".claude.json"), "{}").unwrap();
let consent = ConsentStore::new(dir.path().join("data"));
assert!(has_discoverable_sources(dir.path(), &consent));
assert!(!has_discoverable_sources(
&PathBuf::from("/no-such-home-xyz"),
&consent
));
}
}