Skip to main content

pawan/
init.rs

1//! Project skeleton templates for `pawan init`.
2//!
3//! Generates scaffolding files for new Rust projects that pass `cargo check` immediately.
4
5use std::path::Path;
6
7/// A project skeleton: a named collection of (relative path, content) pairs.
8pub struct ProjectSkeleton {
9    pub name: String,
10    pub files: Vec<(String, String)>,
11}
12
13impl ProjectSkeleton {
14    /// Write all skeleton files under `root`, creating intermediate directories as needed.
15    pub fn write_to(&self, root: &Path) -> std::io::Result<()> {
16        for (rel_path, content) in &self.files {
17            let full = root.join(rel_path);
18            if let Some(parent) = full.parent() {
19                std::fs::create_dir_all(parent)?;
20            }
21            std::fs::write(&full, content)?;
22        }
23        Ok(())
24    }
25}
26
27/// Generate a simple Rust binary project skeleton.
28pub fn rust_binary_skeleton(name: &str) -> ProjectSkeleton {
29    let cargo_toml = format!(
30        r#"[package]
31name = "{name}"
32version = "0.1.0"
33edition = "2021"
34rust-version = "1.75"
35"#
36    );
37    let main_rs = "fn main() {\n    println!(\"Hello, world!\");\n}\n".to_string();
38
39    ProjectSkeleton {
40        name: name.to_string(),
41        files: vec![
42            ("Cargo.toml".into(), cargo_toml),
43            ("src/main.rs".into(), main_rs),
44        ],
45    }
46}
47
48/// Generate a Rust library project skeleton.
49pub fn rust_library_skeleton(name: &str) -> ProjectSkeleton {
50    let cargo_toml = format!(
51        r#"[package]
52name = "{name}"
53version = "0.1.0"
54edition = "2021"
55rust-version = "1.75"
56"#
57    );
58    let lib_rs = "/// Add two numbers.\npub fn add(a: i32, b: i32) -> i32 {\n    a + b\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn it_works() {\n        assert_eq!(add(2, 2), 4);\n    }\n}\n".to_string();
59
60    ProjectSkeleton {
61        name: name.to_string(),
62        files: vec![
63            ("Cargo.toml".into(), cargo_toml),
64            ("src/lib.rs".into(), lib_rs),
65        ],
66    }
67}
68
69/// Generate a Pawan agent project skeleton (binary with pawan dep + pawan.toml config).
70pub fn pawan_agent_skeleton(name: &str) -> ProjectSkeleton {
71    let cargo_toml = format!(
72        r#"[package]
73name = "{name}"
74version = "0.1.0"
75edition = "2021"
76rust-version = "1.75"
77
78[dependencies]
79pawan = {{ git = "https://github.com/dirmacs/pawan.git" }}
80"#
81    );
82    let main_rs = format!(
83        r#"fn main() {{
84    println!("Pawan agent: {name}");
85}}
86"#
87    );
88    let pawan_toml = format!(
89        r#"[agent]
90name = "{name}"
91model = "qwen/qwen3.5-122b-a10b"
92
93[provider]
94name = "nvidia"
95api_url = "https://integrate.api.nvidia.com/v1"
96"#
97    );
98
99    ProjectSkeleton {
100        name: name.to_string(),
101        files: vec![
102            ("Cargo.toml".into(), cargo_toml),
103            ("src/main.rs".into(), main_rs),
104            ("pawan.toml".into(), pawan_toml),
105        ],
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::fs;
113
114    #[test]
115    fn binary_skeleton_files() {
116        let sk = rust_binary_skeleton("myapp");
117        assert_eq!(sk.name, "myapp");
118        let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
119        assert_eq!(paths, vec!["Cargo.toml", "src/main.rs"]);
120    }
121
122    #[test]
123    fn library_skeleton_files() {
124        let sk = rust_library_skeleton("mylib");
125        assert_eq!(sk.name, "mylib");
126        let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
127        assert_eq!(paths, vec!["Cargo.toml", "src/lib.rs"]);
128    }
129
130    #[test]
131    fn agent_skeleton_files() {
132        let sk = pawan_agent_skeleton("myagent");
133        assert_eq!(sk.name, "myagent");
134        let paths: Vec<&str> = sk.files.iter().map(|(p, _)| p.as_str()).collect();
135        assert_eq!(paths, vec!["Cargo.toml", "src/main.rs", "pawan.toml"]);
136    }
137
138    #[test]
139    fn write_to_creates_files() {
140        let dir = tempfile::tempdir().unwrap();
141        let sk = rust_binary_skeleton("testproj");
142        sk.write_to(dir.path()).unwrap();
143
144        let cargo = fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
145        assert!(cargo.contains("name = \"testproj\""));
146        assert!(cargo.contains("edition = \"2021\""));
147        assert!(cargo.contains("rust-version = \"1.75\""));
148
149        let main = fs::read_to_string(dir.path().join("src/main.rs")).unwrap();
150        assert!(main.contains("fn main()"));
151    }
152
153    #[test]
154    fn cargo_toml_is_valid_toml() {
155        for sk in [
156            rust_binary_skeleton("a"),
157            rust_library_skeleton("b"),
158            pawan_agent_skeleton("c"),
159        ] {
160            let cargo_content = &sk.files.iter().find(|(p, _)| p == "Cargo.toml").unwrap().1;
161            let parsed: Result<toml::Value, _> = toml::from_str(cargo_content);
162            assert!(parsed.is_ok(), "Invalid TOML in {} skeleton", sk.name);
163        }
164    }
165
166    #[test]
167    fn pawan_agent_skeleton_includes_pawan_toml_in_file_list() {
168        let sk = pawan_agent_skeleton("demo");
169        let has_pawan_toml = sk.files.iter().any(|(p, _)| p == "pawan.toml");
170        assert!(
171            has_pawan_toml,
172            "pawan_agent_skeleton must include pawan.toml"
173        );
174    }
175
176    #[test]
177    fn write_to_creates_nested_directories() {
178        let dir = tempfile::tempdir().unwrap();
179        let sk = rust_binary_skeleton("nested");
180        sk.write_to(dir.path()).unwrap();
181        // src/ directory must have been created for src/main.rs
182        assert!(
183            dir.path().join("src").is_dir(),
184            "src/ directory not created"
185        );
186        assert!(dir.path().join("src/main.rs").is_file());
187    }
188
189    #[test]
190    fn skeleton_names_set_correctly() {
191        assert_eq!(rust_binary_skeleton("alpha").name, "alpha");
192        assert_eq!(rust_library_skeleton("beta").name, "beta");
193        assert_eq!(pawan_agent_skeleton("gamma").name, "gamma");
194    }
195
196    #[test]
197    fn generated_cargo_toml_contains_edition_and_rust_version() {
198        for (sk, expected_name) in [
199            (rust_binary_skeleton("x"), "x"),
200            (rust_library_skeleton("y"), "y"),
201            (pawan_agent_skeleton("z"), "z"),
202        ] {
203            let cargo = &sk.files.iter().find(|(p, _)| p == "Cargo.toml").unwrap().1;
204            assert!(
205                cargo.contains("edition = \"2021\""),
206                "{expected_name} missing edition"
207            );
208            assert!(
209                cargo.contains("rust-version = \"1.75\""),
210                "{expected_name} missing rust-version"
211            );
212        }
213    }
214
215    #[test]
216    fn agent_skeleton_has_pawan_toml() {
217        let dir = tempfile::tempdir().unwrap();
218        let sk = pawan_agent_skeleton("agent1");
219        sk.write_to(dir.path()).unwrap();
220
221        let pawan_cfg = fs::read_to_string(dir.path().join("pawan.toml")).unwrap();
222        assert!(pawan_cfg.contains("name = \"agent1\""));
223        assert!(pawan_cfg.contains("model ="));
224    }
225}