use std::path::Path;
use std::process::Command;
use std::time::Duration;
use crate::spawn::SpawnOutcome;
pub(crate) fn detect_repo_url(root: &Path) -> Option<String> {
let mut cmd = Command::new("git");
cmd.args(["remote", "get-url", "origin"]).current_dir(root);
match crate::spawn::run(&mut cmd, Duration::from_secs(3)) {
SpawnOutcome::RanClean(out) => Some(out.trim().to_string()),
_ => None,
}
}
pub(crate) fn detect_license(root: &Path) -> Option<String> {
for filename in &["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"] {
let p = root.join(filename);
if let Ok(raw) = std::fs::read_to_string(&p) {
let head = raw.split('\n').take(3).collect::<Vec<_>>().join("\n");
let lower = head.to_lowercase();
if lower.contains("mit license") || lower.contains("permission is hereby granted") {
return Some("MIT".to_string());
}
if lower.contains("apache license") {
return Some("Apache-2.0".to_string());
}
if lower.contains("bsd 3-clause") || lower.contains("neither the name") {
return Some("BSD-3-Clause".to_string());
}
if lower.contains("gnu general public license") {
return Some("GPL-3.0".to_string());
}
}
}
None
}
const README_HEAD_LINES: usize = 500;
pub(crate) fn read_readme_hint(root: &Path) -> Option<String> {
for filename in &["README.md", "README", "readme.md"] {
let p = root.join(filename);
if let Ok(raw) = std::fs::read_to_string(&p) {
let head: String = raw
.lines()
.take(README_HEAD_LINES)
.collect::<Vec<_>>()
.join("\n");
let paragraph = head
.lines()
.skip_while(|l| {
let t = l.trim();
t.is_empty() || t.starts_with('#') || t.starts_with('!') || t.starts_with('<')
})
.take_while(|l| !l.trim().is_empty())
.collect::<Vec<_>>()
.join(" ");
let trimmed = paragraph.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
pub(crate) fn repo_url_name(repo_url: &Option<String>) -> Option<String> {
let url = repo_url.as_ref()?;
let last = url.rsplit('/').next()?.trim_end();
let stem = last.strip_suffix(".git").unwrap_or(last);
Some(stem.to_string())
}
#[cfg(test)]
mod tests {
use super::read_readme_hint;
#[test]
fn read_readme_hint_skips_leading_html_div() {
let dir = std::env::temp_dir().join(format!(
"skillpack-readme-html-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("README.md"),
"<div align=\"center\"><img src=\"logo.png\" alt=\"logo\"></div>\n\n\
# mytool\n\n\
A sample tool that frobs widgets.\n",
)
.unwrap();
let hint = read_readme_hint(&dir).unwrap_or_default();
let _ = std::fs::remove_dir_all(&dir);
assert!(
!hint.contains('<'),
"desc_hint must drop raw HTML, got: {hint:?}"
);
assert!(
hint.contains("frobs widgets"),
"desc_hint must land on first prose line, got: {hint:?}"
);
}
}