Skip to content

Provider Development Guide ​

This guide explains how to create a new provider for vx. The recommended approach is Starlark-first: write a provider.star file and let vx handle the rest. For advanced use cases that require custom Rust logic, see the Custom Rust Provider section.

Two Approaches ​

ApproachWhen to UseEffort
provider.star (recommended)GitHub releases, archive/binary downloads, PyPI/npm tools, system package manager fallbackMinutes
Custom Rust ProviderCustom install logic, complex version parsing, non-standard protocolsHours

For the vast majority of tools, a provider.star file is all you need. See the Manifest-Driven Providers Guide for the complete reference. Here is a condensed walkthrough.

Minimal Example ​

python
# crates/vx-providers/mytool/provider.star
load("@vx//stdlib:github.star", "make_fetch_versions", "github_asset_url")
load("@vx//stdlib:env.star",    "env_prepend")

# --- Metadata ---
name        = "mytool"
description = "My awesome tool"
homepage    = "https://github.com/myorg/mytool"
repository  = "https://github.com/myorg/mytool"
license     = "MIT"
ecosystem   = "devtools"

runtimes = [
    {
        "name":        "mytool",
        "executable":  "mytool",
        "description": "My tool runtime",
        "priority":    100,
        "test_commands": [
            {"command": "{executable} --version", "name": "version_check"},
        ],
    },
]

permissions = {
    "http": ["api.github.com", "github.com"],
    "fs":   [],
    "exec": [],
}

# --- Logic ---
fetch_versions = make_fetch_versions("myorg", "mytool")

def download_url(ctx, version):
    os   = ctx.platform.os
    arch = ctx.platform.arch
    triples = {
        "windows/x64":  "x86_64-pc-windows-msvc",
        "macos/x64":    "x86_64-apple-darwin",
        "macos/arm64":  "aarch64-apple-darwin",
        "linux/x64":    "x86_64-unknown-linux-musl",
        "linux/arm64":  "aarch64-unknown-linux-gnu",
    }
    triple = triples.get("{}/{}".format(os, arch))
    if not triple:
        return None
    ext   = "zip" if os == "windows" else "tar.gz"
    asset = "mytool-{}-{}.{}".format(version, triple, ext)
    return github_asset_url("myorg", "mytool", "v" + version, asset)

def install_layout(ctx, version):
    os  = ctx.platform.os
    exe = "mytool.exe" if os == "windows" else "mytool"
    return {
        "type":             "archive",
        "strip_prefix":     "mytool-{}".format(version),
        "executable_paths": [exe, "mytool"],
    }

def store_root(ctx):
    return ctx.vx_home + "/store/mytool"

def get_execute_path(ctx, version):
    os  = ctx.platform.os
    exe = "mytool.exe" if os == "windows" else "mytool"
    return ctx.install_dir + "/" + exe

def post_install(_ctx, _version):
    return None

def environment(ctx, _version):
    return [env_prepend("PATH", ctx.install_dir)]

Required Files ​

crates/vx-providers/mytool/
├── provider.star     # All logic and metadata (required)
├── Cargo.toml        # Package configuration
├── src/lib.rs        # Minimal Rust shim
└── build.rs          # Rebuild trigger for provider.star changes

provider.star — metadata and logic combined:

python
# provider.star
name        = "mytool"
description = "My awesome tool"
homepage    = "https://github.com/myorg/mytool"
repository  = "https://github.com/myorg/mytool"
ecosystem   = "devtools"
license     = "MIT"

runtimes = [
    runtime_def("mytool"),
]

permissions = github_permissions()

def fetch_versions(ctx):
    return fetch_versions_from_github(ctx, "myorg", "mytool")

def download_url(ctx, version, platform):
    return github_asset_url(ctx, "myorg", "mytool", version, platform)

Required Functions Checklist ​

Every provider.star must implement:

FunctionSignatureNotes
fetch_versionsfetch_versions(ctx) or make_fetch_versions(...)Returns version list
download_urldownload_url(ctx, version) -> str|NoneReturns download URL
install_layoutinstall_layout(ctx, version) -> dict|NoneReturns install descriptor
store_rootstore_root(ctx) -> strReturns store path
get_execute_pathget_execute_path(ctx, version) -> strReturns executable path
post_installpost_install(ctx, version) -> NonePost-install hook
environmentenvironment(ctx, version) -> listReturns env operations

Optional functions:

