π A CLI tool written in Go to standardize project scaffolding, library creation, and internal microservices using predefined templates and architectural patterns. Say goodbye to boilerplate!
scaffold-cli is a universal, dynamically-extensible scaffolding engine. Scaffolds, versions,
dimensions, templates, selector values, and even the CLI flag names themselves are declared by
templates at runtime β none of it is hardcoded in the binary. The engine only knows how to walk an
inheritance chain and render; what gets generated comes entirely from a separate templates
repository (see Requirements below).
- Nothing hardcoded. Scaffolds, versions, dimensions, templates, selector values, and even CLI
flag names are declared entirely in the templates repository (
jig.yaml), not the binary β pointscaffoldat a different templates repo and it drives a completely different toolchain. - One inheritance chain, one precedence rule.
scaffold β version β dimension β template β overlay, later always wins β so a version can override a single file without duplicating everything else it doesn't change. - Inspect before you write anything.
--dry-run(which files would be produced),--print(what's actually in them), and--explain(which level of the chain contributed each file) all run the full resolution with nothing touching disk. - Values files, not just flags.
-f values.yamlsupplies the scaffold/template/name and every variable in one file;-fmay repeat (later wins), and a command-line flag always beats a file. lint --buildproves the output actually works, not just that templates parse β it runs each combination's ownverify:command (e.g.mvn test) against a real scratch build and exits non-zero on any failure, so it doubles as a CI gate for the templates repo.- Prebuilt binaries for Linux, macOS, and Windows (amd64 + arm64) on every tagged release.
- Go 1.26 or newer (to build from source).
- A templates repository laid out for this engine β see
scaffold-templates, the companion repo that
holds the actual scaffolds, versions, templates, and their
jig.yaml-driven inheritance tree.scaffold-cliitself ships with none of that content; it only knows how to read and render it. By default the CLI looks for a checkout of it in a folder literally namedscaffolding-codenext to where you run it β clone it under that name, or point elsewhere with--scaffolding-code=<path>(see Configuration for every way to set that path).
Every tagged release publishes prebuilt binaries for Linux, macOS, and Windows (amd64 + arm64) β see the Releases page. The install scripts below fetch the right one for your platform, verify its checksum, and put it on your PATH.
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/yusronMu77/scaffold-cli/main/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/yusronMu77/scaffold-cli/main/install.ps1 | iexBoth accept a pinned version (SCAFFOLD_CLI_VERSION=v0.3.0 / -Version v0.3.0) and a custom
install directory (SCAFFOLD_CLI_INSTALL_DIR / -InstallDir) if you'd rather not use the default.
Or grab an archive directly from the Releases page
and extract the scaffold (or scaffold.exe) binary yourself.
git clone https://github.com/yusronMu77/scaffold-cli.git
cd scaffold-cli
go build -o scaffold .This produces a scaffold (or scaffold.exe on Windows) binary in the current directory. Verify
it with:
./scaffold --versionThe --scaffolding-code=<path> flag can be skipped on every invocation by setting it once,
resolved in this order (first match wins):
--scaffolding-code=<path>on the command line.- The
SCAFFOLD_CODEenvironment variable. scaffolding_code: <path>in a.scaffold.yamlin the current directory.scaffolding_code: <path>in a.scaffold.yamlin your home directory.- A
scaffolding-codefolder next to thescaffoldexecutable itself. ./scaffolding-code, as a last resort.
Example .scaffold.yaml:
scaffolding_code: ../scaffold-templatesscaffold-cli has five commands: init to bootstrap a fresh templates root, list to browse
what's available, create to generate a project, lint to check that a templates repository is
healthy, and learn to draft a template from an existing example.
scaffold init [path]Writes a starter jig.yaml into path (default .), creating the directory if needed. It's a
pure local file write - no network calls, no git init - meant for starting a brand-new,
independent templates repository from zero rather than cloning
scaffold-templates. The starter registry is
deliberately empty (values: []), so list/create correctly report nothing to do until you
register your first scaffold; pass --force to overwrite an existing jig.yaml at path.
scaffold list # known scaffolds
scaffold list <scaffold> # versions, templates, and optional dimensions
scaffold list <scaffold> --full # same, plus every template's own tree and variables
scaffold list <scaffold> <template> # full selector tree and variables for that template<template> picks a value from the required dimension of the resolved version β it's not how
you pick the version itself. To browse a specific version, use its selector flag (--scaffold-version
by default, whatever the scaffold's jig.yaml names it as):
scaffold list spring-boot --scaffold-version=hello-worldIf that version has no templates/ dimension β it's a leaf, like hello-world above β list
prints its variables directly instead of asking for a <template>.
scaffold create <scaffold> <template> <name> [--flag=value ...]All three positional arguments (scaffold, template, name) can also come from a values file:
scaffold create -f values.yaml
scaffold create -f base.yaml -f prod.yaml --name=payment-canary # -f may repeat, later winsA values file is just the flag namespace without the dashes (--package=x becomes package: x).
Command-line flags always win over values-file entries.
Useful flags for inspecting before you write anything:
| Flag | What it shows |
|---|---|
--dry-run |
Which files would be produced |
--print |
What is actually in them, printed to stdout |
--explain |
Which level of the inheritance chain contributed each file |
--print-written is the one mode that does write: it performs a normal create and then echoes
the exact final content of every file written or spliced, so a caller wanting both the write and a
verifiable transcript of what landed gets both in one call instead of a --print pass followed by
a separate create. It cannot be combined with --dry-run/--print/--explain.
scaffold lint [<scaffold>] [--build]Renders every registered combination in memory and reports anything that breaks β a registered
value with no jig.yaml, a template that fails to parse, a variable with no default, and so on.
Add --build to also run each combination's own verify: commands against a real scratch build
(slower, but the only way to know the generated project actually builds). Exit code is non-zero on
any failure, so it works as a CI gate.
scaffold learn <path> --output=<dir> [--provider=anthropic|openai] [--model=...] [--base-url=...]
scaffold learn <path> --output=<dir> --draft=<path|-> # already-reasoned draft, no provider call
scaffold learn <path1> <path2> ... --output=<dir> # generalize across 2+ examples at once--output must be an empty (or not-yet-existing) directory; pass --force to write into one that
already holds something. Credential files (*.pem, id_rsa, .env, kubeconfig, ...) and
symlinks are never sent to a provider, and the ones skipped are listed before the call is made.
Points at one already-written example folder (a real controller, a CDK stack, ...), calls an LLM
once to separate invariant structure from variable names/paths/fields, and writes the result to
--output as a draft jig.yaml plus templated files. --output is required β the draft is a
candidate, not yet wired into any templates repository, so it's on you to review it (and move it
into place) before create/list/lint would ever see it. Regenerating afterward goes through the
same deterministic create path as every other template β zero further AI calls per instance.
A draft's file paths are relative to the scanned example folder itself, not the destination the
template will eventually write to once registered. Don't bake a real project's destination prefix
(e.g. a per-instance nested subdirectory) into a draft's own paths β scaffold learn-review
compares the draft's render byte-for-byte against that same example folder treated as the root, so a
baked-in prefix reports the entire draft as mismatched. Add that nesting as a target: override
afterward, once you've promoted the draft and are wiring it into the real templates tree.
learn is not tied to one LLM vendor. Set exactly one of these and it's picked automatically
(--provider disambiguates if both happen to be set):
| Env var | Provider |
|---|---|
ANTHROPIC_API_KEY |
Anthropic (Messages API) |
OPENAI_API_KEY |
OpenAI-compatible Chat Completions API |
--base-url points the OpenAI-compatible provider at any endpoint that mimics the same shape
(Groq, OpenRouter, a local Ollama server, ...) without any code changes. --model overrides the
per-provider default.
This makes a real call to whichever provider you configure, at that provider's usual cost.
If you're running learn locally without an agent driving it, here are vetted starting points
for the --base-url path (e.g. an Ollama server) β not an exhaustive model roundup, just four
that hold up per 2026-09 research:
| Model | Why |
|---|---|
devstral:24b |
Apache 2.0; only one of the four with a published SWE-Bench Verified score (46.8%) |
gpt-oss:20b |
Apache 2.0, OpenAI's own open-weight model; runs CPU-only on 16GB RAM, no GPU needed |
qwen3-coder:30b |
Strongest coding quality per VRAM, but needs a 24GB+ GPU or a 32GB Mac |
granite4:8b |
IBM; trained explicitly for tool-use and structured JSON output, fits small machines |
Given two or more paths, learn generalizes across all of them in one call instead of just
one β useful when a single example under-constrains what's actually invariant vs. variable. A
variable's default is always drawn from the first path given, so review the result with
scaffold learn-review <draft-dir> <path1> afterward, exactly as for a single example. A single
<path> behaves exactly as it always has.
--draft=<path|-> skips the provider call entirely. An AI agent invoking learn (e.g. via
scaffold-cli-skill) is already an LLM β rather than pay for a second, separately-billed model
call, it can do the invariant/variable separation itself and hand the result straight to learn
as JSON (matching the same schema a provider call would produce β see internal/learn/prompt.go
for the exact shape), either as a file path or - for stdin. No ANTHROPIC_API_KEY/
OPENAI_API_KEY/--provider/--model/--base-url needed in this mode; combining --draft with
any of those is rejected rather than silently ignored.
Every scaffold, version, dimension, template, and variable lives in the templates repository, not
this engine β see Requirements. Adding or changing one means writing a
jig.yaml; the full contract for that file (reserved names, the precedence rule, a minimal
worked example) is documented in
scaffold-templates' README.
Structured manifests (application.yml, package.json, ...) can opt into merge: so a later
create deep-merges into what's already on disk instead of replacing it. go.mod is deliberately
excluded from this: Go's own go mod tidy reconciles the require block from source imports far
more correctly than scaffold-cli parsing/merging it as text ever could, and create never executes
anything on your behalf (no post_hooks) β run go mod tidy manually after adding a new
entity/component to a go-chi project.
Distributed under the MIT License.
If scaffold-cli saves you some boilerplate, consider supporting its development:

