use std::env;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use crate::catalog::{tool_catalog, tool_usage};
use crate::commands::provision;
use crate::config::GlobalConfig;
use crate::steps::{notify, tarmac, toolchain};
use crate::ui;
pub fn run(tool: Option<&str>) -> Result<()> {
match tool {
Some(key) => setup_tool(key),
None => setup_machine(),
}
}
fn setup_machine() -> Result<()> {
ui::detail("Nothing already installed is reinstalled. Safe to re-run any time.");
let mut config = GlobalConfig::load()?;
provision::run(&mut config)?;
config.save()?;
notify::summary("rproj setup complete", "Your machine is ready for Roblox development.");
println!("\nDone. Run `rproj new <name>` to scaffold your first project.");
Ok(())
}
fn setup_tool(key: &str) -> Result<()> {
let entry = tool_catalog::find(key).with_context(|| {
format!("no tool called `{key}`. `rproj info` lists everything rproj knows about")
})?;
let project_dir = find_project_root()?;
ui::section(&format!("Setting up {} in {}", entry.key, project_dir.display()));
match entry.kind {
tool_catalog::ToolKind::RokitTool { .. } => {
toolchain::ensure_rokit_init(&project_dir)?;
toolchain::add_selected_tools(&project_dir, std::slice::from_ref(&entry.key.to_string()))?;
}
_ => ui::skip(&format!(
"{} is a {}, so there is nothing to pin per project",
entry.key,
entry.kind.label()
)),
}
match entry.key {
"tarmac" => tarmac::ensure_config(&project_dir, &project_name(&project_dir))?,
_ => {
if tool_usage::find(entry.key).is_some() {
ui::skip(&format!(
"no config file for {} - `rproj configure {}` if it has settings",
entry.key, entry.key
));
}
}
}
if let Some(usage) = tool_usage::find(entry.key) {
if !usage.notes.is_empty() {
println!();
ui::detail("Worth knowing:");
for note in usage.notes {
ui::detail(&format!(" - {note}"));
}
}
if let Some((command, what)) = usage.commands.first() {
println!();
ui::detail("Try next:");
ui::detail(&format!(" {command}"));
ui::detail(&format!(" {what}"));
}
}
Ok(())
}
fn find_project_root() -> Result<PathBuf> {
let start = env::current_dir().context("could not read the current directory")?;
for dir in start.ancestors() {
if dir.join("default.project.json").is_file() {
return Ok(dir.to_path_buf());
}
}
bail!(
"not inside a Roblox project - no default.project.json here or above {}.\n\
`rproj setup` with no tool name sets up the machine instead, and \
`rproj new <name>` creates a project.",
start.display()
)
}
fn project_name(project_dir: &Path) -> String {
project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("roblox-project")
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unknown_tool_is_named_in_the_error() {
let err = setup_tool("definitely-not-a-tool").unwrap_err();
let message = format!("{err}");
assert!(message.contains("definitely-not-a-tool"), "{message}");
assert!(message.contains("rproj info"), "{message}");
}
#[test]
fn the_project_name_comes_from_the_folder() {
assert_eq!(project_name(Path::new("/games/my-game")), "my-game");
}
#[test]
fn the_documented_setup_candidates_all_have_usage_text() {
for key in ["tarmac", "mantle", "lute", "luau-lsp-cli"] {
assert!(
tool_catalog::find(key).is_some(),
"{key} is not in the tool catalog"
);
let usage = tool_usage::find(key)
.unwrap_or_else(|| panic!("{key} has no usage entry, so setup would print nothing"));
assert!(!usage.commands.is_empty(), "{key} lists no commands");
}
}
}