-
Notifications
You must be signed in to change notification settings - Fork 0
Extending Lua Plugin Cookbook
Prev: Writing Lua Plugins | Up: Extending ReqPack | Next: Testing Lua Plugins
This page shows practical recipes for common plugin patterns. For exact runtime contract, read Writing Lua Plugins.
Good baseline shape:
plugin = {}
function plugin.getName()
return REQPACK_PLUGIN_ID
end
function plugin.getVersion()
return "1.0.0"
end
function plugin.getRequirements()
return {}
end
function plugin.getCategories()
return { "test", "system" }
end
function plugin.getMissingPackages(packages)
return packages or {}
end
function plugin.install(context, packages)
context.tx.begin_step("install packages")
local result = context.exec.run("demo-pm install " .. packages[1].name)
if not result.success then
context.tx.failed("install failed")
return false
end
context.events.installed({ name = packages[1].name })
context.tx.success()
return true
end
function plugin.installLocal(context, path)
return context.exec.run("demo-pm install-local " .. path).success
end
function plugin.remove(context, packages)
return context.exec.run("demo-pm remove " .. packages[1].name).success
end
function plugin.update(context, packages)
return context.exec.run("demo-pm update " .. packages[1].name).success
end
function plugin.list(context)
return {}
end
function plugin.search(context, prompt)
return {}
end
function plugin.info(context, packageName)
return { name = packageName, version = "1.0.0" }
end
function plugin.init()
return true
end
function plugin.shutdown()
return true
end
return pluginPattern:
- metadata methods stay side-effect free
- action methods do real work
- use
context.tx.*andcontext.events.*around mutations - prefer
context.exec.run(...)overreqpack.exec.run(...)in action methods
ReqPack can batch many packages into one action call. Do not assume exactly one package unless plugin intentionally only supports that.
Simple batch pattern:
function plugin.install(context, packages)
if packages == nil or #packages == 0 then
return true
end
local names = {}
for _, pkg in ipairs(packages) do
table.insert(names, pkg.name)
end
context.tx.begin_step("install packages")
local result = context.exec.run("demo-pm install " .. table.concat(names, " "))
if not result.success then
context.tx.failed("install failed")
return false
end
context.events.installed(packages)
context.tx.success()
return true
endWhen plugin must compute one command per package, build command list first, then execute joined command or loop explicitly.
ReqPack may hand versioned specs as name@version in package name space depending on source.
Keep parsing local and boring.
local function split_name_version(pkg)
local raw = tostring(pkg.name or "")
local name, embedded = raw:match("^(.+)@([^@]+)$")
if name == nil then
return raw, pkg.version
end
if pkg.version ~= nil and pkg.version ~= "" then
return name, pkg.version
end
return name, embedded
endUse explicit pkg.version first when already populated.
For list, search, info, and outdated, return structured records and emit matching events.
Example:
function plugin.info(context, packageName)
local result = context.exec.run("demo-pm info " .. packageName)
if not result.success then
return {}
end
local item = {
name = packageName,
version = "4.5.6",
description = result.stdout,
}
context.events.informed(item)
return item
endWhy:
- better user output
- stronger
test-pluginassertions - better audit/SBOM follow-on behavior when plugin later adds exact resolution
Use context.host, not reqpack.host, when action should reflect current request-time snapshot.
Example:
function plugin.getRequirements()
return {}
end
function plugin.install(context, packages)
local osFamily = context.host.platform.osFamily
local arch = context.host.platform.arch
if osFamily == "linux" and arch == "x86_64" then
return context.exec.run("demo-pm install-linux64 " .. packages[1].name).success
end
return context.exec.run("demo-pm install-generic " .. packages[1].name).success
endUseful fields include:
context.host.os.idcontext.host.os.distroIdcontext.host.cpu.logicalCorescontext.host.cpu.physicalCorescontext.host.memory.totalBytes
If host data looks stale while testing, run rqp host refresh.
Proxy plugins should not try to act like real package managers after resolution fails. Resolve once, return target system, packages or local path, and flags.
Example pattern:
local function normalized_target(value)
local target = tostring(value or ""):lower()
if target == "" then
return nil
end
return target
end
function plugin.resolveProxyRequest(context, request)
local proxy = context.proxy or {}
local target = normalized_target(proxy.default)
if target == nil and proxy.targets ~= nil then
target = normalized_target(proxy.targets[1])
end
if target == nil then
context.log.error("proxy could not choose target")
return nil
end
local resolution = {
targetSystem = target,
flags = request.flags,
}
if request.usesLocalTarget then
resolution.localPath = request.localPath
else
resolution.packages = request.packages
end
return resolution
endRemember source-enforced rules:
- max proxy depth is
4 - cycles are rejected
- self-target is rejected
- returning
nilis failure, not pass-through - returned
packagesandlocalPathare mutually exclusive
Add this as soon as plugin can resolve exact package identity.
Minimal pattern:
function plugin.resolvePackage(context, package)
local result = context.exec.run("demo-pm exact-version " .. package.name)
if not result.success then
return nil
end
return {
action = package.action,
system = package.system,
name = package.name,
version = result.stdout:gsub("%s+$", ""),
sourcePath = package.sourcePath,
localTarget = package.localTarget,
flags = package.flags,
}
endThis improves:
- audit matching accuracy
- SBOM exactness
- unresolved-version handling
Use exec rules when plugin must react to command output while command is still running.
Example:
local rules = {
initial = "default",
rules = {
{
state = "default",
source = "line",
regex = "^Loaded (.+)$",
repeat = true,
actions = {
{ type = "log", level = "info", message = "${1}" },
},
},
{
source = "line",
regex = "^Progress ([0-9]+)%%$",
actions = {
{ type = "progress", percent = "${1}" },
},
},
},
}
local result = context.exec.run("demo-pm install huge-package", rules)Use line rules first. Move to PTY/screen rules only when tool renders interactive full-screen or prompt-driven output.
Important:
- malformed rule schema fails command immediately
- any
screenrule orsendaction switches runner to PTY mode - placeholders like
${1}use regex captures
Only add plugin.pack() when ecosystem has genuine native package build flow.
If you are building ReqPack-native .rqp, use builtin rqp pack instead.
Minimal pattern:
function plugin.pack(context, projectPath, outputPath, flags)
local result = context.exec.run("demo-pack build --project " .. projectPath .. " --output " .. outputPath)
if not result.success then
return false
end
context.artifacts.register(outputPath)
return true
endRules worth remembering:
- during
plugin.pack(), ReqPack temporarily allows writes underprojectPath - register artifact path strings with
context.artifacts.register(...) - if plugin reports success for explicit output path but artifact file is missing, ReqPack treats that as failure
Happy-path list case:
return {
name = "system fixture list",
request = {
action = "list",
system = "demo-system",
},
fakeExec = {
{
match = "demo-pm list",
exitCode = 0,
stdout = "alpha entry",
stderr = "",
success = true,
}
},
expect = {
success = true,
commands = { "demo-pm list" },
stdout = { "alpha entry" },
events = { "listed" },
resultCount = 1,
resultName = "alpha",
resultVersion = "1.2.3",
}
}Happy-path info case:
return {
name = "system fixture info",
request = {
action = "info",
system = "demo-system",
prompt = "delta",
},
fakeExec = {
{
match = "demo-pm info delta",
exitCode = 0,
stdout = "delta details",
stderr = "",
success = true,
}
},
expect = {
success = true,
commands = { "demo-pm info delta" },
stdout = { "delta details" },
events = { "informed" },
resultCount = 1,
resultName = "delta",
resultVersion = "4.5.6",
}
}Recommended mix for each plugin:
- one happy-path query case
- one mutating success case
- one mutating failure case
- one payload-format-sensitive case if parser logic matters
- keep wrapper thin; underlying package manager should still own package-manager semantics
- keep
getName(),getVersion(),getSecurityMetadata(), andplugin.fileExtensionssafe beforeinit() - implement
getMissingPackages()carefully because planning and recovery depend on it - prefer exact result records over placeholder query output in mature plugins
- use real smoke tests after hermetic
test-plugincoverage
Prev: Writing Lua Plugins | Up: Extending ReqPack | Next: Testing Lua Plugins
- User Guide
- Getting Started
- Command Reference
- Configuration
- Configuration Reference
- Security, Audit, and SBOM
- Output and Report Formats
- Remote Mode
- Remote Protocol Reference
- Using Native
rqpPackages - Troubleshooting