FunctionSignatureNotes
system_installsystem_install(ctx) -> dictPackage manager fallback
depsdeps(ctx, version) -> listRuntime dependencies
uninstalluninstall(ctx, version) -> NoneCustom uninstall logic

Registering a Built-in Provider ​

After creating provider.star and provider.toml, register the provider in the Rust registry so it is loaded at startup:

crates/vx-starlark/src/registry.rs (or equivalent registry file):

rust
// Add the provider directory name to the built-in list
pub const BUILTIN_PROVIDERS: &[&str] = &[
    "node",
    "go",
    // ... existing providers ...
    "mytool",   // ← add here
];

No Rust code is needed beyond this registration line.


Approach 2: Custom Rust Provider ​

Use this approach only when provider.star is insufficient — for example:

  • Custom authentication flows
  • Non-HTTP install sources (e.g., S3, internal registries)
  • Complex post-install logic that Starlark cannot express
  • Providers that wrap other Rust crates

Directory Structure ​

crates/vx-providers/mytool/
├── Cargo.toml
├── provider.star     # Still recommended even with Rust code
├── provider.toml
└── src/
    ├── lib.rs        # Module exports
    ├── provider.rs   # Provider implementation
    └── runtime.rs    # Runtime implementation

Cargo.toml ​

toml
[package]
name        = "vx-provider-mytool"
version.workspace   = true
edition.workspace   = true
license.workspace   = true
description = "vx provider for MyTool"

[dependencies]
vx-runtime-core = { workspace = true }
vx-runtime      = { workspace = true }
async-trait     = { workspace = true }
anyhow          = { workspace = true }
serde_json      = { workspace = true }
tracing         = { workspace = true }

Implement the Runtime Trait ​

rust
// src/runtime.rs
use async_trait::async_trait;
use vx_runtime::{Runtime, RuntimeContext, VersionInfo, Ecosystem, Platform};
use anyhow::Result;

pub struct MyToolRuntime;

impl MyToolRuntime {
    pub fn new() -> Self { Self }
}

#[async_trait]
impl Runtime for MyToolRuntime {
    // ── Required ──────────────────────────────────────────────────────────

    fn name(&self) -> &str { "mytool" }

    async fn fetch_versions(&self, ctx: &RuntimeContext) -> Result<Vec<VersionInfo>> {
        let url = "https://api.github.com/repos/myorg/mytool/releases";
        let response: serde_json::Value = ctx.http.get_json_value(url).await?;

        let versions = response
            .as_array()
            .unwrap_or(&vec![])
            .iter()
            .filter_map(|r| {
                let tag = r["tag_name"].as_str()?;
                let version = tag.strip_prefix('v').unwrap_or(tag);
                Some(VersionInfo {
                    version:    version.to_string(),
                    prerelease: r["prerelease"].as_bool().unwrap_or(false),
                    ..Default::default()
                })
            })
            .collect();

        Ok(versions)
    }

    // ── Optional ──────────────────────────────────────────────────────────

    fn description(&self) -> &str { "MyTool - A fantastic development tool" }

    fn aliases(&self) -> &[&str] { &["mt"] }

    fn ecosystem(&self) -> Ecosystem { Ecosystem::Unknown }

    async fn download_url(&self, version: &str, platform: &Platform) -> Result<Option<String>> {
        let triple = match (platform.os.as_str(), platform.arch.as_str()) {
            ("windows", "x86_64") => "x86_64-pc-windows-msvc",
            ("macos",   "x86_64") => "x86_64-apple-darwin",
            ("macos",   "aarch64") => "aarch64-apple-darwin",
            ("linux",   "x86_64") => "x86_64-unknown-linux-musl",
            ("linux",   "aarch64") => "aarch64-unknown-linux-gnu",
            _ => return Ok(None),
        };
        let ext   = if platform.os == "windows" { "zip" } else { "tar.gz" };
        let asset = format!("mytool-{}-{}.{}", version, triple, ext);
        Ok(Some(format!(
            "https://github.com/myorg/mytool/releases/download/v{}/{}",
            version, asset
        )))
    }
}

Implement the Provider Trait ​

rust
// src/provider.rs
use std::sync::Arc;
use vx_runtime::{Provider, Runtime};
use crate::runtime::MyToolRuntime;

pub struct MyToolProvider;

impl Provider for MyToolProvider {
    fn name(&self) -> &str { "mytool" }

