//! `nd init --guess` — fill manifest fields by inspecting the repo: //! Cargo.toml / package.json / pyproject.toml / go.mod / README / git. use nd_core::manifest::Manifest; use std::path::Path; pub fn guess(dir: &Path) -> Manifest { let name = dir .canonicalize() .ok() .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string())) .unwrap_or_else(|| "my-project".into()); let mut m = Manifest { name, ..Default::default() }; let mut languages: Vec = Vec::new(); let mut code_keywords: Vec = Vec::new(); let mut entry_points: Vec = Vec::new(); // rust if dir.join("Cargo.toml").is_file() { languages.push("rust".into()); if let Ok(raw) = std::fs::read_to_string(dir.join("Cargo.toml")) { if let Ok(v) = raw.parse::() { let pkg = v.get("package"); if let Some(n) = pkg.and_then(|p| p.get("name")).and_then(|x| x.as_str()) { m.name = n.to_string(); } m.version = pkg .and_then(|p| p.get("version")) .and_then(|x| x.as_str()) .unwrap_or("0.1.0") .to_string(); m.license = pkg .and_then(|p| p.get("license")) .and_then(|x| x.as_str()) .unwrap_or_default() .to_string(); if let Some(deps) = v.get("dependencies").and_then(|d| d.as_table()) { code_keywords.extend(deps.keys().take(8).cloned()); } } } for ep in ["src/main.rs", "src/lib.rs"] { if dir.join(ep).is_file() { entry_points.push(ep.into()); } } m.install = "cargo build --release".into(); m.run = "cargo run".into(); } // node / typescript if dir.join("package.json").is_file() { languages.push( if dir.join("tsconfig.json").is_file() { "typescript" } else { "javascript" } .into(), ); if let Ok(raw) = std::fs::read_to_string(dir.join("package.json")) { if let Ok(v) = serde_json::from_str::(&raw) { if let Some(n) = v.get("name").and_then(|x| x.as_str()) { m.name = n.to_string(); } if let Some(ver) = v.get("version").and_then(|x| x.as_str()) { m.version = ver.into(); } if let Some(lic) = v.get("license").and_then(|x| x.as_str()) { m.license = lic.into(); } if let Some(deps) = v.get("dependencies").and_then(|d| d.as_object()) { code_keywords.extend(deps.keys().take(8).cloned()); } if let Some(main) = v.get("main").and_then(|x| x.as_str()) { entry_points.push(main.to_string()); } m.install = "npm install".into(); if v.get("scripts").and_then(|s| s.get("start")).is_some() { m.run = "npm start".into(); } } } } // python if dir.join("pyproject.toml").is_file() || dir.join("requirements.txt").is_file() || dir.join("setup.py").is_file() { languages.push("python".into()); if let Ok(raw) = std::fs::read_to_string(dir.join("pyproject.toml")) { if let Ok(v) = raw.parse::() { if let Some(proj) = v.get("project") { if let Some(ver) = proj.get("version").and_then(|x| x.as_str()) { m.version = ver.into(); } if let Some(deps) = proj.get("dependencies").and_then(|d| d.as_array()) { for d in deps.iter().filter_map(|x| x.as_str()).take(8) { let name = d .split(|c: char| { matches!(c, '=' | '<' | '>' | '~' | '!' | ';' | '[') }) .next() .unwrap_or(d); code_keywords.push(name.trim().to_string()); } } } } } if dir.join("main.py").is_file() { entry_points.push("main.py".into()); m.run = "python main.py".into(); } } // go if dir.join("go.mod").is_file() { languages.push("go".into()); if dir.join("main.go").is_file() { entry_points.push("main.go".into()); m.install = "go build .".into(); m.run = "go run .".into(); } } // description: first meaningful readme line for cand in ["README.md", "readme.md", "README"] { let Ok(raw) = std::fs::read_to_string(dir.join(cand)) else { continue; }; for line in raw.lines() { let t = line.trim(); if t.is_empty() || t.starts_with('#') || t.starts_with("![") || t.starts_with("[!") || t.starts_with('<') || t.starts_with('|') { continue; } m.description = t.chars().take(140).collect(); m.description = m.description.trim_end().to_string(); break; } break; } // author from git config if let Ok(out) = std::process::Command::new("git") .args(["config", "--get", "user.name"]) .output() { if out.status.success() { let n = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !n.is_empty() { m.author = n; } } } code_keywords.sort(); code_keywords.dedup(); entry_points.sort(); entry_points.dedup(); m.languages = languages; m.code_keywords = code_keywords; m.entry_points = entry_points; m } /// Render the guessed manifest as commented toml (the commented template /// is part of the llm-facing contract, so keep it readable). pub fn render(m: &Manifest) -> String { let q = |s: &str| toml::Value::String(s.to_string()).to_string(); let list = |v: &[String]| { toml::Value::Array(v.iter().map(|s| toml::Value::String(s.clone())).collect()).to_string() }; let mut out = String::from( "# notdouble.toml — generated by `nd init --guess` from repo inspection.\n\ # guessed fields are marked; the schema is not frozen — add any custom metadata.\n\n", ); out += &format!("name = {}\n", q(&m.name)); if !m.version.is_empty() { out += &format!("version = {}\n", q(&m.version)); } if !m.description.is_empty() { out += &format!("\n# from the readme\ndescription = {}\n", q(&m.description)); } out += "\n# FILL THIS IN — 1-3 sentences; the main field other agents search\nllm_description = \"\"\n"; if m.keywords.is_empty() { out += "\n# topic words — what the project IS about\nkeywords = []\n"; } else { out += &format!("\nkeywords = {}\n", list(&m.keywords)); } if !m.code_keywords.is_empty() { out += &format!( "\n# guessed from dependencies\ncode_keywords = {}\n", list(&m.code_keywords) ); } if !m.languages.is_empty() { out += &format!("\n# guessed\nlanguages = {}\n", list(&m.languages)); } if !m.author.is_empty() { out += &format!("\nauthor = {}\n", q(&m.author)); } if !m.license.is_empty() { out += &format!("license = {}\n", q(&m.license)); } if !m.entry_points.is_empty() { out += &format!("\n# guessed\nentry_points = {}\n", list(&m.entry_points)); } if !m.install.is_empty() { out += &format!("\n# guessed\ninstall = {}\n", q(&m.install)); } if !m.run.is_empty() { out += &format!("run = {}\n", q(&m.run)); } let model = m.vibe.as_ref().map(|v| v.model.clone()).unwrap_or_default(); out += &format!("\n[vibe]\nmodel = {}\nprompt_summary = \"\"\n", q(&model)); out } #[cfg(test)] mod tests { use super::*; #[test] fn guesses_rust_project() { let base = std::env::temp_dir().join(format!("nd-guess-{}", std::process::id())); let proj = base.join("my-rust-thing"); std::fs::create_dir_all(proj.join("src")).unwrap(); std::fs::write( proj.join("Cargo.toml"), "[package]\nname = \"x\"\nversion = \"1.2.3\"\nlicense = \"MIT\"\n\n[dependencies]\nclap = \"4\"\nserde = \"1\"\n", ) .unwrap(); std::fs::write(proj.join("src/main.rs"), "fn main(){}").unwrap(); std::fs::write(proj.join("README.md"), "# my-rust-thing\n\nDoes cool things quickly.\n").unwrap(); let m = guess(&proj); // name is preferred from Cargo.toml, not the dir name assert_eq!(m.name, "x"); assert_eq!(m.version, "1.2.3"); assert_eq!(m.license, "MIT"); assert_eq!(m.languages, vec!["rust"]); assert!(m.code_keywords.contains(&"clap".to_string())); assert!(m.entry_points.contains(&"src/main.rs".to_string())); assert_eq!(m.description, "Does cool things quickly."); assert!(m.run == "cargo run"); let rendered = render(&m); assert!(rendered.contains("name = \"x\"")); assert!(rendered.contains("languages = [\"rust\"]")); std::fs::remove_dir_all(&base).ok(); } }