use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use crate::catalog::tool_catalog::{ToolKind, ROKIT_TOOLS};
use crate::catalog::tool_settings;
use crate::config::PackageWorkflow;
use crate::steps::{capture, run_in};
use crate::ui::{self, Tally};
pub fn sync_installed_tools(project_dir: &Path) -> Result<()> {
run_in("rokit", &["install"], Some(project_dir))
}
pub fn ensure_rokit_init(project_dir: &Path) -> Result<()> {
if project_dir.join("rokit.toml").exists() {
ui::ok("rokit.toml already exists");
return Ok(());
}
run_in("rokit", &["init"], Some(project_dir))
}
pub fn add_selected_tools(project_dir: &Path, selected: &[String]) -> Result<()> {
add_all(Some(project_dir), selected, "rokit tools")
}
pub fn add_global_tools(selected: &[String]) -> Result<()> {
add_all(None, selected, "rokit tools (global)")
}
fn add_all(project_dir: Option<&Path>, selected: &[String], noun: &str) -> Result<()> {
let mut tally = Tally::new();
for key in selected {
let Some(entry) = ROKIT_TOOLS.iter().find(|t| t.key == key) else {
continue;
};
let ToolKind::RokitTool { rokit_source } = entry.kind else {
continue;
};
run_rokit_add(project_dir, rokit_source, key, &mut tally)?;
}
tally.finish(noun);
Ok(())
}
fn run_rokit_add(
project_dir: Option<&Path>,
rokit_source: &str,
key: &str,
tally: &mut Tally,
) -> Result<()> {
let mut args = vec!["add"];
if project_dir.is_none() {
let _ = capture("rokit", &["trust", rokit_source], None);
args.push("--global");
}
args.push(rokit_source);
let output = capture("rokit", &args, project_dir)?;
if ui::is_verbose() {
ui::passthrough(&output.stdout, &output.stderr);
}
if output.success {
tally.did(key);
return Ok(());
}
let combined = output.combined();
if combined.contains("already exists") {
tally.already(key);
} else if combined.contains("not been marked as trusted") {
ui::warn(&format!(
"{key} isn't trusted by rokit - run `rokit trust {rokit_source}` once, then re-run"
));
} else if combined.contains("403 Forbidden") || combined.to_lowercase().contains("rate limit") {
ui::warn(&format!("{key} skipped - GitHub API rate limit reached"));
ui::detail(
"GitHub allows 60 unauthenticated API requests/hour per IP, shared across\n\
every call rproj and rokit make. Wait for it to reset, or run\n\
`rokit authenticate github` with a token to raise the limit.",
);
} else {
ui::warn(&format!("{key} could not be added, continuing"));
ui::passthrough(&output.stdout, &output.stderr);
}
Ok(())
}
pub fn ensure_selene_config(
project_dir: &Path,
testez_selected: bool,
workflow: PackageWorkflow,
allow_mixed_tables: bool,
) -> Result<()> {
let std_value = if testez_selected { "roblox+testez" } else { "roblox" };
let mut overrides = vec![("std", std_value)];
if allow_mixed_tables {
overrides.push(("mixed_table", "allow"));
}
let mut config = tool_settings::default_toml("selene", &overrides)
.context("selene is missing from the configurable-tools catalog")?;
let vendored = match workflow {
PackageWorkflow::Wally => r#"exclude = ["Packages/**", "ServerPackages/**"]"#,
PackageWorkflow::GitSubmodules => r#"exclude = ["modules/submodules/**"]"#,
};
config = tool_settings::insert_top_level(&config, vendored);
ensure_config_file(project_dir, "selene.toml", &config, |content| {
content.lines().any(|l| l.trim() == format!(r#"std = "{std_value}""#))
})
}
pub fn ensure_stylua_config(project_dir: &Path) -> Result<()> {
let config = tool_settings::default_toml("stylua", &[])
.context("stylua is missing from the configurable-tools catalog")?;
ensure_config_file(project_dir, "stylua.toml", &config, |_| true)
}
fn ensure_config_file(
project_dir: &Path,
filename: &str,
default_content: &str,
already_valid: impl Fn(&str) -> bool,
) -> Result<()> {
let path = project_dir.join(filename);
if path.exists() {
let content = fs::read_to_string(&path)?;
if already_valid(&content) {
ui::ok(&format!("{filename} already configured"));
return Ok(());
}
}
fs::write(&path, default_content)?;
ui::ok(&format!("wrote {filename}"));
Ok(())
}