use std::path::Path;
use anyhow::{Context, Result, bail};
use console::style;
use dialoguer::Input;
enum Project {
Rust { crate_name: Option<String> },
Bun,
Unknown,
}
pub fn run(name_flag: Option<&str>, port_flag: Option<u16>, yes: bool) -> Result<()> {
let dir = std::env::current_dir().context("cannot resolve current directory")?;
let service_toml = dir.join("service.toml");
if service_toml.exists() {
bail!(
"service.toml already exists in {} (refusing to overwrite)",
dir.display()
);
}
let dir_name = dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("service")
.to_string();
let project = detect(&dir);
println!(
"Detected {}.",
match &project {
Project::Rust { .. } => "a Rust project (Cargo.toml)",
Project::Bun => "a Bun project (package.json)",
Project::Unknown => "no Cargo.toml or package.json",
}
);
let interactive = super::is_interactive() && !yes;
let default_name = name_flag
.map(str::to_string)
.unwrap_or_else(|| match &project {
Project::Rust {
crate_name: Some(c),
} => c.clone(),
_ => dir_name.clone(),
});
let name = prompt(interactive, "Service name", default_name)?;
let description = name.clone();
let port: Option<u16> = if let Some(p) = port_flag {
Some(p)
} else if interactive {
let raw: String = Input::new()
.with_prompt("HTTP port (blank to fill in later)")
.allow_empty(true)
.interact_text()?;
let raw = raw.trim();
if raw.is_empty() {
None
} else {
Some(raw.parse().context("port must be a number")?)
}
} else {
None
};
let (build, run) = match &project {
Project::Rust { crate_name } => {
let bin = crate_name.clone().unwrap_or_else(|| name.clone());
(
Some("cargo build --release".to_string()),
format!("target/release/{bin}"),
)
}
Project::Bun => (
Some("bun install".to_string()),
"bun --watch run src/index.ts".to_string(),
),
Project::Unknown => (None, "./REPLACE-WITH-YOUR-RUN-COMMAND".to_string()),
};
std::fs::write(
&service_toml,
render_service_toml(&name, &description, build.as_deref(), &run, port),
)
.with_context(|| format!("writing {}", service_toml.display()))?;
let test_toml = dir.join("test.toml");
if !test_toml.exists() {
std::fs::write(&test_toml, render_test_toml(&name))
.with_context(|| format!("writing {}", test_toml.display()))?;
}
println!();
println!(
"{} service.toml{}",
style("Wrote").green().bold(),
if test_toml.exists() {
" + test.toml"
} else {
""
}
);
if matches!(project, Project::Unknown) {
println!(
" {} set {} in service.toml to your start command first",
style("!").yellow().bold(),
style("run").bold()
);
}
println!("Next: {}", style("ryra add").bold());
Ok(())
}
fn detect(dir: &Path) -> Project {
if dir.join("Cargo.toml").is_file() {
let crate_name = std::fs::read_to_string(dir.join("Cargo.toml"))
.ok()
.and_then(|s| s.parse::<toml::Value>().ok())
.and_then(|v| v.get("package")?.get("name")?.as_str().map(str::to_string));
Project::Rust { crate_name }
} else if dir.join("package.json").is_file() {
Project::Bun
} else {
Project::Unknown
}
}
fn prompt(interactive: bool, label: &str, default: String) -> Result<String> {
if interactive {
Ok(Input::new()
.with_prompt(label)
.default(default)
.interact_text()?)
} else {
Ok(default)
}
}
const DOCS_URL: &str = "https://ryra.dev";
fn render_service_toml(
name: &str,
description: &str,
build: Option<&str>,
run: &str,
port: Option<u16>,
) -> String {
let mut s = format!("# {DOCS_URL}\n[service]\n");
s.push_str(&format!("name = {}\n", q(name)));
s.push_str(&format!("description = {}\n", q(description)));
s.push_str("runtime = \"native\"\n");
if let Some(b) = build {
s.push_str(&format!("build = {}\n", q(b)));
}
s.push_str(&format!("run = {}\n", q(run)));
match port {
Some(p) => {
s.push_str(&format!(
"\n[[ports]]\nname = \"http\"\ncontainer_port = {p}\n"
));
}
None => {
s.push_str(
"\n[[ports]]\nname = \"http\"\ncontainer_port = 0 # fill in the port your service listens on\n",
);
}
}
s
}
fn render_test_toml(name: &str) -> String {
format!(
"[[tests]]\n\
name = {n}\n\
[[tests.steps]]\n\
action = \"add\"\n\
service = {n}\n\
timeout = 120\n\
[[tests.steps]]\n\
action = \"wait\"\n\
service = {n}\n",
n = q(name)
)
}
fn q(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}