    fn description(&self) -> &str { "MyTool development tool" }

    fn runtimes(&self) -> Vec<Arc<dyn Runtime>> {
        vec![Arc::new(MyToolRuntime::new())]
    }
}

Export from lib.rs ​

rust
// src/lib.rs
mod provider;
mod runtime;

pub use provider::MyToolProvider;
pub use runtime::MyToolRuntime;

Register the Provider ​

Add to crates/vx-cli/Cargo.toml:

toml
[dependencies]
vx-provider-mytool = { path = "../vx-providers/mytool" }

Add to crates/vx-cli/src/registry.rs:

rust
use vx_provider_mytool::MyToolProvider;

pub fn create_registry() -> ProviderRegistry {
    let mut registry = ProviderRegistry::new();
    registry.register(Box::new(MyToolProvider));
    // ... other providers
    registry
}

Lifecycle Hooks ​

rust
#[async_trait]
impl Runtime for MyToolRuntime {
    /// Called after extraction — rename files, set permissions
    fn post_extract(&self, _version: &str, install_path: &PathBuf) -> Result<()> {
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let exe = install_path.join("mytool");
            if exe.exists() {
                let mut perms = std::fs::metadata(&exe)?.permissions();
                perms.set_mode(0o755);
                std::fs::set_permissions(&exe, perms)?;
            }
        }
        Ok(())
    }

    /// Called after successful installation
    async fn post_install(&self, _version: &str, _ctx: &RuntimeContext) -> Result<()> {
        // Run initialization, install bundled tools, etc.
        Ok(())
    }
}

Testing ​

Unit Tests ​

Place tests in tests/ (not inline #[cfg(test)] modules):

crates/vx-providers/mytool/tests/
├── provider_tests.rs
└── runtime_tests.rs
rust
// tests/runtime_tests.rs
use rstest::rstest;
use vx_provider_mytool::MyToolRuntime;
use vx_runtime::Runtime;

#[rstest]
fn test_runtime_name() {
    let runtime = MyToolRuntime::new();
    assert_eq!(runtime.name(), "mytool");
}

#[rstest]
fn test_aliases() {
    let runtime = MyToolRuntime::new();
    assert!(runtime.aliases().contains(&"mt"));
}

#[tokio::test]
async fn test_download_url_linux() {
    let runtime = MyToolRuntime::new();
    let platform = Platform { os: "linux".into(), arch: "x86_64".into() };
    let url = runtime.download_url("1.0.0", &platform).await.unwrap();
    assert!(url.is_some());
    assert!(url.unwrap().contains("1.0.0"));
}

Testing provider.star ​

For Starlark providers, test by running vx commands against a temporary VX_HOME:

bash
# Set a temp home and test
VX_HOME=/tmp/vx-test vx mytool --version

Checklist ​

provider.star Provider ​

  • [ ] provider.star created with all required functions
  • [ ] provider.toml created (metadata only, no layout fields)
  • [ ] license field set to SPDX identifier
  • [ ] runtimes list includes test_commands
  • [ ] download_url() covers all major platforms (windows/x64, macos/x64, macos/arm64, linux/x64, linux/arm64)
  • [ ] install_layout() returns correct strip_prefix and executable_paths
  • [ ] environment() returns a list (not a dict)
  • [ ] system_install() added if tool is available via brew/winget/choco
  • [ ] Provider registered in built-in registry
  • [ ] Tested on at least one platform

Custom Rust Provider (additional) ​

  • [ ] Cargo.toml created with workspace dependencies
  • [ ] Runtime trait implemented (name() + fetch_versions() required)
  • [ ] Provider trait implemented
  • [ ] Tests in tests/ directory (not inline)
  • [ ] Added to vx-cli/Cargo.toml dependencies
  • [ ] Registered in create_registry()

Reference Providers ​

Study these built-in providers as examples:

ProviderPatternLocation
ripgrepStandard GitHub binary, archive layoutcrates/vx-providers/ripgrep/
mesonPyPI package alias (uvx)crates/vx-providers/meson/
imagemagickHybrid: direct download (Linux) + system pkg (Win/Mac)crates/vx-providers/imagemagick/
nodeCustom Rust provider, multiple runtimes, LTS supportcrates/vx-providers/node/
goCustom Rust provider, official API version fetchingcrates/vx-providers/go/
uvCustom Rust provider, Python ecosystemcrates/vx-providers/uv/

See Also ​

Released under the MIT License.