use crate::discovery::{discover_services, DiscoveredService};
use crate::manifest::{DiscoveryConfig, Manifest, DEFAULT_DISCOVERY_PATHS};
use anyhow::{bail, Result};
use std::path::{Path, PathBuf};
fn infer_language(root: &Path, service_path: &str) -> Option<String> {
let dir = root.join(service_path);
let candidates: &[(&str, &str)] = &[
("Cargo.toml", "Rust"),
("go.mod", "Go"),
("package.json", "TypeScript"),
("pyproject.toml", "Python"),
("requirements.txt", "Python"),
("pom.xml", "Java"),
("build.gradle", "Java"),
("build.gradle.kts", "Kotlin"),
("CMakeLists.txt", "C++"),
("Directory.Build.props", "C#"),
("Gemfile", "Ruby"),
("mix.exs", "Elixir"),
("pubspec.yaml", "Dart"),
];
for (marker, lang) in candidates {
if dir.join(marker).exists() {
return Some(lang.to_string());
}
}
None
}
pub fn render_yaml(services: &[DiscoveredService], root: &Path) -> String {
let mut out = String::new();
out.push_str("# Generated by `svccat init`\n");
out.push_str("# Fill in the ~placeholder~ fields and commit this file.\n");
out.push_str("# Run `svccat check` to verify there is no drift.\n\n");
out.push_str("version: \"1\"\n\n");
out.push_str("discovery:\n");
out.push_str(" paths:\n");
for p in DEFAULT_DISCOVERY_PATHS {
out.push_str(&format!(" - {}\n", p));
}
out.push('\n');
out.push_str("services:\n");
if services.is_empty() {
out.push_str(" [] # no services detected — add entries manually\n");
} else {
for svc in services {
let lang = infer_language(root, &svc.path).unwrap_or_else(|| "~".to_string());
out.push_str(&format!(" - name: {}\n", svc.name));
out.push_str(&format!(" path: {}\n", svc.path));
out.push_str(&format!(" language: {}\n", lang));
out.push_str(" platform: ~ # e.g. gcp-cloud-run, fly.io, vercel, aws-lambda\n");
out.push_str(" role: ~ # e.g. api, worker, frontend, database\n");
out.push_str(" url: ~ # e.g. https://my-service.example.com\n");
}
}
out
}
pub fn run(root: &Path, output_path: PathBuf, force: bool) -> Result<()> {
if output_path.exists() && !force {
bail!(
"{} already exists. Use --force to overwrite.",
output_path.display()
);
}
let bootstrap = Manifest {
version: "1".to_string(),
discovery: DiscoveryConfig {
paths: vec![],
markers: crate::manifest::default_markers_pub(),
ignore: vec![],
},
policy: Default::default(),
services: vec![],
};
let discovered = discover_services(root, &bootstrap);
let yaml = render_yaml(&discovered, root);
std::fs::write(&output_path, &yaml)?;
let verb = if output_path.exists() {
"Wrote"
} else {
"Created"
};
println!(
"{} {} ({} service{} detected)",
verb,
output_path.display(),
discovered.len(),
if discovered.len() == 1 { "" } else { "s" }
);
println!("Edit the file, then run `svccat check` to verify.");
Ok(())
}