docs/plugin-lua-modules.md
mise plugins have access to a comprehensive set of built-in Lua modules that provide common functionality. These modules are available in both backend plugins and tool plugins, making it easy to perform common operations like HTTP requests, JSON parsing, file operations, and more.
cmd - Execute shell commandsjson - Parse and generate JSONhttp - Make HTTP requests and downloadsfile - File system operationsenv - Environment variable operationsstrings - String manipulation utilitiessemver - Semantic version comparison and sortinghtml - HTML parsing and manipulationarchiver - Archive extractionlog - Structured loggingThe HTTP module provides functionality for making web requests and downloading files.
local http = require("http")
-- GET request
local resp, err = http.get({
url = "https://api.github.com/repos/owner/repo/releases",
headers = {
['User-Agent'] = "mise-plugin",
['Accept'] = "application/json"
}
})
if err ~= nil then
error("Request failed: " .. err)
end
if resp.status_code ~= 200 then
error("HTTP error: " .. resp.status_code)
end
local body = resp.body
local http = require("http")
-- HEAD request to check file info
local resp, err = http.head({
url = "https://example.com/file.tar.gz"
})
if err ~= nil then
error("HEAD request failed: " .. err)
end
local content_length = resp.headers['content-length']
local content_type = resp.headers['content-type']
local http = require("http")
-- Download file
local err = http.download_file({
url = "https://github.com/owner/repo/archive/v1.0.0.tar.gz",
headers = {
['User-Agent'] = "mise-plugin"
}
}, "/path/to/download.tar.gz")
if err ~= nil then
error("Download failed: " .. err)
end
try_*)The standard http.get, http.head, and http.download_file methods raise a Lua error on transport failures (timeouts, DNS errors, connection refused, etc.). Since pcall() cannot catch errors from async functions in this environment, non-raising variants are provided:
local http = require("http")
-- try_get: returns (resp, nil) on success, (nil, err_string) on failure
local resp, err = http.try_get({
url = "https://primary.example.com/index"
})
if err ~= nil then
-- fallback to another source
resp, err = http.try_get({ url = "https://fallback.example.com/index" })
end
-- try_head: same return convention as try_get
local resp, err = http.try_head({ url = "https://example.com/file.tar.gz" })
-- try_download_file: returns (true, nil) on success, (nil, err_string) on failure
local ok, err = http.try_download_file({
url = "https://example.com/archive.tar.gz"
}, "/path/to/download.tar.gz")
if err ~= nil then
error("Download failed: " .. err)
end
HTTP responses contain the following fields:
{
status_code = 200,
headers = {
['content-type'] = "application/json",
['content-length'] = "1234"
},
body = "response content"
}
The JSON module provides encoding and decoding functionality.
local json = require("json")
-- Encode table to JSON string
local obj = {
name = "mise-plugin",
version = "1.0.0",
tools = {"prettier", "eslint"}
}
local jsonStr = json.encode(obj)
-- Result: '{"name":"mise-plugin","version":"1.0.0","tools":["prettier","eslint"]}'
-- Decode JSON string to table
local decoded = json.decode(jsonStr)
print(decoded.name) -- "mise-plugin"
print(decoded.tools[1]) -- "prettier"
local json = require("json")
-- Safe JSON parsing
local success, result = pcall(json.decode, response_body)
if not success then
error("Failed to parse JSON: " .. result)
end
-- Use the parsed data
for _, item in ipairs(result) do
print(item.version)
end
The strings module provides various string manipulation utilities.
local strings = require("strings")
-- Split string into parts
local parts = strings.split("hello,world,test", ",")
print(parts[1]) -- "hello"
print(parts[2]) -- "world"
print(parts[3]) -- "test"
-- Join strings
local joined = strings.join({"hello", "world", "test"}, " - ")
print(joined) -- "hello - world - test"
-- Trim whitespace
local trimmed = strings.trim_space(" hello world ")
print(trimmed) -- "hello world"
local strings = require("strings")
-- Check prefixes and suffixes
local text = "hello world"
print(strings.has_prefix(text, "hello")) -- true
print(strings.has_suffix(text, "world")) -- true
print(strings.contains(text, "lo wo")) -- true
-- Trim specific characters
local trimmed = strings.trim("hello world", "world")
print(trimmed) -- "hello "
local strings = require("strings")
-- Common version string operations
local function normalize_version(version)
-- Remove 'v' prefix if present
version = strings.trim_prefix(version, "v")
-- Remove pre-release suffixes
local parts = strings.split(version, "-")
return parts[1]
end
local version = normalize_version("v1.2.3-beta.1") -- "1.2.3"
The semver module provides semantic version comparison and sorting functionality. This is useful for sorting version lists returned by Available() hooks.
local semver = require("semver")
-- Compare two versions
-- Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
local result = semver.compare("1.2.3", "1.2.4") -- -1
local result = semver.compare("2.0.0", "1.9.9") -- 1
local result = semver.compare("1.0.0", "1.0.0") -- 0
-- Handles numeric comparison correctly
local result = semver.compare("9.6.9", "9.6.24") -- -1 (not lexicographic!)
local result = semver.compare("10.0.0", "9.6.24") -- 1
local semver = require("semver")
-- Parse version string into numeric parts
local parts = semver.parse("1.2.3")
print(parts[1]) -- 1
print(parts[2]) -- 2
print(parts[3]) -- 3
-- Works with prefixes and suffixes
local parts = semver.parse("v1.2.3-beta") -- {1, 2, 3}
local semver = require("semver")
-- Sort array of version strings (ascending order)
local versions = {"1.10.0", "1.2.0", "1.9.0", "2.0.0"}
local sorted = semver.sort(versions)
-- Result: {"1.2.0", "1.9.0", "1.10.0", "2.0.0"}
local semver = require("semver")
-- Sort array of tables by a version field (ascending order)
local releases = {
{version = "1.10.0", url = "..."},
{version = "1.2.0", url = "..."},
{version = "1.9.0", url = "..."},
}
local sorted = semver.sort_by(releases, "version")
-- Result: sorted by version ascending
local http = require("http")
local semver = require("semver")
function PLUGIN:Available(ctx)
local resp, err = http.get({
url = "https://example.com/releases/"
})
if err ~= nil then
error("Failed to fetch versions: " .. err)
end
local result = {}
-- Parse versions from response...
for version in string.gmatch(resp.body, 'v([0-9]+%.[0-9]+%.[0-9]+)') do
table.insert(result, {version = version})
end
-- Sort versions semantically (ascending order - oldest first)
return semver.sort_by(result, "version")
end
local semver = require("semver")
-- Sort with custom comparator (descending order - newest first)
table.sort(versions, function(a, b)
return semver.compare(a.version, b.version) > 0
end)
-- Sort ascending (oldest first) - default for Available()
table.sort(versions, function(a, b)
return semver.compare(a.version, b.version) < 0
end)
The HTML module provides HTML parsing capabilities.
local html = require("html")
-- Parse HTML document
local doc = html.parse([[
<html>
<body>
<div id="version" class="info">1.2.3</div>
<ul class="downloads">
<li><a href="/download/v1.2.3.tar.gz">Source</a></li>
<li><a href="/download/v1.2.3.zip">Windows</a></li>
</ul>
</body>
</html>
]])
-- Extract text content
local version = doc:find("#version"):text() -- "1.2.3"
-- Extract attributes
local links = doc:find("a")
for _, link in ipairs(links) do
local href = link:attr("href")
local text = link:text()
print(text .. ": " .. href)
end
local html = require("html")
local doc = html.parse(html_content)
-- Find by ID
local element = doc:find("#version")
-- Find by class
local elements = doc:find(".download-link")
-- Find by tag
local links = doc:find("a")
-- Complex selectors
local specific_links = doc:find("ul.downloads a[href$='.tar.gz']")
local html = require("html")
local http = require("http")
function get_github_releases(owner, repo)
local resp, err = http.get({
url = "https://github.com/" .. owner .. "/" .. repo .. "/releases"
})
if err ~= nil then
error("Failed to fetch releases: " .. err)
end
local doc = html.parse(resp.body)
local releases = {}
-- Find all release tags
local release_elements = doc:find("a[href*='/releases/tag/']")
for _, element in ipairs(release_elements) do
local href = element:attr("href")
local version = href:match("/releases/tag/(.+)")
if version then
table.insert(releases, {
version = version,
url = "https://github.com" .. href
})
end
end
return releases
end
The archiver module provides functionality for extracting compressed archives.
local archiver = require("archiver")
-- Extract archive to directory
local err = archiver.decompress("archive.tar.gz", "extracted/")
if err ~= nil then
error("Extraction failed: " .. err)
end
-- Extract ZIP file
local err = archiver.decompress("package.zip", "destination/")
if err ~= nil then
error("ZIP extraction failed: " .. err)
end
local archiver = require("archiver")
local http = require("http")
function install_from_archive(download_url, install_path)
-- Download the archive
local archive_path = install_path .. "/download.tar.gz"
local err = http.download_file({
url = download_url
}, archive_path)
if err ~= nil then
error("Download failed: " .. err)
end
-- Extract to installation directory
local err = archiver.decompress(archive_path, install_path)
if err ~= nil then
error("Extraction failed: " .. err)
end
-- Clean up archive
os.remove(archive_path)
end
The file module provides file system operations.
local file = require("file")
-- Join path segments using the OS-specific separator
local full_path = file.join_path("/foo", "bar", "baz.txt")
print(full_path) -- On Unix: /foo/bar/baz.txt, on Windows: \foo\bar\baz.txt
The file.join_path(...) function joins any number of path segments using the correct separator for the current operating system. This is the recommended way to construct file paths in cross-platform plugins.
local file = require("file")
print(file.read("/path/to/file"))
local file = require("file")
file.symlink("/path/to/source", "/path/to/new-symlink")
local file = require("file")
if file.exists("important_file.txt") then
print("File exists")
else
print("File does not exist")
end
The env module provides environment variable operations.
local env = require("env")
-- Set environment variable
env.setenv("MY_VAR", "my_value")
To read variables in Lua, use
os.getenv("MY_VAR").
local env = require("env")
-- Get current PATH
local current_path = os.getenv("PATH")
-- Add to PATH
local new_path = "/usr/local/bin:" .. current_path
env.setenv("PATH", new_path)
-- Platform-specific PATH separator
local separator = package.config:sub(1,1) == '\\' and ";" or ":"
local paths = {"/usr/local/bin", "/opt/bin", current_path}
env.setenv("PATH", table.concat(paths, separator))
The cmd module provides shell command execution.
local cmd = require("cmd")
-- Execute command and get output
local output = cmd.exec("ls -la")
print("Directory listing:", output)
-- Execute command with error handling
local success, output = pcall(cmd.exec, "some-command")
if not success then
error("Command failed: " .. output)
end
local cmd = require("cmd")
-- Execute command in a specific directory
local output = cmd.exec("pwd", {cwd = "/tmp"})
print("Current directory:", output)
-- Execute command with custom environment variables
local result = cmd.exec("echo $TEST_VAR", {
cwd = "/path/to/project",
env = {TEST_VAR = "hello", NODE_ENV = "production"}
})
-- Install package in specific directory
local result = cmd.exec("npm install package-name", {cwd = "/path/to/project"})
The options table supports the following keys:
cwd (string): Set the working directory for the commandenv (table): Set environment variables for the command execution. These are merged on top of the inherited environment (see below).timeout (number): Set a timeout for command execution (future feature)When cmd.exec() is called from environment module hooks (MiseEnv, MisePath), the command automatically inherits the mise-constructed environment instead of the process environment. This includes environment variables set by preceding directives and _.path entries accumulated so far.
When the module directive has tools = true, the inherited environment also includes tool installation bin paths. This means mise-managed tools are directly callable:
[env]
_.my-plugin = { tools = true }
function PLUGIN:MiseEnv(ctx)
-- With tools=true, mise-managed tools are on PATH
local version = cmd.exec("node --version")
return {
{key = "NODE_VERSION", value = version:gsub("%s+", "")}
}
end
Without tools = true, only _.path directive entries and the original system PATH are available to cmd.exec().
Any explicit env options passed to cmd.exec() are merged on top of the inherited environment, allowing selective overrides.
local cmd = require("cmd")
-- Cross-platform command execution
local function is_windows()
return package.config:sub(1,1) == '\\'
end
local function get_os_info()
if is_windows() then
return cmd.exec("systeminfo")
else
return cmd.exec("uname -a")
end
end
local os_info = get_os_info()
print("OS Info:", os_info)
local http = require("http")
local json = require("json")
function fetch_npm_versions(package_name)
local resp, err = http.get({
url = "https://registry.npmjs.org/" .. package_name,
headers = {
['User-Agent'] = "mise-plugin"
}
})
if err ~= nil then
error("Failed to fetch package info: " .. err)
end
local package_info = json.decode(resp.body)
local versions = {}
for version, _ in pairs(package_info.versions) do
table.insert(versions, version)
end
-- Sort versions (simple string sort)
table.sort(versions)
return versions
end
local http = require("http")
local file = require("file")
function download_with_verification(url, dest_path, expected_sha256)
-- Download file
local err = http.download_file({
url = url,
headers = {
['User-Agent'] = "mise-plugin"
}
}, dest_path)
if err ~= nil then
error("Download failed: " .. err)
end
-- Verify file exists
if not file.exists(dest_path) then
error("Downloaded file not found")
end
-- Note: SHA256 verification would need additional implementation
-- This is a simplified example
print("Downloaded successfully to: " .. dest_path)
end
local file = require("file")
local json = require("json")
local strings = require("strings")
function parse_config_file(config_path)
if not file.exists(config_path) then
return {} -- Return empty config
end
local content = file.read(config_path)
if not content then
error("Failed to read config file: " .. config_path)
end
-- Trim whitespace
content = strings.trim_space(content)
-- Parse JSON
local success, config = pcall(json.decode, content)
if not success then
error("Invalid JSON in config file: " .. config_path)
end
return config
end
local http = require("http")
local html = require("html")
local strings = require("strings")
function scrape_versions_from_releases(base_url)
local resp, err = http.get({
url = base_url .. "/releases"
})
if err ~= nil then
error("Failed to fetch releases page: " .. err)
end
local doc = html.parse(resp.body)
local versions = {}
-- Find version tags
local version_elements = doc:find("h2 a[href*='/releases/tag/']")
for _, element in ipairs(version_elements) do
local version_text = element:text()
local version = strings.trim_space(version_text)
-- Remove 'v' prefix if present
version = strings.trim_prefix(version, "v")
if version and version ~= "" then
table.insert(versions, {
version = version,
url = base_url .. element:attr("href")
})
end
end
return versions
end
The log module provides structured logging that routes through Rust's log crate, respecting MISE_DEBUG and MISE_TRACE environment variables.
local log = require("log")
log.trace("detailed tracing info") -- only visible with MISE_TRACE=1
log.debug("debugging info") -- visible with MISE_DEBUG=1
log.info("status message") -- visible by default
log.warn("warning message") -- visible by default
log.error("error message") -- visible by default
All log functions accept multiple arguments of any type. Arguments are converted to strings via tostring() and joined with tab characters (\t), matching Lua's print() behavior:
log.info("version", version, "installed to", path)
-- Output: [plugin-name] version<TAB>1.0.0<TAB>installed to<TAB>/path
All log messages are automatically prefixed with [plugin_name]:
mise [INFO] [my-plugin] Installing version 1.0.0
print() is overridden to route through info!() level logging. This means:
print() output goes to stderr instead of stdout[plugin_name]-- These are equivalent:
print("hello", "world")
log.info("hello", "world")
The log module is also available as vfox.log:
local log = require("vfox").log
log.info("message")
Always handle errors gracefully:
local http = require("http")
local json = require("json")
function safe_api_call(url)
local resp, err = http.get({url = url})
if err ~= nil then
error("HTTP request failed: " .. err)
end
if resp.status_code ~= 200 then
error("API returned error: " .. resp.status_code .. " " .. resp.body)
end
local success, data = pcall(json.decode, resp.body)
if not success then
error("Failed to parse JSON response: " .. data)
end
return data
end
Implement caching for expensive operations:
local cache = {}
local cache_ttl = 3600 -- 1 hour
function cached_http_get(url)
local now = os.time()
local cache_key = url
-- Check cache
if cache[cache_key] and (now - cache[cache_key].timestamp) < cache_ttl then
return cache[cache_key].data
end
-- Fetch fresh data
local http = require("http")
local resp, err = http.get({url = url})
if err ~= nil then
error("HTTP request failed: " .. err)
end
-- Cache the result
cache[cache_key] = {
data = resp,
timestamp = now
}
return resp
end
Handle cross-platform differences:
local function get_platform_info()
local is_windows = package.config:sub(1,1) == '\\'
local cmd = require("cmd")
if is_windows then
return {
os = "windows",
arch = os.getenv("PROCESSOR_ARCHITECTURE") or "x64",
path_sep = "\\",
env_sep = ";"
}
else
local uname = cmd.exec("uname -s"):lower()
local arch = cmd.exec("uname -m")
return {
os = uname,
arch = arch,
path_sep = "/",
env_sep = ":"
}
end
end