1pub mod apply;
7pub mod consent;
8pub mod discover;
9pub mod error;
10pub mod extract;
11pub mod parse;
12pub mod types;
13
14pub use apply::{apply, apply_and_save, plan};
15pub use consent::ConsentStore;
16pub use discover::{KNOWN_SOURCES, scan, scan_with_home, why_config_missing};
17pub use error::{ImportError, Result};
18pub use extract::extract;
19pub use types::{
20 Extracted, FoundSource, ImportItem, ImportItemKind, ImportPlan, Product, SourceState,
21};
22
23pub fn preview(
25 sources: &[FoundSource],
26 config: &whycodes_config::Config,
27 force: bool,
28) -> Result<(Vec<Extracted>, ImportPlan)> {
29 let mut extracted = Vec::new();
30 for src in sources {
31 match src.state {
32 SourceState::Approved => extracted.push(extract(src)?),
33 SourceState::New | SourceState::Denied | SourceState::Symlink => {}
34 }
35 }
36 let plan = plan(config, &extracted, force);
37 Ok((extracted, plan))
38}
39
40pub fn has_discoverable_sources(home: &std::path::Path, consent: &ConsentStore) -> bool {
42 scan_with_home(home, consent)
43 .iter()
44 .any(|s| s.state != SourceState::Symlink)
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use std::path::PathBuf;
51
52 #[test]
53 fn preview_skips_unapproved() {
54 let dir = tempfile::tempdir().unwrap();
55 let path = dir.path().join(".claude.json");
56 std::fs::write(&path, r#"{"mcpServers":{"fs":{"command":"npx"}}}"#).unwrap();
57 let src = FoundSource {
58 product: Product::Claude,
59 rel_path: ".claude.json",
60 path: path.clone(),
61 state: SourceState::New,
62 };
63 let cfg = whycodes_config::Config::default();
64 let (ex, plan) = preview(&[src], &cfg, false).unwrap();
65 assert!(ex.is_empty());
66 assert!(plan.is_empty());
67 let src = FoundSource {
68 product: Product::Claude,
69 rel_path: ".claude.json",
70 path,
71 state: SourceState::Approved,
72 };
73 let (ex, plan) = preview(&[src], &cfg, false).unwrap();
74 assert_eq!(ex.len(), 1);
75 assert_eq!(plan.mcp_add.len(), 1);
76 }
77
78 #[test]
79 fn preview_skips_denied_and_symlink_and_surfaces_extract_error() {
80 let dir = tempfile::tempdir().unwrap();
81 let path = dir.path().join(".claude.json");
82 std::fs::write(&path, r#"{"mcpServers":{"fs":{"command":"npx"}}}"#).unwrap();
83 let denied = FoundSource {
84 product: Product::Claude,
85 rel_path: ".claude.json",
86 path: path.clone(),
87 state: SourceState::Denied,
88 };
89 let link = FoundSource {
90 product: Product::Claude,
91 rel_path: ".claude.json",
92 path: path.clone(),
93 state: SourceState::Symlink,
94 };
95 let cfg = whycodes_config::Config::default();
96 let (ex, plan) = preview(&[denied, link], &cfg, false).unwrap();
97 assert!(ex.is_empty());
98 assert!(plan.is_empty());
99 let missing = FoundSource {
100 product: Product::Claude,
101 rel_path: ".claude.json",
102 path: dir.path().join("missing.json"),
103 state: SourceState::Approved,
104 };
105 assert!(preview(&[missing], &cfg, false).is_err());
106 }
107
108 #[test]
109 fn has_discoverable_sources_true() {
110 let dir = tempfile::tempdir().unwrap();
111 std::fs::write(dir.path().join(".claude.json"), "{}").unwrap();
112 let consent = ConsentStore::new(dir.path().join("data"));
113 assert!(has_discoverable_sources(dir.path(), &consent));
114 assert!(!has_discoverable_sources(
115 &PathBuf::from("/no-such-home-xyz"),
116 &consent
117 ));
118 }
119}