use std::path::{Path, PathBuf};
use std::sync::Arc;
use async_trait::async_trait;
use clap::ValueHint;
use eyre::{Result, eyre};
use indoc::formatdoc;
use mise_interactive_config::{
BackendInfo, BackendProvider, ConfigResult, InteractiveConfig, ToolInfo, ToolProvider,
VersionProvider,
};
use strum::IntoEnumIterator;
use crate::backend::backend_type::BackendType;
use crate::cli::args::BackendArg;
use crate::cli::version::VERSION_PLAIN;
use crate::config::config_file;
use crate::config::{Config, Settings};
use crate::file::display_path;
use crate::plugins::PluginType;
use crate::registry::REGISTRY;
use crate::toolset::install_state;
use crate::ui::progress_report::{ProgressIcon, SingleReport};
use crate::{env, file};
struct MiseToolProvider;
impl ToolProvider for MiseToolProvider {
fn list_tools(&self) -> Vec<ToolInfo> {
REGISTRY
.iter()
.map(|(name, rt)| ToolInfo {
name: name.to_string(),
description: rt.description.map(|s| s.to_string()),
aliases: rt.aliases.iter().map(|s| s.to_string()).collect(),
})
.collect()
}
}
struct MiseVersionProvider;
#[async_trait]
impl VersionProvider for MiseVersionProvider {
async fn latest_version(&self, tool: &str) -> Option<String> {
let ba = Arc::new(BackendArg::from(tool));
let backend = ba.backend().ok()?;
let config = Config::get().await.ok()?;
backend.latest_version(&config, None).await.ok()?
}
}
struct MiseBackendProvider;
impl BackendProvider for MiseBackendProvider {
fn list_backends(&self) -> Vec<BackendInfo> {
let mut backends = Vec::new();
for backend_type in BackendType::iter() {
let (name, description) = match backend_type {
BackendType::Aqua => ("aqua", Some("Install tools from aquaproj registry")),
BackendType::Asdf => ("asdf", Some("Install tools via asdf plugins")),
BackendType::Cargo => ("cargo", Some("Install Rust packages from crates.io")),
BackendType::Conda => ("conda", Some("Install packages from conda-forge")),
BackendType::Dotnet => ("dotnet", Some("Install .NET tools")),
BackendType::Forgejo => ("forgejo", Some("Install from Forgejo releases")),
BackendType::Gem => ("gem", Some("Install Ruby gems")),
BackendType::Github => ("github", Some("Install from GitHub releases")),
BackendType::Gitlab => ("gitlab", Some("Install from GitLab releases")),
BackendType::Go => ("go", Some("Install Go modules")),
BackendType::Npm => ("npm", Some("Install npm packages globally")),
BackendType::Pipx => ("pipx", Some("Install Python CLI tools")),
BackendType::Spm => ("spm", Some("Install Swift packages")),
BackendType::Http => ("http", Some("Download files from HTTP URLs")),
BackendType::S3 => ("s3", Some("Download from S3 buckets")),
BackendType::Ubi => ("ubi", Some("Universal Binary Installer")),
BackendType::Core
| BackendType::Vfox
| BackendType::VfoxBackend(_)
| BackendType::Unknown => continue,
};
if backend_type.is_experimental() && !Settings::get().experimental {
continue;
}
backends.push(BackendInfo {
name: name.to_string(),
description: description.map(|s| s.to_string()),
});
}
for (plugin_name, plugin_type) in install_state::list_plugins().iter() {
if *plugin_type == PluginType::VfoxBackend {
backends.push(BackendInfo {
name: plugin_name.clone(),
description: Some(format!("Plugin-provided backend: {}", plugin_name)),
});
}
}
backends
}
}
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Edit {
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(verbatim_doc_comment, value_hint = ValueHint::FilePath)]
path: Option<PathBuf>,
#[clap(long, short, verbatim_doc_comment, value_hint = ValueHint::FilePath)]
tool_versions: Option<PathBuf>,
}
#[derive(Debug, Clone)]
struct DetectedTool {
name: String,
version: Option<String>,
#[allow(dead_code)]
source: String,
}
impl Edit {
pub async fn run(self) -> Result<()> {
let path = self
.path
.clone()
.unwrap_or_else(|| PathBuf::from(&*env::MISE_DEFAULT_CONFIG_FILENAME));
if let Some(tool_versions) = &self.tool_versions {
let doc = self.tool_versions(tool_versions).await?;
if self.dry_run {
info!("would write to {}", display_path(&path));
miseprintln!("{doc}");
} else {
info!("writing to {}", display_path(&path));
file::write(&path, doc)?;
}
} else if self.should_run_interactive() {
self.interactive(&path).await?;
} else {
let doc = self.default();
if self.dry_run {
info!("would write to {}", display_path(&path));
miseprintln!("{doc}");
} else {
info!("writing to {}", display_path(&path));
file::write(&path, doc)?;
}
}
Ok(())
}
fn should_run_interactive(&self) -> bool {
!Settings::get().yes && console::user_attended_stderr()
}
async fn interactive(&self, path: &Path) -> Result<()> {
use crate::ui::progress_report::ProgressReport;
let title = format!("mise {} by @jdx", &*VERSION_PLAIN);
let pr = ProgressReport::new("edit".into());
pr.set_message("Loading...".into());
let mut editor = if path.exists() {
pr.set_message("Loading config...".into());
InteractiveConfig::open(path.to_path_buf()).map_err(|e| eyre!(e))?
} else {
InteractiveConfig::new(path.to_path_buf())
};
editor = editor
.title(&title)
.dry_run(self.dry_run)
.with_tool_provider(Box::new(MiseToolProvider))
.with_version_provider(Box::new(MiseVersionProvider))
.with_backend_provider(Box::new(MiseBackendProvider));
pr.set_message("Detecting tools...".into());
let detected = detect_tools();
for tool in detected {
let version = tool.version.unwrap_or_else(|| "latest".to_string());
editor.add_tool(&tool.name, &version);
}
if Settings::get().experimental {
pr.set_message("Detecting prepare providers...".into());
let cwd = env::current_dir().unwrap_or_default();
let prepare_providers = crate::prepare::detect_applicable_providers(&cwd);
for provider in prepare_providers {
editor.add_prepare(&provider);
}
}
pr.finish_with_icon("Ready".into(), ProgressIcon::Success);
match editor.run().await {
Ok(ConfigResult::Saved(content)) => {
if self.dry_run {
info!("would write to {}", display_path(path));
miseprintln!("{content}");
} else {
info!("saved to {}", display_path(path));
}
}
Ok(ConfigResult::Cancelled) => {
info!("cancelled");
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
std::process::exit(130);
}
Err(e) => return Err(eyre!(e)),
}
Ok(())
}
async fn tool_versions(&self, tool_versions: &Path) -> Result<String> {
let to =
config_file::parse_or_init(&PathBuf::from(&*env::MISE_DEFAULT_CONFIG_FILENAME)).await?;
let from = config_file::parse(tool_versions).await?;
let tools = from.to_tool_request_set()?.tools;
for (ba, tools) in tools {
to.replace_versions(&ba, tools)?;
}
to.dump()
}
fn default(&self) -> String {
formatdoc! {r#"
# mise config files are hierarchical. mise will find all of the config files
# in all parent directories and merge them together.
# You might have a structure like:
#
# * ~/work/project/mise.toml # a config file for a specific work project
# * ~/work/mise.toml # a config file for projects related to work
# * ~/.config/mise/config.toml # the global config file
# * /etc/mise/config.toml # the system config file
#
# This setup allows you to define default versions and configuration across
# all projects but override them for specific projects.
# [env]
# NODE_ENV = "development"
# mise.file = ".env" # load vars from a dotenv file
# mise.path = "./node_modules/.bin" # add a directory to PATH
# [tools]
# node = "22"
# python = "3.12"
# go = "latest"
"#}
}
}
fn detect_tools() -> Vec<DetectedTool> {
let cwd = env::current_dir().unwrap_or_default();
let mut detected = Vec::new();
let mut seen_tools = std::collections::HashSet::new();
for (name, tool) in REGISTRY.iter() {
if tool.detect.is_empty() {
continue;
}
for detect_file in tool.detect.iter() {
let path = cwd.join(detect_file);
if path.exists() && !seen_tools.contains(*name) {
let version = extract_version(name, &path);
detected.push(DetectedTool {
name: name.to_string(),
version,
source: detect_file.to_string(),
});
seen_tools.insert(*name);
break; }
}
}
detected.sort_by(|a, b| a.name.cmp(&b.name));
detected
}
fn extract_version(tool: &str, path: &Path) -> Option<String> {
let filename = path.file_name()?.to_str()?;
let content = file::read_to_string(path).ok()?;
match (tool, filename) {
("node", "package.json") => {
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
json.get("engines")
.and_then(|e| e.get("node"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
("python", "pyproject.toml") => {
let doc: toml::Value = toml::from_str(&content).ok()?;
doc.get("project")
.and_then(|p| p.get("requires-python"))
.and_then(|v| v.as_str())
.map(|s| {
s.trim_start_matches(|c: char| !c.is_ascii_digit())
.to_string()
})
.filter(|s| !s.is_empty())
}
("go", "go.mod") => content
.lines()
.find(|line| line.starts_with("go "))
.map(|line| line.trim_start_matches("go ").trim().to_string()),
(_, f) if f.starts_with('.') && f.ends_with("-version") => {
let v = content.trim().to_string();
if v.is_empty() { None } else { Some(v) }
}
(_, ".nvmrc") => {
let v = content.trim().to_string();
if v.is_empty() { None } else { Some(v) }
}
_ => None,
}
}
pub static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise edit</bold> <dim># edit mise.toml interactively</dim>
$ <bold>mise edit .mise.toml</bold> <dim># edit a specific file</dim>
$ <bold>mise edit -y</bold> <dim># skip interactive editor</dim>
$ <bold>mise edit -n</bold> <dim># preview without writing</dim>
"#
);