Utility Modules
Beyond prompting and rendering, archetypes lean on a toolbox of utilities — some global, some loaded with require("archetect.*").
Serialization: format
Convert between Lua tables (or Contexts) and JSON/YAML/TOML:
-- Serialize
local yaml = format.to_yaml(context)
local json = format.to_json({ name = "billing", port = 8080 })
-- Parse
local config = format.from_toml(file.read("data/defaults.toml"))
context:merge(config)
The load-defaults pattern — ship structured data with your archetype instead of hardcoding it in Lua:
if file.exists("data/defaults.yaml") then
context:merge(format.from_yaml(file.read("data/defaults.yaml")))
end
Messaging: log and output
log.info("Rendering service module") -- to the log stream (verbosity-controlled)
log.debug(tostring(context)) -- diagnostic detail
output.print("Created " .. name) -- always shown to the user
output.banner("Next steps") -- prominent banner
Use log for what the archetype is doing (debuggability) and output for what the user should read (next steps, important notices).
Early exit
if not context:get("proceed") then
output.print("Nothing to do.")
exit() -- clean termination, not an error
end
Shell commands: archetect.shell
local shell = require("archetect.shell")
shell.run("cargo", { "fmt" }, { cwd = project_dir })
local rustc = shell.capture("rustc", { "--version" })
run streams output and raises on failure; capture returns trimmed stdout. Both accept cwd and env options.
:::warning Execution is gated
Shell execution is governed by the user's security policy — prompt for approval per command by default, with --allow-exec / configuration to pre-approve and forbidden to block. Design archetypes so command execution is a finishing touch, not a requirement; and remember headless CI runs need --allow-exec explicitly.
:::
Git: archetect.git
The classic finishing move — leave the generated project as a ready git repository:
local git = require("archetect.git")
local repo = git.init(project_dir, { branch = "main" })
repo:add_all()
repo:commit("initial commit")
git.init defaults the initial branch to main (deterministic across machines, whatever the host's git config says). The returned handle also offers add(patterns) (string or list), branch(name), checkout(name), remote_add(name, url), and push(remote, branch).
GitHub: archetect.github
local github = require("archetect.github")
if not github.repo_exists("acme/billing-service") then
local result = github.create_repo("acme/billing-service", { visibility = "private" })
if result.empty then
repo:remote_add("origin", "git@github.com:acme/billing-service.git")
repo:push("origin", "main")
end
end
Authentication uses the GITHUB_TOKEN environment variable, falling back to gh auth token if the GitHub CLI is logged in. create_repo reports { created, empty } so scripts can decide whether pushing is safe.
Archives: archetect.archive
local archive = require("archetect.archive")
archive.zip("dist", "release.zip") -- also: tar_gz, tar
Paths resolve relative to the render destination.
Your own modules
Anything in the archetype's lib/ directory is require-able:
-- lib/naming.lua
local M = {}
function M.artifact_id(org, name) return org .. "-" .. name end
return M
-- archetype.lua
local naming = require("naming")
context:set("artifact_id", naming.artifact_id(org, name))
Shared across archetypes, this same mechanism becomes Libraries.
Full signatures for everything here: format, log & output and shell, git, github & archive.