Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/dev-tools/backends/vfox.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ mise install my-plugin:[email protected]
mise use my-plugin:some-tool@latest
```

### Install from Zip File

```bash
# Install a plugin from a zip file over HTTPS
mise plugin install <plugin-name> <zip-url>
# Example: Installing a plugin from a zip file
mise plugin install vfox-cmake https://github.com/mise-plugins/vfox-cmake/archive/refs/heads/main.zip
```

For more information, see:

- [Using Plugins](../../plugin-usage.md) - End-user guide
Expand Down
10 changes: 10 additions & 0 deletions docs/plugin-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ mise plugin install <plugin-name> <repository-url>
mise plugin install vfox-npm https://github.com/jdx/vfox-npm
```

### From Zip File

```bash
# Install a plugin from a zip file over HTTPS
mise plugin install <plugin-name> <zip-url>

# Example: Installing a plugin from a zip file
mise plugin install tiny https://github.com/mise-plugins/mise-tiny.git
```

### From Local Directory

```bash
Expand Down
8 changes: 8 additions & 0 deletions e2e/plugins/test_plugin_install
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ rm -rf "$MISE_DATA_DIR/plugins/tiny"
mise plugin install tiny https://github.com/mise-plugins/rtx-tiny
assert_contains "mise plugin ls" "tiny"

rm -rf "$MISE_DATA_DIR/plugins/tiny"
mise plugin install tiny https://github.com/mise-plugins/mise-tiny/archive/refs/heads/main.zip
assert_contains "mise plugin ls" "tiny"

rm -rf "$MISE_DATA_DIR/plugins/vfox-cmake"
mise plugin install vfox-cmake https://github.com/mise-plugins/vfox-cmake/archive/refs/heads/main.zip
assert_contains "mise plugin ls" "vfox-cmake"

rm -rf "$MISE_DATA_DIR/plugins/zprint"
mise plugin install --all
assert_contains "mise plugin ls" "zprint"
Expand Down
77 changes: 55 additions & 22 deletions src/plugins/asdf_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use crate::config::{Config, Settings};
use crate::errors::Error::PluginNotInstalled;
use crate::file::{display_path, remove_all};
use crate::git::{CloneOptions, Git};
use crate::plugins::{Plugin, Script, ScriptManager};
use crate::http::HTTP;
use crate::plugins::{Plugin, PluginSource, Script, ScriptManager};
use crate::result::Result;
use crate::timeout::run_with_timeout;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::ui::prompt;
use crate::{dirs, env, exit, lock_file, registry};
use crate::{dirs, env, exit, file, lock_file, registry};
use async_trait::async_trait;
use clap::Command;
use console::style;
Expand Down Expand Up @@ -180,6 +181,25 @@ impl AsdfPlugin {
})
.collect()
}
async fn install_from_zip(&self, url: &str, pr: &dyn SingleReport) -> eyre::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_archive = temp_dir.path().join("archive.zip");
HTTP.download_file(url, &temp_archive, Some(pr)).await?;

pr.set_message("extracting zip file".to_string());

let strip_components = file::should_strip_components(&temp_archive, file::TarFormat::Zip)?;

file::unzip(
&temp_archive,
&self.plugin_path,
&file::ZipOptions {
strip_components: if strip_components { 1 } else { 0 },
},
)?;

Ok(())
}
}

#[async_trait]
Expand Down Expand Up @@ -327,35 +347,48 @@ impl Plugin for AsdfPlugin {

async fn install(&self, config: &Arc<Config>, pr: &dyn SingleReport) -> eyre::Result<()> {
let repository = self.get_repo_url(config)?;
let (repo_url, repo_ref) = Git::split_url_and_ref(&repository);
let source = PluginSource::parse(&repository);
debug!("asdf_plugin[{}]:install {:?}", self.name, repository);

if self.is_installed() {
self.uninstall(pr).await?;
}

if regex!(r"^[/~]").is_match(&repo_url) {
Err(eyre!(
r#"Invalid repository URL: {repo_url}
match source {
PluginSource::Zip { url } => {
self.install_from_zip(&url, pr).await?;
self.exec_hook(pr, "post-plugin-add")?;
pr.finish_with_message(url.to_string());
Ok(())
}
PluginSource::Git {
url: repo_url,
git_ref,
} => {
if regex!(r"^[/~]").is_match(&repo_url) {
Err(eyre!(
r#"Invalid repository URL: {repo_url}
If you are trying to link to a local directory, use `mise plugins link` instead.
Plugins could support local directories in the future but for now a symlink is required which `mise plugins link` will create for you."#
))?;
}
let git = Git::new(&self.plugin_path);
pr.set_message(format!("clone {repo_url}"));
git.clone(&repo_url, CloneOptions::default().pr(pr))?;
if let Some(ref_) = &repo_ref {
pr.set_message(format!("check out {ref_}"));
git.update(Some(ref_.to_string()))?;
}
self.exec_hook(pr, "post-plugin-add")?;
))?;
}
let git = Git::new(&self.plugin_path);
pr.set_message(format!("clone {repo_url}"));
git.clone(&repo_url, CloneOptions::default().pr(pr))?;
if let Some(ref_) = &git_ref {
pr.set_message(format!("check out {ref_}"));
git.update(Some(ref_.to_string()))?;
}
self.exec_hook(pr, "post-plugin-add")?;

let sha = git.current_sha_short()?;
pr.finish_with_message(format!(
"{repo_url}#{}",
style(&sha).bright().yellow().for_stderr(),
));
Ok(())
let sha = git.current_sha_short()?;
pr.finish_with_message(format!(
"{repo_url}#{}",
style(&sha).bright().yellow().for_stderr(),
));
Ok(())
}
}
}

fn external_commands(&self) -> eyre::Result<Vec<Command>> {
Expand Down
106 changes: 106 additions & 0 deletions src/plugins/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::errors::Error::PluginNotInstalled;
use crate::git::Git;
use crate::plugins::asdf_plugin::AsdfPlugin;
use crate::plugins::vfox_plugin::VfoxPlugin;
use crate::toolset::install_state;
Expand Down Expand Up @@ -296,3 +297,108 @@ impl Display for PluginEnum {
write!(f, "{}", self.name())
}
}

#[derive(Debug, Clone)]
pub enum PluginSource {
/// Git repository with URL and optional ref
Git {
url: String,
git_ref: Option<String>,
},
/// Zip file accessible via HTTPS
Zip { url: String },
}

impl PluginSource {
pub fn parse(repository: &str) -> Self {
// Split Parameters
let url_path = repository
.split('?')
.next()
.unwrap_or(repository)
.split('#')
.next()
.unwrap_or(repository);
// Check if it's a zip file (ends with -zip)
if url_path.to_lowercase().ends_with(".zip") {
return PluginSource::Zip {
url: repository.to_string(),
};
}
// Otherwise treat as git repository
let (url, git_ref) = Git::split_url_and_ref(repository);
PluginSource::Git {
url: url.to_string(),
git_ref: git_ref.map(|s| s.to_string()),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_plugin_source_parse_git() {
// Test parsing Git URL
let source = PluginSource::parse("https://github.com/user/plugin.git");
match source {
PluginSource::Git { url, git_ref } => {
assert_eq!(url, "https://github.com/user/plugin.git");
assert_eq!(git_ref, None);
}
_ => panic!("Expected a git plugin"),
}
}

#[test]
fn test_plugin_source_parse_git_with_ref() {
// Test parsing Git URL with refs
let source = PluginSource::parse("https://github.com/user/plugin.git#v1.0.0");
match source {
PluginSource::Git { url, git_ref } => {
assert_eq!(url, "https://github.com/user/plugin.git");
assert_eq!(git_ref, Some("v1.0.0".to_string()));
}
_ => panic!("Expected a git plugin"),
}
}

#[test]
fn test_plugin_source_parse_zip() {
// Test parsing zip URL
let source = PluginSource::parse("https://example.com/plugins/my-plugin.zip");
match source {
PluginSource::Zip { url } => {
assert_eq!(url, "https://example.com/plugins/my-plugin.zip");
}
_ => panic!("Expected a Zip source"),
}
}

#[test]
fn test_plugin_source_parse_uppercase_zip_with_query() {
// Test parsing zip URL with query
let source =
PluginSource::parse("https://example.com/plugins/my-plugin.ZIP?version=v1.0.0");
match source {
PluginSource::Zip { url } => {
assert_eq!(
url,
"https://example.com/plugins/my-plugin.ZIP?version=v1.0.0"
);
}
_ => panic!("Expected a Zip source"),
}
}

#[test]
fn test_plugin_source_parse_edge_cases() {
// Test parsing git url which contains `.zip`
let source = PluginSource::parse("https://example.com/.zip/plugin");
match source {
PluginSource::Git { .. } => {}
_ => panic!("Expected a git plugin"),
}
}
}
74 changes: 53 additions & 21 deletions src/plugins/vfox_plugin.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::file::{display_path, remove_all};
use crate::git::{CloneOptions, Git};
use crate::plugins::Plugin;
use crate::http::HTTP;
use crate::plugins::{Plugin, PluginSource};
use crate::result::Result;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::{config::Config, dirs, registry};
use crate::{config::Config, dirs, file, registry};
use async_trait::async_trait;
use console::style;
use contracts::requires;
Expand Down Expand Up @@ -88,6 +89,25 @@ impl VfoxPlugin {
let rx = vfox.log_subscribe();
(vfox, rx)
}

async fn install_from_zip(&self, url: &str, pr: &dyn SingleReport) -> eyre::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_archive = temp_dir.path().join("archive.zip");
HTTP.download_file(url, &temp_archive, Some(pr)).await?;

pr.set_message("extracting zip file".to_string());

let strip_components = file::should_strip_components(&temp_archive, file::TarFormat::Zip)?;

file::unzip(
&temp_archive,
&self.plugin_path,
&file::ZipOptions {
strip_components: if strip_components { 1 } else { 0 },
},
)?;
Ok(())
}
}

#[async_trait]
Expand Down Expand Up @@ -208,34 +228,46 @@ impl Plugin for VfoxPlugin {

async fn install(&self, config: &Arc<Config>, pr: &dyn SingleReport) -> eyre::Result<()> {
let repository = self.get_repo_url(config)?;
let (repo_url, repo_ref) = Git::split_url_and_ref(repository.as_str());
let source = PluginSource::parse(repository.as_str());
debug!("vfox_plugin[{}]:install {:?}", self.name, repository);

if self.is_installed() {
self.uninstall(pr).await?;
}

if regex!(r"^[/~]").is_match(&repo_url) {
Err(eyre!(
r#"Invalid repository URL: {repo_url}
match source {
PluginSource::Zip { url } => {
self.install_from_zip(&url, pr).await?;
pr.finish_with_message(url.to_string());
Ok(())
}
PluginSource::Git {
url: repo_url,
git_ref,
} => {
if regex!(r"^[/~]").is_match(&repo_url) {
Err(eyre!(
r#"Invalid repository URL: {repo_url}
If you are trying to link to a local directory, use `mise plugins link` instead.
Plugins could support local directories in the future but for now a symlink is required which `mise plugins link` will create for you."#
))?;
}
let git = Git::new(&self.plugin_path);
pr.set_message(format!("clone {repo_url}"));
git.clone(&repo_url, CloneOptions::default().pr(pr))?;
if let Some(ref_) = &repo_ref {
pr.set_message(format!("git update {ref_}"));
git.update(Some(ref_.to_string()))?;
}
))?;
}
let git = Git::new(&self.plugin_path);
pr.set_message(format!("clone {repo_url}"));
git.clone(&repo_url, CloneOptions::default().pr(pr))?;
if let Some(ref_) = &git_ref {
pr.set_message(format!("git update {ref_}"));
git.update(Some(ref_.to_string()))?;
}

let sha = git.current_sha_short()?;
pr.finish_with_message(format!(
"{repo_url}#{}",
style(&sha).bright().yellow().for_stderr(),
));
Ok(())
let sha = git.current_sha_short()?;
pr.finish_with_message(format!(
"{repo_url}#{}",
style(&sha).bright().yellow().for_stderr(),
));
Ok(())
}
}
}
}

Expand Down
Loading