use anyhow::Result;
use inquire::MultiSelect;
use crate::catalog::tool_catalog::{
self, ToolEntry, PLUGINS, ROKIT_TOOLS, SYSTEM_APPS, VSCODE_EXTENSIONS,
};
use crate::config::GlobalConfig;
use crate::steps::{blender, bootstrap, rojo, studio_plugin, toolchain, vscode};
use crate::ui::{self, Tally};
pub fn run(config: &mut GlobalConfig) -> Result<()> {
let system_apps = pick_from_catalog(
"System apps",
&SYSTEM_APPS.iter().collect::<Vec<_>>(),
&config.selected_system_apps,
)?;
let rokit_tools = pick_from_catalog(
"Rokit-managed CLI tools (rojo/wally/selene/stylua/...)",
&ROKIT_TOOLS.iter().collect::<Vec<_>>(),
&config.selected_rokit_tools,
)?;
let plugins = pick_plugins(&system_apps, &config.selected_studio_plugins)?;
let vscode_extensions = if system_apps.iter().any(|k| k == "vscode") {
pick_from_catalog(
"VS Code extensions & themes",
&VSCODE_EXTENSIONS.iter().collect::<Vec<_>>(),
&config.selected_vscode_extensions,
)?
} else {
Vec::new()
};
ui::section("Machine setup");
bootstrap::ensure_rokit()?;
let mut apps = Tally::new();
for key in &system_apps {
let Some(entry) = SYSTEM_APPS.iter().find(|e| e.key == *key) else { continue };
if bootstrap::is_installed(entry) {
apps.already(entry.key);
} else {
match bootstrap::install(entry) {
Ok(()) => apps.did(entry.key),
Err(err) => {
ui::warn(&format!("{}: {err}", entry.key));
if format!("{err}").contains("installer hash") {
ui::detail(bootstrap::WINGET_HASH_HELP);
}
}
}
}
}
apps.finish("system apps");
let projects_root = config.roblox_projects_root.clone().unwrap_or(config.projects_root()?);
bootstrap::ensure_projects_folder(&projects_root)?;
if let Err(err) = toolchain::add_global_tools(&rokit_tools) {
warn_and_continue("rokit tools", &err);
}
let studio_installed = SYSTEM_APPS.iter().find(|e| e.key == "studio").is_some_and(bootstrap::is_installed);
if plugins.iter().any(|k| k == "rojo-plugin") {
if studio_installed {
if let Err(err) = rojo::install_studio_plugin() {
warn_and_continue("rojo-plugin", &err);
}
} else {
ui::skip("rojo-plugin needs Roblox Studio, which isn't installed yet");
}
}
for key in ["hoarcekat", "luau-lsp-plugin"] {
if plugins.iter().any(|k| k == key)
&& let Some((repo, suffix)) = studio_plugin_repo(key)
&& let Err(err) = studio_plugin::install_from_latest_release(repo, suffix)
{
warn_and_continue(key, &err);
}
}
if plugins.iter().any(|k| k == "blender-plugin")
&& let Some(repo) = blender_addon_repo()
{
match blender::download_latest_plugin_zip(repo).and_then(|zip| blender::install_addon(&zip)) {
Ok(()) => blender::print_account_link_instructions(),
Err(err) => warn_and_continue("blender-plugin", &err),
}
}
if system_apps.iter().any(|k| k == "vscode") {
let extension_ids: Vec<&str> = vscode_extensions
.iter()
.filter_map(|key| match tool_catalog::find(key)?.kind {
tool_catalog::ToolKind::VsCodeExtension { extension_id } => Some(extension_id),
_ => None,
})
.collect();
if let Err(err) = vscode::ensure_extensions(&extension_ids) {
warn_and_continue("vscode extensions", &err);
}
}
config.roblox_projects_root = Some(projects_root);
config.selected_system_apps = system_apps;
config.selected_rokit_tools = rokit_tools;
config.selected_studio_plugins = plugins;
config.selected_vscode_extensions = vscode_extensions;
config.last_checked = Some(now_unix_seconds());
Ok(())
}
fn warn_and_continue(what: &str, err: &anyhow::Error) {
ui::warn(&format!("{what} skipped - {err}"));
}
fn studio_plugin_repo(key: &str) -> Option<(&'static str, &'static str)> {
match tool_catalog::find(key)?.kind {
tool_catalog::ToolKind::StudioPlugin { github_repo, asset_suffix } => Some((github_repo, asset_suffix)),
_ => None,
}
}
fn blender_addon_repo() -> Option<&'static str> {
match tool_catalog::find("blender-plugin")?.kind {
tool_catalog::ToolKind::BlenderAddon { github_repo } => Some(github_repo),
_ => None,
}
}
fn pick_plugins(system_apps: &[String], previously_selected: &[String]) -> Result<Vec<String>> {
let relevant: Vec<&ToolEntry> = PLUGINS
.iter()
.filter(|p| {
!matches!(p.kind, tool_catalog::ToolKind::BlenderAddon { .. })
|| system_apps.iter().any(|k| k == "blender")
})
.collect();
pick_from_catalog("Plugins", &relevant, previously_selected)
}
fn pick_from_catalog(
prompt: &str,
entries: &[&ToolEntry],
previously_selected: &[String],
) -> Result<Vec<String>> {
let has_prior = !previously_selected.is_empty();
let options: Vec<String> = entries
.iter()
.map(|e| ui::option_line(e.key, e.description, e.maintenance.short_badge()))
.collect();
let default_indices: Vec<usize> = entries
.iter()
.enumerate()
.filter(|(_, e)| {
if has_prior {
previously_selected.iter().any(|k| k == e.key)
} else {
e.default_selected
}
})
.map(|(i, _)| i)
.collect();
let selected = MultiSelect::new(prompt, options)
.with_default(&default_indices)
.with_help_message(ui::MULTISELECT_HELP)
.with_formatter(&ui::compact_multi_answer)
.prompt()?;
Ok(entries
.iter()
.filter(|e| selected.iter().any(|s| ui::option_is(s, e.key)))
.map(|e| e.key.to_string())
.collect())
}
fn now_unix_seconds() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_default()
}