cardano-init
Go from zero to a running Cardano protocol in one command.
Pick a tool for each role you need (on-chain, off-chain, devnet, infrastructure, formal-methods). cardano-init generates a monorepo where every component is already connected to the others. It also includes a worked end-to-end example that builds and passes its tests as soon as it is generated.
$ cardano-init --name my-protocol --on-chain aiken --off-chain meshjs --devnet yaci
my-protocol/
├── on-chain/ # Validators (smart contracts)
├── off-chain/ # Tx building (protocol transactions)
├── devnet/ # Local chain for integration testing
├── blueprint/ # shared CIP-57 contract interface
├── spec/ # Protocol specification (plain Markdown; --no-spec to skip)
├── .env # shared between components
├── Justfile # Commands to build, test, and clean
├── AGENTS.md # Agent brief
└── README.md
$ cd my-protocol && just test
✓ All tests passed
Why cardano-init?
- One command to a working project. The generated monorepo builds and passes its tests immediately.
- Combine tools freely. Components share a common contract, so a tool from one role works with any tool from another.
- Built for coding agents. Every command can print JSON, and every project includes an
AGENTS.mdthat tells an agent what the project is and what to do next. - Help with setup. The built-in
doctorchecks your toolchains and prints the install command for anything missing. - A real example. Every stack ships the same gift-card scenario, and it runs end to end.
Where to go next
- New to Cardano: the Cardano Developer Portal has a hands-on onboarding path, from wallets and transactions to writing and testing validators.
- First project: Installation, then the Quick start.
- How the components connect: How it works.
- All flags: the Command reference.
- Using
cardano-initfrom an LLM: For coding agents. - Adding your tool to the registry: Adding a tool.
cardano-init is open source (Apache-2.0) and developed on GitHub.
Installation
cardano-init is a single binary with no runtime dependencies.
Run without installing
Using npx:
npx cardano-init help
Using Nix:
nix run github:input-output-hk/cardano-init -- help
Install script
Linux and macOS (x86_64 and arm64):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/input-output-hk/cardano-init/releases/latest/download/cardano-init-installer.sh | sh
Windows (PowerShell):
irm https://github.com/input-output-hk/cardano-init/releases/latest/download/cardano-init-installer.ps1 | iex
To install a specific version, or to download the binary yourself, use the Releases page.
Nix (flake)
# Install the CLI into your profile
nix profile add github:input-output-hk/cardano-init
# Or run it once, without installing
nix run github:input-output-hk/cardano-init -- help
Cargo
You need a recent Rust toolchain that supports the 2024 edition.
# From the published repository
cargo install --git https://github.com/input-output-hk/cardano-init
# Or from a local clone
cargo install --path .
Verify
cardano-init --version
The projects cardano-init generates need just and the toolchain of each tool you select (Aiken, Node.js, a JVM, Docker, …). You can install these after you generate a project: run cardano-init doctor inside it to see what is missing and how to install it.
Quick start
1. Create a project
Run the command with no arguments to start the interactive setup. It asks for a project name and a tool for each role:
cardano-init
If you already know which tools you want, use one-shot mode. Passing --name makes the command run without any prompts:
cardano-init --name my-protocol --on-chain aiken --off-chain meshjs --devnet yaci
Select only the roles you need. One role is enough:
cardano-init --name my-validators --on-chain aiken
To see the tools available for each role:
cardano-init list # detailed list
cardano-init list --table # compact matrix
2. Check your dependencies
Each tool needs its own toolchain. Run the dependency doctor from inside the generated project:
cd my-protocol
cardano-init doctor
It finds the components in the project, checks which toolchains are installed, and prints the install command for each one that is missing.
3. Build and test
Every generated project uses just:
just build # build every component (on-chain first, so the blueprint exists)
just test # build, then run every component's tests
just clean # remove build artifacts
just test passes on a new project, because every stack includes a worked gift-card example. Change the protocol as you need and run just test often.
4. Run a local chain (optional)
If you selected a devnet or an infrastructure provider, start it from its own component:
just -f devnet/Justfile dev # for example, start Yaci DevKit
just -f infra/Justfile dev # for example, start Kupo and Ogmios with cardano-up
Both write their connection details into the project’s .env. The off-chain component reads them from there.
More examples
# Show what would be generated, without writing anything
cardano-init --name my-protocol --on-chain aiken --dry-run
# Fullstack: one tool for both on-chain and off-chain, as a single protocol/ component
cardano-init --name my-protocol --fullstack scalus
# Short flags
cardano-init -n my-protocol -p scalus -d yaci
# Also generate a Nix flake for the project's dependencies
cardano-init --name my-protocol --on-chain aiken --off-chain meshjs --nix
# Later, from inside the project: add or replace a tool, or remove one
cardano-init add --on-chain plinth
cardano-init remove --devnet
The Command reference lists every flag.
How it works
Roles
You choose a tool for each role you need. cardano-init creates a directory only for the roles you select, plus a base layer (top-level Justfile, README.md, .env, blueprint/) that connects them.
It also creates a spec/ folder for the protocol’s specification in plain Markdown. Pass --no-spec to skip it.
| Role | What it does | Directory | Multiple tools? |
|---|---|---|---|
on-chain | Validators (smart-contract logic); produces the CIP-57 blueprint | on-chain/ | no |
off-chain | Transaction building and submission | off-chain/ | no |
devnet | A local test chain to develop and run integration tests against | devnet/ | no |
infrastructure | Indexers, node providers, chain followers | infra/ | yes |
formal-methods | Specification and verification | formal-methods/ | no |
The interface contract
Components connect through an interface contract with two parts:
- Every on-chain component writes its compiled validators to
blueprint/plutus.json. - Every component that starts a local chain endpoint writes its address (for example
INDEXER_URL) to.env.
Other components read these two files and keep working when a value is empty. No component reads another component’s files directly, so you can replace a tool on one side and the other side still works.
flowchart LR
OC["on-chain<br/>(validators)"] -->|"blueprint/plutus.json"| BP[["blueprint/"]]
BP --> OFF["off-chain<br/>(tx building)"]
INFRA["devnet / infrastructure<br/>(local endpoint)"] -->|"INDEXER_URL, …"| ENV[[".env"]]
ENV --> OFF
The worked example: a gift card
Every on-chain and off-chain template includes the same example, a gift card. It has two validators:
- a one-shot minting policy, which mints a unique token and can run only once because it requires a specific UTxO to be spent;
- a
redeemvalidator, which releases a locked gift when that token is burned.
All tools implement this example with the same parameters. As a result, any on-chain tool works with any off-chain tool. For example, the Scalus off-chain code can drive an Aiken contract, and the MeshJS off-chain code can drive a Scalus contract.
When spec/ is enabled and you select an on-chain, off-chain, or fullstack component, spec/giftcard.md describes the example’s actors, assets, validators, transactions, and invariants. Use it as a starting point for specifying your own protocol.
Fullstack tools
Some tools, such as Scalus, write both the on-chain and the off-chain code in one language. If you select such a tool for both roles, you get one protocol/ component in place of two directories:
cardano-init --name my-protocol --fullstack scalus
# same result as
cardano-init --name my-protocol --on-chain scalus --off-chain scalus
protocol/ also writes blueprint/plutus.json and reads .env, so it works with devnet, formal-methods, and infrastructure tools.
Compatibility checks
Each off-chain tool can connect to some providers and not to others. cardano-init knows which pairs work. If the off-chain tool you select cannot reach any of the providers you select, it stops before generating the project and lists the providers that would work.
- Pass
--ignore-warningto generate the project anyway. The error becomes a warning. - Interactive mode hides the options that would not work.
Experimental tools
A tool is experimental when the tool itself is still in development, or when its cardano-init template does not yet pass every build check. Experimental tools still generate, but you must opt in:
- In one-shot or JSON mode, pass
--allow-experimental(-e). Without it, selecting an experimental tool is an error and nothing is generated. - In interactive mode, you are asked to confirm. The default answer is No.
cardano-init list marks these tools as [experimental].
Networks
Generated projects use the preview testnet. To change the network, edit CARDANO_NETWORK in the generated .env.
The generated project
A typical project (on-chain + off-chain + devnet) looks like this:
my-protocol/
├── Justfile # top-level build / test / clean
├── README.md # how to work with this specific stack
├── AGENTS.md # brief for coding agents
├── CLAUDE.md # imports AGENTS.md
├── .gitignore
├── .env # shared connection details
├── blueprint/ # CIP-57 blueprint, written by on-chain `build`
├── spec/ # protocol specification in plain Markdown
├── on-chain/ # validators + their own Justfile
├── off-chain/ # transaction building + its own Justfile
└── devnet/ # local chain + its own Justfile
Only the roles you select get a directory. Add --nix to also get a flake.nix and .envrc that pin the project’s toolchains. Add --dry-run to see the full file list without writing anything.
The Justfile workflow
Every component has its own Justfile and works on its own. The top-level Justfile runs the tasks that finish and can be combined:
| Target | What it does |
|---|---|
just build | Builds every component. On-chain builds first, so blueprint/plutus.json exists for the other components. |
just test | Builds the on-chain blueprint, then runs each component’s tests (including formal verification, if selected). |
just clean | Removes build artifacts from every component. |
There are also targets for each role, such as just build-on-chain and just test-off-chain. Run just with no arguments to list them.
Long-running tasks: dev
Tasks that keep running (watch modes, local devnets, service stacks) belong to one component, and the top-level Justfile does not run them. A component has a dev target only when it has such a task. Run it from the component’s Justfile:
just -f devnet/Justfile dev
just -f infra/Justfile dev
just -f <dir>/Justfile --list # see what a component offers
blueprint/
The on-chain component’s build writes the compiled validators to blueprint/plutus.json, a CIP-57 blueprint. The off-chain code reads the compiled scripts from this file and applies its parameters. See The interface contract.
spec/
spec/ describes what your protocol does and why it is safe. Write the design here before implementing a change, then review the code against it. The files are plain Markdown; nothing in this folder is built or executed.
spec/README.mdexplains the conventions for documenting actors, assets, validators, transactions, and invariants.spec/giftcard.mdspecifies the worked example. It is generated only when you select an on-chain, off-chain, or fullstack component. Projects without those components get the README alone.
The folder is generated by default. Pass --no-spec to skip it in one-shot or JSON mode. Interactive setup asks whether to include it, with Yes as the default.
Keep the specification in step with your code. The folder is yours after generation: add and remove never rewrite or delete it. Machine-checked specifications belong in the formal-methods component instead.
.env
.env holds the chain connection details for all components:
CARDANO_NETWORK=preview
INDEXER_URL=
INDEXER_PORT=
NODE_SOCKET_PATH=
OGMIOS_URL=
TX_SUBMIT_URL=
DOLOS_GRPC_URL=
CARDANO_NODE_API_URL=
The values start empty. A component that starts a local endpoint (a devnet such as Yaci DevKit, or an infrastructure provider) fills them in during its dev task. When a value is empty, the components that read it keep working, for example by connecting to a public provider.
AGENTS.md
Every project includes an AGENTS.md, and a CLAUDE.md that imports it, written for the tools you selected. See For coding agents for what it contains.
Changing the stack later
From inside the project, use add and remove to add, replace, or remove components. cardano-init reads the current selection from the component directories (the project has no metadata file) and updates the shared top-level files.
Command reference
cardano-init [OPTIONS] # create a project (interactive, or one-shot with --name)
cardano-init list [--table] # list roles and tools
cardano-init doctor # check this project's dependencies
cardano-init add [ROLE FLAGS] # add or replace a tool in the current project
cardano-init remove [ROLE FLAGS] # remove a role or infrastructure provider
cardano-init help [COMMAND] # print help
Every command accepts the global --format <human|json> flag (default human). See For coding agents for the JSON output.
Create a project
cardano-init # interactive mode
cardano-init --name my-app --on-chain aiken # one-shot, single role
cardano-init --name my-app --on-chain aiken --off-chain meshjs --nix
cardano-init -n my-app -p scalus -d yaci # short flags
With --name, the command runs in one-shot mode and never prompts. Without it, the command starts the interactive setup. Any other creation flag without --name is an error (name_required).
| Flag | Short / aliases | Description |
|---|---|---|
--name <NAME> | -n | Project name (required in one-shot mode). Letters, digits, - and _ only; must not start with .. |
--on-chain <TOOL_ID> | --on, --onchain | On-chain tool (e.g. aiken, scalus). |
--off-chain <TOOL_ID> | --off, --offchain | Off-chain tool (e.g. meshjs, scalus). |
--fullstack <TOOL_ID> | -f, -p, --protocol | One tool for both on-chain and off-chain, generated as a single protocol/ component. Can’t be combined with --on-chain / --off-chain. |
--infra <TOOL_ID> | -i, --infrastructure | Infrastructure provider. Repeatable: --infra kupo --infra ogmios. |
--devnet <TOOL_ID> | -d | Devnet tool (e.g. yaci). |
--formal-methods <TOOL_ID> | --formal, --formalmethods | Formal-methods tool (e.g. blaster). |
--nix | Also generate a Nix flake (flake.nix + .envrc) for the project’s dependencies. | |
--no-spec | Skip the spec/ protocol-specification folder. Generated by default; interactive setup asks whether to include it (default Yes). | |
--allow-experimental | -e, --experimental | Opt in to experimental tools. |
--dry-run | --dryrun | Show what would be generated without writing to disk. |
--ignore-warning | --ignore-warnings | Scaffold a combination the compatibility check flags as incompatible. |
--format <FORMAT> | human (default) or json. | |
--help / --version | -h / -V | Print help / version. |
Short flags and aliases behave exactly like the long flag. Errors and JSON output always use the long name. If the project directory already exists and is not empty, the command stops with dir_exists.
list
List the available roles and tools, including each tool’s language, status, documentation, supporting tooling, and community links.
cardano-init list # detailed, grouped by role
cardano-init list --table # compact matrix of tools by role
cardano-init list --format json # machine-readable catalog
| Flag | Description |
|---|---|
--table | Show a compact matrix of tools by role (human output only). |
doctor
Run doctor inside a generated project. It finds the project’s components, checks that the toolchains they need are installed, and prints the install command for each one that is missing. It also lists the supporting tools (editor extensions, language servers) and the community channels for those components.
cd my-protocol
cardano-init doctor
cardano-init doctor --format json
add
Add a tool to the project in the current directory, or replace the tool of a role. add takes the same role flags as project creation.
cardano-init add --on-chain plinth # replace the on-chain tool
cardano-init add --devnet yaci # add a devnet
cardano-init add --infra kupo --infra ogmios # add infrastructure providers
cardano-init add --fullstack scalus # switch to a fullstack protocol/ component
| Flag | Short / aliases | Description |
|---|---|---|
--on-chain <TOOL_ID> | --on, --onchain | On-chain tool (replaces the current one, if any). |
--off-chain <TOOL_ID> | --off, --offchain | Off-chain tool (replaces the current one, if any). |
--fullstack <TOOL_ID> | -f, -p, --protocol | Fullstack tool for both roles as one protocol/ component. |
--infra <TOOL_ID> | -i, --infrastructure | Infrastructure provider to add (repeatable). |
--devnet <TOOL_ID> | -d | Devnet tool (replaces the current one, if any). |
--formal-methods <TOOL_ID> | --formal, --formalmethods | Formal-methods tool (replaces the current one, if any). |
Plus the shared update flags.
remove
Remove a role, or one infrastructure provider, from the project in the current directory. Role flags take no value, except --infra, which takes the provider id.
cardano-init remove --devnet
cardano-init remove --infra ogmios
cardano-init remove --off-chain --formal-methods
| Flag | Short / aliases | Description |
|---|---|---|
--on-chain | --on, --onchain | Remove the on-chain component. |
--off-chain | --off, --offchain | Remove the off-chain component. |
--infra <TOOL_ID> | -i, --infrastructure | Remove an infrastructure provider by id (repeatable). |
--devnet | -d | Remove the devnet component. |
--formal-methods | --formal, --formalmethods | Remove the formal-methods component. |
Plus the shared update flags.
Shared update flags
add and remove read the project’s current tools from its component directories, apply the change, and update the shared top-level files (Justfile, README.md, AGENTS.md, …). By default they need a clean git working tree, so you can review the change with git diff and revert it.
| Flag | Short / aliases | Description |
|---|---|---|
--dry-run | --dryrun | Show the change set without writing anything. |
--force | Update even when the git working tree has uncommitted changes (or isn’t a git repository). | |
--ignore-warning | --ignore-warnings | Proceed even if the resulting off-chain ↔ provider combination is flagged incompatible. |
--allow-experimental | -e, --experimental | Allow experimental tools in the resulting selection. |
Exit codes
| Code | Meaning |
|---|---|
0 | Success. Also returned for --dry-run, and when you answer No to the interactive confirmation. |
1 | Runtime error: the target directory exists, a dirty git tree, I/O or rendering failures. |
2 | Usage or validation error: unknown tool, tool in the wrong role, invalid project name, missing --name, incompatible tools, … |
For the exact error, use --format json and read error.code. See Error codes.
Tools catalog
Tools currently in the registry (✅ available · 🧪 experimental · ⬜ planned):
| On-chain | Off-chain | Devnet | Infrastructure | Formal methods |
|---|---|---|---|---|
| ✅ Aiken | ✅ MeshJS | ✅ Yaci DevKit | ✅ Kupo | 🧪 Blaster |
| ✅ Scalus | ✅ Scalus | ✅ Ogmios | ||
| ✅ Plinth | ✅ Evolution SDK | ✅ Dolos | ||
| ⬜ Pebble | 🧪 Tx3 | ✅ Tx Submit API | ||
| ⬜ Plutarch | ⬜ Lucid Evolution | ✅ Cardano Node | ||
| ⬜ Opshin | ⬜ Blaze | ✅ Cardano Node API | ||
| ⬜ Elm Cardano | ✅ Dingo | |||
| ⬜ PyCardano |
This table can be out of date. list shows the tools in the version you have installed:
cardano-init list # every tool with its id, language, status, and links
cardano-init list --table # the same matrix as above
Use the tool id shown by list (for example aiken, meshjs, yaci, tx-submit-api) as the value for role flags.
Notes
- Scalus fills both on-chain and off-chain and supports
--fullstack, which generates a singleprotocol/component. See Fullstack tools. - Experimental tools (🧪) require
--allow-experimentalin one-shot mode. See Experimental tools. - Infrastructure is the only role that accepts several tools. See Infrastructure providers.
- To add a tool to the registry, see Adding a tool.
Infrastructure providers
The infrastructure role uses cardano-up, which needs Docker. It is the only role that accepts several tools: repeat the --infra flag to select more than one. All selected providers run together in one cardano-up context for the project, inside a single infra/ component. Each provider writes its connection details to the project’s .env, and the off-chain component reads them from there.
| Provider | Flag | Publishes to .env | Upstream |
|---|---|---|---|
| Kupo | --infra kupo | INDEXER_URL | https://github.com/CardanoSolutions/kupo |
| Ogmios | --infra ogmios | OGMIOS_URL | https://ogmios.dev |
| Dolos | --infra dolos | DOLOS_GRPC_URL, NODE_SOCKET_PATH | https://github.com/txpipe/dolos |
| Tx Submit API | --infra tx-submit-api | TX_SUBMIT_URL | https://github.com/blinklabs-io/tx-submit-api |
| Cardano Node | --infra cardano-node | NODE_SOCKET_PATH | https://github.com/IntersectMBO/cardano-node |
| Cardano Node API | --infra cardano-node-api | CARDANO_NODE_API_URL | https://github.com/blinklabs-io/cardano-node-api |
| Dingo | --infra dingo | INDEXER_URL, NODE_SOCKET_PATH | https://github.com/blinklabs-io/dingo |
# An indexer (Kupo) and a query bridge (Ogmios) over one node (cardano-up adds cardano-node):
cardano-init --name my-protocol --off-chain meshjs --infra kupo --infra ogmios
# Start the services and write their connection details to .env (keeps running):
just -f infra/Justfile dev
- Dolos and Dingo are nodes themselves. They need no separate
cardano-node, and each provides its ownNODE_SOCKET_PATH. Dingo also serves a Blockfrost-compatible API asINDEXER_URL. - One chain index per project.
.envhas oneINDEXER_URL, so select either Kupo or Dingo.
For coding agents
cardano-init can be run completely by an LLM agent.
- JSON output. Every command accepts
--format jsonand prints one JSON object. Errors carry a stablecodeand acontextthat says how to fix them.cardano-init list --format jsonreturns every role and tool. - No prompts. One-shot mode (
--name …) never prompts, so an agent can create a project in one call.--format jsonalso turns prompts off: if required input is missing, the command returns an error.
Generated AGENTS.md
Every project includes an AGENTS.md, and a CLAUDE.md that imports it. It is written for the tools you selected and contains:
- the project layout;
- the interface contract and the rules it sets;
- the
justcommands to build, test, and clean; - links to the official documentation of each tool;
- the cardano-dev-skills most useful for that stack.
cardano-dev-skills is a Cardano Foundation skill set for agents. It teaches how to write validators, build transactions, and debug on-chain failures. cardano-init creates the project, and the generated AGENTS.md points the agent to the skills it needs.
A typical agent flow
# 1. Discover what's available
cardano-init list --format json
# 2. Scaffold in one call
cardano-init --format json --name my-protocol --on-chain aiken --off-chain meshjs --devnet yaci
# 3. Check toolchains from inside the project
cd my-protocol && cardano-init doctor --format json
# 4. Build and test
just test
JSON envelope
A --format json response has one of two shapes:
{ "schema_version": 1, "ok": true, "data": { } }
{ "schema_version": 1, "ok": false, "error": { "code": "<stable>", "message": "<human>", "context": { } } }
message is for humans and can change between versions. code and the context keys are stable. The exit code gives the category: 0 success, 1 runtime error, 2 usage error (see Exit codes).
Error codes
code | Exit | context | Meaning |
|---|---|---|---|
name_required | 2 | {} | A creation flag was given without --name. |
invalid_project_name | 2 | { name, reason } | The name has characters outside [A-Za-z0-9_-] or starts with .. |
unknown_tool | 2 | { tool_id, role, valid_tools } | No tool with that id for the role. |
tool_role_mismatch | 2 | { tool_id, role, valid_roles } | The tool exists but doesn’t fill that role. |
fullstack_conflict | 2 | {} | --fullstack combined with --on-chain / --off-chain. |
fullstack_unsupported | 2 | { tool_id, valid_tools } | The tool has no fullstack template. |
no_roles_selected | 2 | {} | No role flag was given. |
experimental_not_allowed | 2 | { tools, remedy } | An experimental tool was selected without --allow-experimental. |
incompatible_tools | 2 | { tools, reason, compatible_providers, compatible_off_chain, remedy } | The off-chain tool can’t reach any selected provider. |
project_unrecognized | 2 | { dirs } | add / remove couldn’t identify a component directory. |
slot_occupied | 2 | { role, dir } | A target directory exists and isn’t the recognized component. |
nothing_to_change | 2 | {} | The requested add / remove is a no-op. |
dir_exists | 1 | { path } | The project directory already exists and isn’t empty. |
worktree_dirty | 1 | { path } | Uncommitted changes (or not a git repository) and no --force. |
registry_load | 1 | { file?, detail } | The embedded registry failed to load. |
scaffold_error | 1 | { path?, detail } | A template asset, render, or I/O failure. |
Ecosystem: aikup, cardano-up, and friends
cardano-init creates a project once and then has no further role in it. Tools such as aikup and cardano-up install and update the toolchains and services that the project uses. The two kinds of tool work together:
cardano-initdecides which tools the project uses and how they connect.aikup,cardano-up, and similar installers install and manage those tools.
They meet in the doctor command. When a toolchain is missing, doctor tells you which installer to use, for example aikup for Aiken and cardano-up for the infrastructure role.
cardano-init does not:
- pin or upgrade tool versions;
- manage dependencies after the project is generated;
- migrate existing projects.
There is no cardano-init update command.
Supporting tools and getting help
Learn Cardano
The Cardano Developer Portal has a hands-on onboarding path for new developers. Its lectures start with wallets, keys, and transactions, and continue to writing, testing, and using validators. Its Get started page creates a project with cardano-init, and a tutorial builds a complete atomic swap dApp: validator, off-chain code, and frontend.
Supporting tools
Each language has supporting tools: editor extensions (VS Code, Neovim, Zed, Emacs, JetBrains), language servers, and online playgrounds.
cardano-init doctorlists the ones that match the stack in your project, under Supporting tools (deps[].supportin JSON).cardano-init listshows them for every tool, on a Tooling line (tools[].supportin JSON).
The links are stored in the support field of registry/deps.toml, and come from each tool’s own documentation.
Some tools are useful with any stack:
- CBOR and transaction inspectors, to decode datums, redeemers, and raw transactions;
- block explorers for mainnet, preprod, and preview;
- the testnet faucet, for free test ADA.
The Builder Tools page on the Developer Portal lists them.
Getting help
Every tool in the registry records its official community channels, usually a Discord server, sometimes GitHub Discussions or a forum. They are the channels that each project links from its own website or repository.
cardano-init doctorshows them for the components in your project, under Get help.cardano-init listshows them for every tool (tools[].communityin JSON).
For questions about Cardano in general, the Cardano Developer Portal links to the developer Discord, the Cardano Forum, and Cardano Stack Exchange.
Adding a Tool to cardano-init
This guide is for contributors who want to integrate their own tooling into cardano-init. If your tool fills one of the supported roles (on-chain, off-chain, infrastructure, devnet, or formal-methods) you can add it by providing two things:
- A registry entry (
registry/tools/<your-tool>.toml) - A template directory (
templates/<your-tool>/<role>/)
No changes to Rust source code are required. Because all templates and registry files are embedded into the binary at compile time via rust-embed, you will need to recompile (cargo build) after adding your files.
Step 1: Create the registry entry
Create registry/tools/<your-tool>.toml. Use the tool’s id as the filename.
[tool]
id = "mytool" # Unique identifier, used in CLI flags
name = "MyTool" # Human-readable name shown in the UI
description = """\
One to three sentences. What does this tool do, and when should \
someone choose it over alternatives?"""
website = "https://mytool.dev"
community = [ # Optional: official help channels, shown by `doctor` and `list`
{ name = "Discord", url = "https://discord.gg/…" },
{ name = "GitHub Discussions", url = "https://github.com/acme/mytool/discussions" },
]
languages = ["typescript"] # Languages the generated project uses
system_deps = ["mytool-cli"] # What the user needs installed (drives `doctor`; each id needs a registry/deps.toml entry)
nix_packages = ["mytool"] # Nix package name(s), if available (omit if none)
# nix_self_contained = true # optional; set if the template ships its own component flake (keeps nix_packages out of the top-level shell — see The manifest)
detect = ["mytool.config.js"] # How `doctor` recognizes this tool in a scanned project (see below)
[roles.off-chain] # The role this tool fills
template = "mytool/off-chain" # Path under templates/ for this role
detect signatures tell cardano-init doctor how to recognize your tool inside a generated project’s role directory (so it knows which system_deps to check). Each entry is either a bare path (matches if the file exists) or a table { file = "<path>", contains = "<substring>" } (matches if the file exists and contains the substring). Use the contains form when the filename is generic — e.g. a package.json only means your tool if it references your package:
detect = ["mytool.config.js"] # distinctive filename → existence is enough
detect = [{ file = "package.json", contains = "mytool" }] # generic filename → require content
Only tools that declare a role are tested against that role’s directory, so signatures only need to disambiguate within a role. A directory that matches nothing is reported as “unrecognized” — doctor checks dependencies, it does not validate that the component builds (that’s just test).
Supporting tools. In the dep’s registry/deps.toml entry, you can add an optional support list pointing users at your language’s editor tooling (VS Code / Neovim extensions, a language server, a playground). cardano-init doctor lists these for every detected tool, and cardano-init list shows them under each tool that depends on this dep:
[mytool-cli]
binaries = ["mytool"]
docs = "https://mytool.dev/install"
install = [{ npm = "mytool-cli" }]
support = [
{ name = "VS Code extension", url = "https://marketplace.visualstudio.com/items?itemName=acme.mytool" },
{ name = "Neovim plugin", url = "https://github.com/acme/mytool.nvim" },
]
A tool can fill multiple roles. Add one [roles.<role>] section per role:
[roles.on-chain]
template = "mytool/on-chain"
[roles.off-chain]
template = "mytool/off-chain"
Fullstack tools (one component for both on-chain and off-chain)
If your tool implements both on-chain and off-chain in one language and one build (e.g. Scalus), you can offer a fullstack experience: add a [fullstack] table with a third template. When a user assigns your tool to both roles (--fullstack mytool, or --on-chain mytool --off-chain mytool), the two collapse into a single unified protocol/ component built from that template, instead of two folders that hand off through blueprint/plutus.json.
[roles.on-chain]
template = "mytool/on-chain" # used when mytool fills on-chain only (e.g. with a different off-chain tool)
[roles.off-chain]
template = "mytool/off-chain" # used when mytool fills off-chain only
[fullstack]
template = "mytool/fullstack" # used when mytool fills BOTH → one protocol/ component
All three shapes are first-class, so you provide three templates. Rules:
[fullstack]requires both[roles.on-chain]and[roles.off-chain](validated at load).protocolis not a role — you never write[roles.protocol]. It is a fused component the CLI derives from the two role assignments.- The
protocol/component must still honor the interface contract (Step 3): itsbuildwrites../blueprint/plutus.jsonand it reads/writes../.env, so it composes with devnet/formal/infra like a normal on-chain producer. Internally it may share types between its on-chain and off-chain halves and skip the blueprint round-trip — that private short-cut is the whole point — but the external seam is mandatory. - Your tool’s
detectsignatures must be present in the fullstack template too, sodoctorrecognizes the tool inside aprotocol/directory. - Adding a fullstack tool is still pure data — no Rust changes.
Valid role names
| Role key | Description |
|---|---|
on-chain | Smart contract / validator logic |
off-chain | Transaction building and submission |
infrastructure | Indexers, chain followers, node providers |
devnet | Local throwaway chain to develop and integration-test against |
formal-methods | Specification and automated verification |
Declaring provider compatibility ([compat])
An off-chain tool reaches a local chain over one or more seams — the wire protocols it speaks — and its providers (the selected devnet and/or infra tools) each expose some set of seams. If none of the selected providers serve a seam the off-chain tool consumes, they can’t talk and the generated project won’t run. The optional [compat] table lets the CLI catch that before generation instead of handing the user a broken project. It’s pure data, decided by registry::compat with no per-pair logic.
The seams today:
| Seam | Wire protocol | Served by | Consumed by |
|---|---|---|---|
blockfrost | Blockfrost-compatible REST | Yaci, Dingo | Evolution, Mesh, Scalus |
u5c | UTxORPC (gRPC) | Dolos | Mesh |
trp | Tx3 Transaction Resolve Protocol | (self-hosted; Dolos/Demeter) | Tx3 |
ogmios | Ogmios JSON-RPC | Ogmios | Evolution, Mesh |
kupo | Kupo HTTP | Kupo | Evolution |
ogmios + kupo together form a Kupmios provider; an off-chain tool that uses Kupmios lists both, and a match on either counts as usable (so a partial selection is allowed, not force-completed).
A seam means “a working provider for the templates,” not merely “an endpoint exists.” Dolos, for instance, exposes a Blockfrost-compatible “minibf” API, but it omits tx evaluation (/utils/txs/evaluate), which off-chain Blockfrost providers need to budget script execution units — so Dolos declares u5c (its complete provider), not blockfrost. Declare a seam only when a tool can actually build the (script-based) reference example over it.
Declare what your tool consumes or serves:
# An off-chain tool: which seam(s) it can consume.
[compat]
consumes = ["blockfrost", "kupo", "ogmios"] # e.g. Evolution (Blockfrost + Kupmios)
# A devnet or infra tool: which seam(s) it exposes to off-chain consumers.
[compat]
serves = ["blockfrost"] # e.g. Yaci (devnet) or Dingo (infra); Dolos serves ["u5c"]
# A tool that bundles its OWN devnet and needs no devnet role at all.
[compat]
consumes = ["trp"]
self_contained_devnet = true # e.g. Tx3 — a separately-chosen devnet is reported incompatible
How the gate behaves — it evaluates the off-chain tool against the union of all selected providers (the devnet plus every infra tool):
- Compatible when at least one selected provider serves a seam the off-chain tool consumes. So
--off-chain meshjs --infra dolosworks (u5c), and--off-chain evolution --devnet yaci --infra dolosworks too — Yaci covers Evolution’s Blockfrost seam even though the extra Dolos doesn’t. - Incompatible when providers are selected but none serve a consumed seam → one-shot stops generation with
CliError::IncompatibleTools(exit 2), listing the providers that would work for the off-chain tool and the off-chain tools that would work with the selected providers;--ignore-warningdowngrades the stop to a warning. Interactive mode shows an incompatible devnet option as✗ unavailable — <reason>(infra is multi-select with union semantics, so individual infra picks aren’t disabled — the run-level gate covers them). - Self-hosting (
self_contained_devnet) makes a separately-selected devnet redundant/incompatible regardless of seams. [compat]is optional and permissive: a tool with no[compat]table (or emptyconsumes/serves) imposes no constraint. Supplementary infra that isn’t an off-chain endpoint (a bare node, a submit API) simply declares noserves, so it never triggers a conflict, and an off-chain tool with no selected provider falls back to a public provider per the interface contract. A mismatch only becomes a hard stop when both sides declare seams.
Step 2: Create the template directory
Create a directory at the path you declared in the registry entry, e.g. templates/mytool/off-chain/. It must contain a manifest.toml plus all the files it declares.
The manifest
manifest.toml lists every file your template emits.
[manifest]
summary = "MyTool off-chain project with a hello-world transaction"
[[files]]
source = "Justfile.jinja" # Path relative to this template directory
dest = "Justfile" # Destination relative to the role directory
[[files]]
source = "src/index.ts"
dest = "src/index.ts"
[[files]]
source = "package.json.jinja"
dest = "package.json"
[[files]]
source = "flake.nix.jinja" # optional: `when` gates emission on the selection
dest = "flake.nix" # `when = "nix"` ⇒ emit only under `--nix`
when = "nix"
A file may carry an optional when guard. The only condition today is when = "nix", which emits the file only when the project is generated with --nix. Use it to ship a component-local Nix flake — as the Plinth on-chain template does, bundling the recommended haskell.nix setup from IntersectMBO/plinth-template. A tool that does this should also set nix_self_contained = true in its registry entry. Its nix_packages are then not listed as bare attributes in the top-level dev shell (a plain nixpkgs mkShell cannot build a haskell.nix project); instead the top-level flake adds the component as a path:./<dir> input and composes its dev shell via inputsFrom, so the toolchain — and just build for that component — is available from the project root.
Commit the flake.lock. A component flake must also ship a pinned flake.lock (emit it with when = "nix"). Without it, inputs resolve at HEAD — for haskell.nix that is frequently broken (e.g. a missing bootstrap-GHC attribute) and never reproducible. Vendor a known-good lock (Plinth’s is taken verbatim from upstream) and keep it consistent with cabal.project’s index-state.
Whether a file is rendered through MiniJinja is determined solely by its source filename: files ending in .jinja are rendered, all others are copied verbatim. Name a file foo.jinja when it contains template variables or conditional blocks; leave it without the extension when it should be copied byte-for-byte (source code, lock files, binary assets, or any file whose content contains {{ or {% as literal syntax).
Directory layout
templates/
└── mytool/
└── off-chain/
├── manifest.toml
├── Justfile.jinja
├── package.json.jinja
└── src/
└── index.ts
Step 3: Implement the interface contract
Every template, regardless of role, must conform to the interface contract defined in src/contract.rs. This is what allows any on-chain tool to compose with any off-chain tool without per-pair integration logic.
Mandatory Justfile targets
Every template must include a Justfile that exposes these three targets:
| Target | Purpose |
|---|---|
build | Compile / package the component |
test | Run the component’s tests |
clean | Remove build artifacts |
The top-level project Justfile delegates to these by calling just -f <role>/Justfile <target>, so the names are non-negotiable. The top level aggregates only these three (the terminating, composable tasks).
Optional: dev
| Target | Purpose |
|---|---|
dev | Start development mode (watch, REPL, local daemon, local devnet) |
dev is optional — add it only when your tool has a genuine long-running or interactive mode (don’t ship a no-op dev just to fill the slot). It is never aggregated at the top level; the developer runs it directly (just -f <role>/Justfile dev). A dev that provisions a local chain endpoint writes the standard connection vars to ../.env (see below).
The reference example: Gift Card
Every on-chain, off-chain, and fullstack template implements the same example — a gift card — so that any on-chain tool genuinely composes with any off-chain one (an Aiken contract driven by the Scalus off-chain, a Scalus contract driven by the MeshJS off-chain, and so on). Matching it is what makes a new tool interoperable, not just self-consistent.
The flow is two validators:
gift_card(minting policy) — a one-shot policy that mints exactly one token, gated by a specific seed UTxO that must be spent (so the policy id is unique). Burning that one token is the redeem step.redeem(spending validator) — guards the gift locked at its script address; it only allows the UTxO to be spent when the gift-card token is burned in the same transaction.
To stay interchangeable, keep the wire ABI identical across tools:
| Aspect | Convention |
|---|---|
| Blueprint titles | <module>.gift_card.mint and <module>.redeem.spend — off-chain tools locate validators by the <validator>.<purpose> suffix, so the module prefix is free. |
gift_card parameters | Two separate compile-time parameters, applied with applyParamsToScript: the token name (ByteString) then the seed OutputReference. |
redeem parameters | Two separate parameters: the token name (ByteString) then the gift-card policy id (ByteString). |
Seed OutputReference | The flat shape Constr 0 [transaction_id, output_index] — the transaction id is a bare byte string, not wrapped in a TxId constructor (this is Aiken’s OutputReference / MeshJS’s outputReference). |
| Mint redeemer | An Action enum: Mint = Constr 0 [], Burn = Constr 1 []. |
| Redeem redeemer / datum | Unused by the validator — burning the token is the authorization. |
An off-chain template consumes these by reading the compiled code straight from ../blueprint/plutus.json and applying the parameters above (see Off-chain tools below), so it drives whatever on-chain tool produced the blueprint.
Role-specific requirements
On-chain tools must produce the CIP-57 Plutus blueprint during build:
build:
mytool build
cp -f output/plutus.json ../blueprint/plutus.json
The off-chain and devnet templates read from ../blueprint/plutus.json. If your tool outputs to a different path, the build target must copy it to the canonical location. This is the primary integration seam between on-chain and off-chain.
Off-chain tools should build transactions against the validators in the blueprint, not a private recompiled copy — parameterize the compiledCode read from ../blueprint/plutus.json (applying the gift-card parameters above). That is what makes the off-chain tool composable with any on-chain tool rather than only its own. They must also handle the case where no blueprint exists yet (a standalone off-chain project, before an on-chain component has built) — degrade gracefully, and gate any tests that need real scripts on the blueprint being present:
build:
@test -f ../blueprint/plutus.json || echo "Warning: no blueprint found, skipping type generation"
npm run build
Off-chain tools may also read chain connection details from ../.env, and should switch behavior on their presence — e.g. talk to a local devnet when INDEXER_URL is set, and fall back to a public provider otherwise:
import * as dotenv from "dotenv";
dotenv.config({ path: "../.env" });
const indexerUrl = process.env.INDEXER_URL; // set → a local endpoint is up
If your off-chain tool talks a specific wire protocol (Blockfrost, TRP, UTxORPC), declare it with consumes in [compat] (see Declaring provider compatibility) so an incompatible provider pairing is caught before generation. A tool that bundles its own devnet (and reads no .env endpoint) instead sets self_contained_devnet = true.
Fullstack tools (a [fullstack] template, rendered into protocol/) must satisfy the on-chain contract from the fused component: build writes ../blueprint/plutus.json, and the component reads/writes ../.env like an off-chain consumer. Internally it may link its on-chain and off-chain halves directly (shared types, no blueprint round-trip); the blueprint file is written for the other roles (devnet/formal/infra), which still consume it. The three mandatory Justfile targets (build/test/clean) apply, and dev is optional as usual.
Tools that provision a local chain endpoint must write the connection details to ../.env during dev. This applies to infrastructure services and, equally, to a local devnet in the devnet role (e.g. Yaci DevKit) — the seam is the .env keys, not the role. Use the standard variable names:
| Variable | Meaning |
|---|---|
INDEXER_URL | Base URL of the chain indexer |
INDEXER_PORT | Port of the indexer |
NODE_SOCKET_PATH | Path to the local node socket |
CARDANO_NETWORK | Target network (preview, preprod, or mainnet) |
dev:
mytool start &
echo "INDEXER_URL=http://localhost" >> ../.env
echo "INDEXER_PORT=1442" >> ../.env
Write the keys idempotently (replace in place rather than appending) so repeated just dev runs don’t accumulate duplicate lines. Off-chain/devnet consumers read these and never need to know which tool wrote them.
Infrastructure providers are added as data, not a new template. The infra role is backed by a single shared cardano-up driver (templates/_infra/cardano-up), and all selected providers aggregate into one infra/ component. To add a provider (e.g. dolos), you write only a registry/tools/<provider>.toml — no template:
[tool]
id = "dolos"
name = "Dolos"
description = "…"
website = "https://…"
languages = [] # infra providers have no user-facing language
system_deps = ["docker", "cardano-up"]
detect = [] # infra is scanned by a driver marker, not per-tool
[roles.infrastructure]
template = "_infra/cardano-up" # always the shared driver
[infra]
cardano_up_package = "dolos" # the `cardano-up install` package id
# Map cardano-up's `context env` outputs → contract .env keys. A mapping for an
# existing key (e.g. NODE_SOCKET_PATH) overrides the default at generation time.
env = [{ from = "DOLOS_SOCKET_PATH", to = "NODE_SOCKET_PATH" }]
If a mapping targets a contract .env key that isn’t seeded yet (a brand-new
connection var), promote it: add a contract::ENV_* constant and a seeded
KEY= line in templates/_base/env.jinja so every project always carries it.
See TECH_SPEC §3.2 (infra config) and §9.6 (infra detection) for the full model.
Devnet tools provision a local throwaway chain. They should read both the blueprint and the .env if they are present, but must work if neither exists, and write the connection vars above during dev — that is how off-chain components reach the devnet, and it composes with any off-chain tool without per-pair code. Declare the seam(s) the devnet exposes with serves in [compat] (see Declaring provider compatibility) so the CLI can steer users away from an off-chain tool it can’t serve.
Formal-methods tools have no extra contract beyond the four Justfile targets.
Template variables
Any rendered file (one whose source ends in .jinja) can reference the following variables from the template context:
{{ project_name }} {# e.g. "my-protocol" #}
{{ network }} {# "preview", "preprod", or "mainnet" #}
{{ blueprint_path }} {# "blueprint/plutus.json" #}
{{ nix }} {# true or false #}
{# Flags for conditional sections #}
{{ has_on_chain }}
{{ has_off_chain }}
{{ has_fullstack }} {# one tool fills both on-chain + off-chain → protocol/ #}
{{ has_infra }}
{{ has_devnet }}
{{ has_formal_methods }}
{# Per-role context (only safe to access when the corresponding has_* is true) #}
{{ on_chain.tool_id }} {# e.g. "aiken" #}
{{ on_chain.tool_name }} {# e.g. "Aiken" #}
{{ on_chain.language }} {# first entry from the tool's languages list #}
{{ on_chain.dir }} {# "on-chain" #}
{{ off_chain.tool_id }}
{{ off_chain.tool_name }}
{{ off_chain.language }}
{{ off_chain.dir }} {# "off-chain" #}
{# Fullstack: set instead of on_chain/off_chain when one tool fills both.
When has_fullstack is true, has_on_chain and has_off_chain are false. #}
{{ fullstack.tool_id }}
{{ fullstack.tool_name }}
{{ fullstack.language }}
{{ fullstack.dir }} {# "protocol" #}
{{ devnet.tool_id }}
{{ devnet.dir }} {# "devnet" #}
{{ formal_methods.tool_id }}
{{ formal_methods.dir }} {# "formal-methods" #}
{# Infrastructure allows multiple tools simultaneously #}
{% for t in infra_tools %}
{{ t.tool_id }}
{{ t.dir }} {# "infra" #}
{% endfor %}
{# Nix packages from all selected tools, deduplicated #}
{% for pkg in nix_packages %}{{ pkg }}{% endfor %}
Your template files only need to reference the variables relevant to them. An off-chain template does not need to reference on_chain.* at all. The integration is handled by the base-level Justfile template, not by individual role templates.
Worked example: a minimal off-chain tool
registry/tools/mytool.toml:
[tool]
id = "mytool"
name = "MyTool"
description = "A TypeScript SDK for building Cardano transactions."
website = "https://mytool.dev"
languages = ["typescript"]
system_deps = ["node"]
nix_packages = ["nodejs_20"]
detect = [{ file = "package.json", contains = "mytool" }]
[roles.off-chain]
template = "mytool/off-chain"
templates/mytool/off-chain/manifest.toml:
[manifest]
summary = "MyTool off-chain project"
[[files]]
source = "Justfile.jinja"
dest = "Justfile"
[[files]]
source = "src/index.ts"
dest = "src/index.ts"
templates/mytool/off-chain/Justfile.jinja:
# Off-chain component (MyTool)
# Part of {{ project_name }}
build:
npm install
npm run build
test:
npm test
dev:
npm run dev
clean:
rm -rf dist/ node_modules/
templates/mytool/off-chain/src/index.ts:
// Off-chain entry point; replace with your transaction logic
console.log("Hello from MyTool");
After adding these files, run cargo build to embed them into the binary, then verify with a dry run:
cargo run -- --name test-project --off-chain mytool --dry-run
Testing your integration
Dry run: check the file plan without writing anything:
cargo run -- --name my-project --off-chain mytool --dry-run
Full scaffold: generate a real project and inspect it:
cargo run -- --name my-project --off-chain mytool
ls my-project/off-chain/
Unit tests: the registry loader test all_fields_populated will automatically pick up your tool and verify all required fields are present:
cargo test
For a thorough validation, also scaffold a project with your tool combined with tools from other roles and confirm the top-level just build and just test targets wire up correctly.
cardano-init: Architecture
Status: Draft · Last updated: 2026-06-01 · Owner: Robertino Martinez
This is the canonical architecture document. It supersedes the legacy root-level
REQUIREMENTS.mdandARCHITECTURE.md(now deleted; available in git history). Read PRD.md for the why and for whom; this document owns the how. Detailed contracts, data shapes, and edge cases live in TECH_SPEC.md; sequencing lives in ROADMAP.md.
1. Design principles
Five principles drive every structural decision in the codebase. When a tradeoff arises, these are the tie-breakers.
-
The interface contract is the core abstraction. Every tool template conforms to a shared set of conventions (canonical blueprint path, standard Justfile tasks, standard
.envvariables). Because each template independently conforms, any producer composes with any consumer without per-pair integration code. Composition is generic over the set of roles present, never over which tools fill them. This is what makes the system scale as O(tools) rather than O(tools²). -
Tools are data-driven; roles are a fixed code vocabulary. Tools and templates are declarative data embedded at compile time: adding a tool is a data change (a TOML file + a template directory + a recompile), never a change to CLI logic. Roles, by contrast, are a small fixed vocabulary defined in code (the
Roleenum, §3.1): the registry references roles but cannot introduce them. The set is not frozen at a particular number (it can grow) but growing it is a deliberate, rare code change, not a data change. -
Pure core, impure edges.
registry/,scaffold/,contract, and the pure part ofdoctor/are pure logic over data with zero dependency oncli/. All user interaction, terminal formatting, network, and system probing live at the edges (cli/, the impure half ofdoctor/). This keeps the core testable and makes future extraction straightforward. -
Deterministic generation. Identical inputs produce byte-identical output. This is a hard requirement for coding-agent trust, reproducibility, and snapshot tests. Determinism is guaranteed at the planning phase (§6.4).
-
Offline and self-contained. The registry and all templates are embedded in the binary; generation makes no network calls. Network is used only for installing toolchains (the doctor) and a best-effort version-update notice (§9). The binary, for a given version, is the single source of truth for what it generates.
2. Crate & module structure
A single Rust crate. The boundary between “library logic” and “CLI concerns” is enforced by the module dependency graph (§2.2), not by separate crates. Items marked (planned) are introduced by the PRD and not yet implemented.
cardano-init/
├── Cargo.toml
├── src/
│ ├── main.rs # Entry point: delegates to cli::run()
│ │
│ ├── cli/ # Impure edge: user interaction, formatting, process control
│ │ ├── mod.rs # Arg parsing (clap), dispatch, top-level error type
│ │ ├── interactive.rs # Guided interactive flow (dialoguer)
│ │ ├── oneshot.rs # Flag → Selection, validation, machine-readable errors
│ │ ├── output.rs # Presenter: renders results/errors as human text or JSON
│ │ ├── theme.rs # Terminal styling palette (console)
│ │ ├── git.rs # git helpers: clean-tree gate + init a new project's repo
│ │ └── update.rs # add/remove: mutate an existing scaffolded project (§6.4, TECH_SPEC §15)
│ │
│ ├── registry/ # Pure: tool + role definitions from embedded TOML
│ │ ├── mod.rs
│ │ ├── types.rs # Role, ToolDef, RoleConfig, Selection, Network, Seam, …
│ │ ├── loader.rs # rust-embed → Registry (indexed by id and by role)
│ │ ├── compat.rs # off-chain ↔ provider (devnet+infra) seam compatibility
│ │ └── view.rs # read-only projections of the registry (for `list`)
│ │
│ ├── scaffold/ # Pure: project generation pipeline
│ │ ├── mod.rs # Orchestrator (scaffold / dry_run) + embedded templates
│ │ ├── context.rs # Phase 1: Selection + Registry → TemplateContext
│ │ ├── planner.rs # Phase 2: → FilePlan (canonical order; dry-run stops here)
│ │ ├── renderer.rs # Phase 3: MiniJinja render / pass-through
│ │ └── writer.rs # Phase 4: the only phase with disk side effects
│ │
│ ├── doctor/ # Dependency detection + install advice (§8)
│ │ ├── mod.rs # Pure: (deps, environment) → missing + advice
│ │ ├── catalog.rs # Pure: loads registry/deps.toml → dep recipes
│ │ ├── installers.rs # Pure: closed Installer vocabulary + command templating
│ │ └── probe.rs # Impure: detect OS, package managers, PATH
│ │
│ └── contract.rs # Interface-contract constants (paths, env vars, dirs)
│
├── registry/tools/ # Embedded data: one TOML per tool (15 tools)
│ ├── aiken.toml plinth.toml scalus.toml # on-chain
│ ├── meshjs.toml evolution.toml tx3.toml # off-chain
│ ├── cardano-node.toml cardano-node-api.toml dingo.toml # infra
│ ├── dolos.toml kupo.toml ogmios.toml tx-submit-api.toml # infra
│ ├── yaci.toml blaster.toml # devnet / formal-methods
│
└── templates/ # Embedded data: tool/role template trees
├── _base/ (Justfile.jinja, README.md.jinja, AGENTS.md.jinja, CLAUDE.md, gitignore, env.jinja)
├── _nix/ (flake.nix.jinja)
├── _spec/ (README.md.jinja, giftcard.md.jinja)
└── <tool>/<role>/ (manifest.toml + template files)
Assets are embedded with rust-embed via #[folder = "registry/"] and #[folder = "templates/"]. There is no build.rs: embedding is handled by the derive macro directly. (The legacy architecture doc referenced a build.rs asset manifest; that is obsolete.)
2.2 Module dependency graph
The graph flows strictly downward; there are no cycles. The key invariant: registry, scaffold, contract, and the pure part of doctor never depend on cli.
main.rs
│
├── cli/ ──────┬─▶ scaffold/ ─▶ registry/
│ ├─▶ doctor/ ─▶ registry/
│ ├─▶ registry/
│ └─▶ contract
│
scaffold/, doctor/(pure), registry/ ──▶ contract
cli/ is the edge: it orchestrates the pure core and presents results. It is not depended upon by the core.
3. Data model
All core types live in registry/types.rs (and scaffold/ for pipeline-internal types). Exact field-level definitions and invariants are in TECH_SPEC; this is the shape and intent.
3.1 Roles
#![allow(unused)]
fn main() {
pub enum Role { OnChain, OffChain, Infrastructure, Devnet, FormalMethods }
}
Role::ALLdefines the canonical order used for deterministic output.- Each role maps to a kebab string (
on-chain,formal-methods, …) for TOML/flags, aDisplayname for humans, and a contract directory (dir()→ §4). - The enum is the sole source of truth for the role vocabulary: roles are not defined by the repository data. A tool’s
[roles.<kebab>]blocks merely reference existing roles; the registry cannot introduce a new one. Role strings are validated against the enum at load time viaRole::from_kebab(an unknown role →RegistryError::UnknownRole). What the registry data determines is which tools exist and which of these fixed roles each can fill, not the set of roles itself. - Adding a role is therefore a deliberate code change touching every site that names roles: a new
Rolevariant +Role::ALL+from_kebab/as_kebab/dir()/Display, acontract::DIR_*constant,TemplateContexthandling, and a CLI flag. Adding a tool, by contrast, is pure data. The role set is small and grows rarely. - The fullstack
protocol/component (§3.2) is a related but distinct kind of code change: it adds acontract::DIR_PROTOCOLconstant andTemplateContexthandling, but noRolevariant — it is a fused component derived from two existing roles, not a sixth role. New fullstack tools remain pure data (a[fullstack]table + a template dir).
3.2 Tools
#![allow(unused)]
fn main() {
pub struct ToolDef {
pub id, name, description, website: String,
pub languages: Vec<String>,
pub nix_packages: Vec<String>, // toolchains for the Nix dev shell
pub roles: HashMap<Role, RoleConfig>, // which roles this tool can fill
pub fullstack: Option<RoleConfig>, // unified on-chain+off-chain template (opt-in)
}
pub struct RoleConfig { pub template: String } // path under templates/
}
system_deps is declared in the tool TOML and consumed by the doctor (§8). One tool can fill multiple roles (e.g. Scalus: on-chain + off-chain), each with its own template path.
Fullstack tools. A tool that fills both on-chain and off-chain may also declare a [fullstack] template. When the same tool fills both roles, the two collapse into one fused protocol/ component (built from fullstack.template) instead of two folders — the value of a same-language stack (shared types, one build, no plutus.json round-trip between the halves). This is a tool capability, not a new role: protocol has no Role variant and Role::ALL stays at five; the collapse is derived from the selection at planning time, mirroring the infrastructure aggregation (§6.2). The protocol/ component still conforms to the interface contract — its build writes the blueprint and it reads/writes .env — so it composes with every other role like a normal on-chain producer (§4).
3.3 Selection (the resolved user choice)
#![allow(unused)]
fn main() {
pub struct Selection {
pub project_name: String,
pub assignments: Vec<RoleAssignment>, // Infrastructure may appear multiple times
pub network: Network, // Always Preview; switch via CARDANO_NETWORK in the generated .env
pub nix: bool,
pub spec: bool, // spec/ folder (default true; --no-spec)
}
pub struct RoleAssignment { pub role: Role, pub tool_id: String }
}
Constraint enforcement is by construction. Role uniqueness (one tool per role, except Infrastructure) is enforced at the edge: interactive mode only allows one tool per non-infra role; one-shot uses single-value flags per role (--infra is repeatable). A Selection that exists is valid: there is no separate validation module.
3.4 Pipeline types (scaffold/)
#![allow(unused)]
fn main() {
pub struct TemplateContext { … } // per-role flags + RoleContexts + contract constants
pub struct RoleContext { tool_id, tool_name, language, dir }
pub struct FilePlan { entries: Vec<FileEntry> }
pub struct FileEntry { dest: PathBuf, source: TemplateSource, render: bool }
pub enum TemplateSource { Base(String), Role(String), Optional(String), Inline(Vec<u8>) }
}
TemplateContext is Serialize and is the entire surface templates can see. It carries has_* booleans per role, an Option<RoleContext> per single-tool role, infra_tools: Vec<InfraToolContext> plus infra_context_name and the resolved infra_env (the aggregated infra component, TECH_SPEC §4.6), the contract constants (blueprint_path, env_vars), and Nix info. render is derived from the .jinja extension.
4. Interface contract (contract.rs)
The contract is a set of constants every template conforms to. It is the seam that makes composition generic.
#![allow(unused)]
fn main() {
pub const BLUEPRINT_PATH: &str = "blueprint/plutus.json";
pub const DIR_ON_CHAIN = "on-chain"; DIR_OFF_CHAIN = "off-chain";
pub const DIR_INFRA = "infra"; DIR_DEVNET = "devnet"; DIR_FORMAL_METHODS = "formal-methods";
pub const DIR_PROTOCOL = "protocol"; // fused on-chain+off-chain component (fullstack, §3.2)
pub const ENV_INDEXER_URL = "INDEXER_URL"; ENV_INDEXER_PORT = "INDEXER_PORT";
pub const ENV_NODE_SOCKET_PATH = "NODE_SOCKET_PATH"; ENV_NETWORK = "CARDANO_NETWORK";
// Provider-specific endpoints, seeded empty and populated when the matching
// infrastructure provider is provisioned:
pub const ENV_OGMIOS_URL = "OGMIOS_URL"; ENV_TX_SUBMIT_URL = "TX_SUBMIT_URL";
pub const ENV_DOLOS_GRPC_URL = "DOLOS_GRPC_URL"; ENV_CARDANO_NODE_API_URL = "CARDANO_NODE_API_URL";
}
DIR_PROTOCOL is the one directory not backed by a Role (§3.2): a fullstack tool’s fused
component. Its build still produces BLUEPRINT_PATH — it is the on-chain producer for its
project — so it is a full contract citizen; fullstack only privatizes the internal
on-chain↔off-chain link between its two halves.
Compliance checklist (enforced mechanically by contract-compliance tests):
- Every template ships a
Justfileexposingbuild,test,clean, and works independently (itsjust buildsucceeds with no other roles present). A no-op-for-this-tool target among those three still exists (printing a message is fine).devis optional — provided only when the tool has a real watch/daemon/devnet mode. The top level aggregates onlybuild/test/clean;dev, where present, is per-component (§6.2 / TECH_SPEC §7.2), never aggregated. - On-chain produces the CIP-57 blueprint at
../blueprint/plutus.jsonduringbuild. Other roles read it from that path if present. - The component that provisions a local chain endpoint writes the standard connection vars to
../.envduring itsdev. This is role-agnostic — usually an infrastructure service, but a local devnet such as Yaci DevKit in the devnet role does it too. Role = a tool’s purpose; writing.env= the orthogonal capability of exposing a local endpoint. Consumers react to the presence ofINDEXER_URL, never to which role set it (principle 1). - Off-chain / devnet / formal-methods read the blueprint and
.envif present, and degrade gracefully when absent.
Layered on top of that mechanical seam is a shared worked example: every on-chain and off-chain template implements the same gift card (a one-shot minting policy plus a redeem spending validator) with a common parameter/redeemer ABI. The mechanical contract makes tools pluggable; the shared example makes a mixed pair actually work end-to-end — the off-chain template parameterizes the compiled validators straight from the blueprint, so e.g. the Scalus off-chain drives an Aiken contract, and MeshJS drives a Scalus one. See ADDING_A_TOOL.md (“The reference example: Gift Card”) for the exact ABI a new tool must match.
The blueprint/ directory is scaffolded whenever any blueprint-producing-or-consuming
role is present, every project except infrastructure-only (§6.2), so the canonical
path exists wherever it’s meaningful; the plutus.json file within it may still be
absent (no on-chain role, or no build yet), which is why consumers must tolerate its
absence. The CLI never tracks which tools produce/consume blueprints: it is a
template-level convention verified by tests, not registry metadata.
5. Registry
Each tool is one TOML file under registry/tools/:
[tool]
id = "aiken"
name = "Aiken"
description = "…newcomer-friendly explanation…"
website = "https://aiken-lang.org"
languages = ["aiken"]
system_deps = ["aiken"] # abstract dep ids → resolved via registry/deps.toml (§8)
nix_packages = ["aiken"] # packages for the generated Nix dev shell
[roles.on-chain]
template = "aiken/on-chain" # path under templates/
loader.rs iterates embedded assets, parses each TOML into a ToolDef, and builds a Registry with two indexes: by_id (lookup) and by_role (list tools for a role). Loading rejects duplicate ids and an empty registry. The registry is immutable after load.
6. Scaffolding pipeline
Four independent, individually testable phases. --dry-run stops after phase 2.
Selection + Registry
│
▼
┌────────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐
│ 1. Context │──▶│ 2. Plan │──▶│ 3. Render │──▶│ 4. Write │
│ build_context()│ │ plan() │ │ render() │ │ write() │
│ (pure) │ │ (pure) │ │ (pure) │ │ (side effects)│
└────────────────┘ └──────────────┘ └──────────────┘ └───────────────┘
│
--dry-run exits here (returns FilePlan)
6.1 Context (context.rs)
Walks selection.assignments, resolves each tool against the registry, and builds the TemplateContext: per-role has_* flags and RoleContexts, deduplicated nix_packages, contract constants, and .env variable seeds. Errors on unknown tool or role mismatch.
6.2 Plan (planner.rs)
Produces the ordered FilePlan:
- Base layer (always):
Justfile,README.md,AGENTS.md,CLAUDE.md,.gitignore,.env. - Blueprint dir:
blueprint/.gitkeep, emitted whenever the selection includes any blueprint-producing-or-consuming role: i.e., any role except infrastructure (equivalently: present unless the project is infrastructure-only). - Role layers: for each assignment, read the template’s
manifest.tomland add its files. Two cases aggregate instead of emitting one directory per assignment:- Fullstack collapses: when the same tool fills on-chain + off-chain and declares a
[fullstack]template (§3.2), the two assignments emit oneprotocol/component (fullstack.template), emitted once on the first of the pair; the second is skipped.has_fullstackis set;has_on_chain/has_off_chainare not. - Infrastructure is special — it aggregates: all selected infra tools share one driver template (
_infra/cardano-up) emitted once atinfra/, rendered over the full set (TemplateContext.infra_tools). This is because the infra engine (cardano-up) manages the whole stack as a single unit, not per service. Every other role is one tool → one directory.
- Fullstack collapses: when the same tool fills on-chain + off-chain and declares a
- Optional layer:
flake.nix+.envrcwhennixis set. - Spec layer (default; skipped by
--no-spec):spec/README.md, plusspec/giftcard.md(a plain-Markdown spec of the gift-card worked example) when an on-chain or off-chain role is present. User-owned once generated:add/removenever touch it.
No I/O: only embedded assets are read. render is set from the .jinja extension.
The blueprint/ directory gives every blueprint-consuming role (off-chain, devnet, formal-methods) a stable, predictable path to read from, and lets a user drop a hand-supplied or externally-built plutus.json into the same place even when on-chain isn’t scaffolded in this project. It is omitted only for infrastructure-only projects, where no role produces or consumes a blueprint. Only the directory (via .gitkeep) is created; the plutus.json file is produced by on-chain build, so consumers must still handle its absence gracefully (§4).
Code note: the current
planner.rscreatesblueprint/.gitkeeponly when on-chain is present (guarded by ahas_on_chaincheck). The rule above broadens that guard to “any non-infrastructure role present”.
6.3 Render (renderer.rs) & Write (writer.rs)
Render processes each entry whose source is a .jinja template through MiniJinja with the TemplateContext. Render-ness is derived from the file extension at plan time: the planner sets FileEntry.render = source.ends_with(".jinja") (§6.2); it is not an authored manifest field (manifests list only source/dest). Non-.jinja files, and Inline sources, pass through verbatim. Write is the only phase that touches disk: it creates parent directories and writes each file’s content.
6.4 Determinism rule
Determinism is a guarantee of the planning phase. The planner emits entries in a fixed order: base layer → blueprint dir (when any non-infrastructure role is present) → role layers in Role::ALL order → optional layer. Within Infrastructure (the only multi-tool role), tools are ordered by sorted tool id. Any HashMap (e.g. env_vars) is iterated through a sorted/canonical view before it reaches output. Snapshot tests over --dry-run and rendered output enforce byte-stability. No other phase may introduce nondeterministic ordering.
Current code stores
rolesin aHashMapand preservesassignmentsin flag order; formalizing the canonical ordering above (esp. sorting infra by id and iteratingassignmentsinRole::ALLorder) is the concrete work item this rule mandates.
7. CLI surfaces & output model
7.1 Modes
cli/mod.rs parses args with clap. There is an optional subcommand and a flattened set of init flags:
- One-shot (
--name+ role flags): flags →Selectioninoneshot.rs, non-interactive, deterministic. Primary path for agents and CI. - Interactive (no
--name): guideddialoguerflow ininteractive.rs. listsubcommand: capability discovery; lists roles/tools, human by default,--format jsonfor agents (see §7.3).
A safety check refuses to overwrite an existing target directory.
7.2 Output model: --format + presenter
To serve both humans and agents without scattering format branches:
- A global
--format human|jsonflag (defaulthuman;jsonimplies non-interactive: JSON mode never prompts). output.rsas a presenter: the core returns structured results and typed errors; only the presenter knows about colors, tables, or JSON. Every command’s JSON wraps in the §2.4 envelope viaoutput::emit_json_ok/print_error. Adding a new output is a presenter change, nothing else.
7.3 Machine-readable errors & discovery (PRD FR-13/FR-15)
- Errors carry a stable string code (e.g.
unknown_tool,tool_role_mismatch,name_required,dir_exists) plus context (offending input + valid alternatives) and map to meaningful exit codes. In--format json, errors serialize to a stable shape on stderr; the core never falls back to interactive prompting in non-interactive mode.CliError::code()/context()carry the code + serializable context (§2.5). - Discovery is the
listsubcommand (cardano-init list) that emits the registry (roles, tools, the roles each fills, languages). Human by default (with--tablefor a compact tools-by-role matrix),--format jsonfor agents (§8 schema).listrenders from the shared modelregistry::view(role_views()/tool_views()); the human tool block reusescli::format_tool(shared with--help).
8. Dependency doctor (doctor/)
Scope (DX.02, implemented): the standalone cardano-init doctor command plus check + advise after generation. The doctor is a dependency checker/advisor, not a project validator — it reports presence and prints install plans; it never asserts a component actually builds (that’s just build/just test, §7), never offers alternative runtimes (templates fix their toolchain), and checks presence only (no versions). Auto-install (running the resolved plan with consent) is a later nice-to-have install command (DX.05); see ROADMAP and the scope note in TECH_SPEC §9. The dependency catalog is a small graph, split between code and data along the purity invariant:
doctor/
├── mod.rs Pure: resolve(targets, catalog, env) -> Report (recursive, cycle-safe)
├── installers.rs Pure (code): the closed `Installer` vocabulary. Per installer: detect binaries, command template, and a `bootstrap` list of dep ids
├── catalog.rs Loads embedded registry/deps.toml -> DepCatalog (dep id → recipe)
└── probe.rs Impure: detect OS + which installers are on PATH -> Environment
- Two-tier inputs. The selection yields required deps =
{just}(universal task runner) ∪ thesystem_depsof all selected tools (unioned, deduped); and recommended deps (soft notes, never blocking). The two-tier mechanism stands, but there is currently no recommended dep: the formerprocess-compose/≥2-infra case existed only to smooth a multi-service top-leveljust dev, which no longer exists (the top level no longer aggregatesdev; long-running services start per-component — TECH_SPEC §7.2/§9.1).justis a base/derived dep owned by no tool. - Installers vs deps: the key model. An installer is just another dependency. Code owns a closed
Installervocabulary (Brew,Apt,Dnf,Pacman,Winget,Nix,Go,Cargo,Npm,Aikup,CardanoUp,Tx3up,Curl,PowerShell); each declares its detect-binaries, a command template (brew install {arg},npm install -g {arg},curl -sSfL {arg} | sh, …), and abootstraplist of dep ids. An emptybootstraplist ⇒ terminal (we detect it, never install it: system package managers,nix, the OS shells); a non-empty list ⇒ bootstrappable by installing any one of those deps in order (npm→["node"],aikup→["aikup"],cargo→["rustup","rust"]). This is what makes the catalog a graph rather than a flat list. - Recipes live in data. Per-dep recipes are an embedded TOML file (
registry/deps.toml), keyed by dep id:binaries(presence check),docs(universal fallback), and an orderedinstalllist of{ installer = arg }methods. Installer names are validated against the code enum at load (unknown installer → load error, like an unknownRole). See §8.1 for why code/data split this way. - Resolver (
resolve, pure, recursive). A dep is present if any of itsbinariesis onPATH. For a missing dep, the walk is two-pass over the orderedinstallmethods: Pass 1 returns the first method whose installer is detected (a one-step command); only if none is directly available does Pass 2 walk the methods again and, for the first bootstrappable installer, recurse to satisfy one of itsbootstrapdeps and prepend those steps. The result is an ordered, possibly multi-step plan (e.g.aikenmissing with nonix/aikup→ installaikupvianpm, thenaikup install). Two passes — rather than bootstrapping each method before trying later ones — are exactly why thenixpath needs noaikupwhennixis present (a single method is still chosen per dep). Cycle detection guards the walk;docsis the fallback when nothing resolves (advice never empty, FR-20). Version constraints are out of scope for v1 (presence only); doctor output is host-dependent by design (not part of the byte-identical generation contract). Full algorithm in TECH_SPEC §9.4. - Infrastructure deps install via
cardano-up(theCardanoUpinstaller);cardano-upis itself a dep inregistry/deps.toml(bootstrappable via its own installer methods). Auto-installing it arrives with the DX.05 install command; bootstrappingcardano-upwhen absent may follow post-RC (ROADMAP). - Project scan (no metadata file). The standalone doctor derives its target set by scanning the cwd: each contract role directory present is matched against the
detectsignatures of the tools that fill that role; an identified tool contributes itssystem_deps, and an unmatched directory is reported as unrecognized. Aprotocol/directory (the fullstack fused component, §3.2) is scanned by a dedicated branch against the tools that declare a[fullstack]template; the identified tool is a real registry tool, so itssystem_depsfeed the required set through the normalregistry.getpath (unlike the synthetic infra driver). Signatures are tool-author data inregistry/tools/<tool>.toml(detect = [...]), either a bare path (existence) or{ file, contains }(content) — the content form keeps generic filenames likepackage.jsonfrom mislabeling foreign projects without claiming to validate viability. Full algorithm + schema in TECH_SPEC §9.6. - Boundary:
mod.rs/installers.rs/catalog.rsare pure and unit-tested with syntheticEnvironments; onlyprobe.rstouches the system (PATH/OS probes + the project scan).doctordepends onregistry/contract, never oncli.
8.1 The code/data split
The catalog is a graph with two kinds of node, split by what each kind is:
- Installers are code (
installers.rs). Detection, command templating, and thebootstrapedges are logic, and the set is a closed vocabulary, so it earns compile-time safety (installer references are un-typo-able; a removed installer fails to compile) and one tested home for platform quirks. Adding an installer is a deliberate code change, done only when a real recipe needs it on a supported platform. - Recipes are data (
registry/deps.toml). This is what honors the project’s extensibility promise: a tool author adds a tool by writingsystem_deps = [...]and, if a dep is new, aregistry/deps.tomlentry that chooses from the existing installer vocabulary, with no Rust. Recipes are deduplicated by dep id (shared deps likenode/jvmare defined once and referenced by many tools), and installer names are validated against the enum at load.
This split is the reversal of the earlier “in-code catalog” : the common case (a new tool whose deps install via existing installers) becomes pure data, which is the whole point of the registry model. The narrow case that still needs code (a brand-new installer) is rare and benefits from maintainer review anyway. Safety is preserved because data only ever names a closed, code-defined installer plus an arg; it never carries free-form command logic.
# registry/deps.toml: keyed by dep id; install = ordered [{ installer = arg }]
[node]
binaries = ["node"]
docs = "https://nodejs.org/en/download"
install = [ { brew = "node" }, { apt = "nodejs" }, { winget = "OpenJS.NodeJS" }, { nix = "nodejs" } ]
[aikup]
binaries = ["aikup"]
docs = "https://aiken-lang.org/installation-instructions"
install = [ { npm = "@aiken-lang/aikup" }, { curl = "https://install.aiken-lang.org" }, { powershell = "https://windows.aiken-lang.org" } ]
[aiken]
binaries = ["aiken"]
docs = "https://aiken-lang.org/installation-instructions"
install = [ { aikup = "" }, { nix = "aiken" } ]
Referential integrity (tests): every system_deps id (plus the base dep just) has a registry/deps.toml entry; every installer named in the data exists in the Installer enum; every dep id in an installer’s bootstrap list exists. The full field-by-field schema and the resolver algorithm are in TECH_SPEC §9.
9. Version-update check
The chosen mechanism for template freshness without runtime template fetching (PRD A-3/FR-24). It is a thin cli/ concern (UX, network, never core). It lives in cli/version_check/ (the cli/update.rs name is taken by the add/remove project mutations, §6.4), with pure, unit-tested install-method detection:
- Best-effort check against the GitHub releases API; the notice (if any) is surfaced before the write phase, so the user can update and regenerate rather than discovering it post-write. It informs, never gates (the user may Ctrl-C to update first); it never alters generated output.
- Latency is hidden, not added. In interactive mode the check fires async at startup and completes during tool selection: zero added latency. In human one-shot there’s no think-time to hide it, so the result is joined with a ≤1s deadline behind a spinner before writing (worst case +1s, once/day).
- Cached once/day (small file in the OS cache dir): already-checked-today → zero network, zero latency.
- Install-aware: the notice shows the update command for how the binary was installed (npm/npx, nix, cargo-dist installer,
cargo install), falling back to the releases link (TECH_SPEC §10). - Gated and fail-silent: the network check runs only for a TTY with
humanformat, and is off whenCI/CARDANO_INIT_NO_UPDATE_CHECKis set.--format jsonnever touches the network. Offline/timeout/parse error → no-op. Preserves offline operation and determinism (A-3).
10. Testing strategy
- Unit (pure core): registry loading (every TOML parses, fields present); context building; planning (exact file set + order); rendering (context + template → expected output); doctor
resolveover synthetic environments (incl. multi-step bootstrap chains and the cycle guard). - Contract compliance (mechanical): for each template, assert the Justfile exposes
build/test/clean(devis optional); for on-chain, assertjust buildproducesblueprint/plutus.json. This is what lets us avoid testing tool combinations. - Per-tool build smoke tests: scaffold each tool in isolation and, where CI has the toolchain (or via Nix), run
just build && just test. New tools must add these (PRD SM-1). - Scheduled maintenance gate: the per-tool smoke tests also run on a schedule (weekly cron + manual dispatch,
.github/workflows/scheduled-smoke.yml), not only on PR/commit. This is what detects a generated project breaking with no repo change — a Cardano hardfork, a breaking upstream tool release, or an unmaintained dependency (templates pin floating version ranges). A failure opens a tracking issue. It is distinct from the PR gates, which catch regressions we introduce. - Determinism / snapshot tests:
--dry-runand rendered output compared against committed snapshots for a set of selections; guards §6.4. - No combinatorial testing: composition is guaranteed by the contract, so we verify each tool individually rather than every pair.
11. Extensibility: adding a tool
- Add
registry/tools/<tool>.tomlwith metadata,system_deps,nix_packages, and a[roles.<role>]block per supported role. - Add
templates/<tool>/<role>/with amanifest.tomland template files (conforming to the contract, §4). - If the tool introduces a new
system_depsid, add aregistry/deps.tomlentry (pure data; code is needed only if the dep requires a brand-new installer, §8). - Add the per-tool tests (§10).
- Recompile (assets are embedded at compile time).
No CLI/core code changes are required for a new tool. Contract conformance guarantees it composes with every existing tool in other roles.
12. Open architectural decisions
None currently open.
cardano-init: Product Requirements Document
Status: Draft · Last updated: 2026-06-01 · Owner: Robertino Martinez
Companion documents: ARCHITECTURE.md (system design), TECH_SPEC.md (technical decisions, contracts, data models), ROADMAP.md (phases and milestones). This PRD owns the why and for whom; those documents own the how and when.
1. Summary
cardano-init is a CLI tool that scaffolds a complete, runnable Cardano protocol monorepo. The user picks a tool for each functional role they need (on-chain, off-chain, infrastructure, devnet, and other registry-defined roles) and the tool generates a project where every component is already wired together and a small end-to-end example builds and passes its tests out of the box.
The defining bet is the interface contract: every tool template conforms to a shared set of conventions (canonical blueprint path, standard Justfile tasks, standard .env variables) so any producer composes with any consumer without per-pair integration code. Adding a tool is a data change, not a code change.
2. Problem
Two distinct pains block people from starting a Cardano protocol project:
-
Cross-tool wiring is hard and undifferentiated work. Individual tools ship their own
init(aiken new, etc.), but nothing wires across roles. Connecting an on-chain validator to an off-chain transaction builder, off-chain with local infrastructure or a local devnet, on-chain with formal methods, environment variables, build commands across tools, etc. This takes time, is error-prone, and has to be redone for every new combination of tools. -
The tooling landscape is opaque to newcomers. A developer arriving at Cardano faces choice paralysis: which tool writes validators, which builds transactions, which indexes the chain, and which of them actually work together? The cost of a wrong early choice is high, and there is no opinionated, trustworthy starting point that explains the landscape while setting them up.
cardano-init attacks (1) with the interface contract and a generic composition pipeline, and (2) with a curated, self-describing tool registry plus guided selection and recommendations.
Why now / why this and not aiken new
We needed this years ago. Cardano developer experience is not great at many points of the develpment cycle, but getting started is one of the worst and most notable. Since onboarding is suffered by newcomer developers exploring Cardano, having a bad and confusing developer experience means their first impression incentivizes them to move away to other ecosystems. A tool that solve the “choosing the stack and setting up the project” problem means more people will be able to pass through those initial hurdles and get to building their own project faster.
Per-tool scaffolders solve the single-tool case and stop at the role boundary. The value here is precisely the seam between tools: the part no single tool owns. As the Cardano tooling ecosystem fragments across languages (Aiken, Haskell/Plinth, Scala/Scalus, TypeScript) the combinatorial wiring problem gets worse, and a contract-based composer is an approach that doesn’t scale as O(tools²).
3. Target users (personas)
3.1 Primary: Cardano newcomer
A competent developer (possibly new to blockchain entirely) who does not know the Cardano tooling landscape. We optimize for this persona when tradeoffs arise:
- Explanation over terseness.
- Sane defaults over open-ended choices.
- And a project that runs immediately so they can see the shape of a working protocol before writing anything themselves.
3.2 Primary: Coding agents
LLM-driven agents (e.g. Claude Code) scaffolding a project on a user’s behalf. Agents are a first-class consumer end-to-end:
- They drive the CLI: One-shot mode must be rock-solid, deterministic output, stable machine-parseable flags, meaningful exit codes, no TTY assumptions.
- They discover capabilities: An agent must learn what roles and tools exist without guessing, via a structured (JSON) registry dump it reads before composing a command.
- They read the generated output: Predictable structure, self-explanatory READMEs, and the interface contract documented in-repo so the agent can keep building inside the project.
- They need actionable errors: Invalid selections must produce machine-readable, corrective errors (e.g. “tool X does not fill role Y; valid tools: …”) rather than dropping into an interactive re-prompt.
3.3 Secondary: Experienced Cardano developer
Already ships Cardano code; wants a fast, opinionated starting point to skip boilerplate. Benefits from one-shot mode, sane defaults, and the contract, but we do not sacrifice newcomer/agent ergonomics to serve this persona.
4. Goals & success metrics
The PRD commits to two headline metrics. Both are measurable in CI and tied to the two core problems.
| # | Metric | Target | Measurement |
|---|---|---|---|
| SM-1 | Generated project builds out of the box | just build && just test passes for every shipped template, verified individually, with zero manual edits. Composition across roles is guaranteed by the interface contract, so combinations are not tested pairwise. | CI scaffolds each tool/template in isolation, then runs build+test with required toolchains present. |
| SM-2 | Time-to-first-working-protocol (TTFP) | < 5 minutes from cardano-init to a green just build && just test. If toolchains are missing and the user accepts the offered install, that path must also complete within ~5 minutes. | Timed walkthrough of a representative selection on a clean machine. Clock excludes nothing the user experiences except their own reading time. |
Supporting (tracked, not headline): tool/role coverage and composability (every advertised role has ≥1 working tool; any on-chain × off-chain combo works without per-pair code); adoption (projects generated, repos that retain the scaffold structure) once the tool is public.
5. Scope
5.1 In scope for v1
- Roles are a fixed, code-defined vocabulary; tools are the open-ended part. The set of roles is defined in code, not data: the registry references roles but cannot introduce them. It is not frozen at “four”: the current set is on-chain, off-chain, infrastructure, devnet, and formal-methods, and it can grow in a future version via a deliberate code change. Tools, by contrast, are fully data-driven: adding one is a registry + template change with no core code change (see ARCHITECTURE.md §3.1).
- At least one working tool per advertised role. No role is advertised with zero working tools. There is no single “golden path” combination: every shipped tool is verified individually and the interface contract guarantees that any combination composes (§7), so combinations are not tested pairwise.
- Whatever ships in the registry must work. Every registered tool is held to the build/contract bar; adding a tool requires adding its tests (§7, SM-1).
- Two surfaces: One-shot CLI and interactive CLI (§6).
- Interface contract enforced mechanically by contract-compliance tests.
- Target network is selectable:
preview/preprod/mainnet, defaulting topreview(the natural starting point for newcomers). The choice is written to.envasCARDANO_NETWORK, so it is a cheap, late-binding decision, trivially changed after generation. - Dependency doctor (check + advise): Detect which
system_depsare missing, detect the OS and available package manager, and print exact install instructions. For the infrastructure role, the recommended install path delegates tocardano-up. - User documentation: Thorough user documentation to be used both by developers and LLM agents. Also, links and references to documentation and Discord servers based on the user chosen stack on the generated README.
5.2 Explicit non-goals
These are things users might reasonably expect that we deliberately will not do:
- Not a build / deploy / runtime tool. It scaffolds once and exits. It does not build, deploy, submit transactions, manage keys or wallets, or run the protocol. The generated Justfile delegates to native tools;
cardano-initis not in the loop after generation. - Not a package or version manager. It does not pin/upgrade tool versions over time, or manage tool/dependency versions after generation. Editing the project’s composition is supported (see below), but bumping the tooling itself is out of scope. (Off-chain packages are still version-pinned in the generated
package.jsonetc.; the user owns them thereafter.)
Note — editing an existing project (FR-25): the project’s role/tool composition is editable after generation via
cardano-init add/remove(swap Aiken→Pebble, add off-chain, drop a role). This adds/removes/replaces whole component folders and re-wires the shared top-level files; it never rewrites user code inside a component it keeps, and it is guarded by a git-clean check so every change is reviewable withgit diff. It recovers the current selection by detection (no metadata file — matching the doctor, TECH_SPEC §9.6). See TECH_SPEC §15. This is deliberately not version management (above).
- Not a tutorial or learning platform. Generated READMEs/AGENT.md/docs/skills explain enough to start and link out, but the tool is not a course, interactive tutorial, or docs site. It produces a runnable example, not curriculum.
- It does not author tool templates for you. Adding a tool requires a human to write the template + registry entry and recompile. The tool does not auto-generate templates or scrape tool capabilities.
5.3 Deferred (post-v1)
- Dependency doctor auto-install (running installs with consent): a nice-to-have targeted for the RC (ROADMAP DX.05);
cardano-upis installed as a dependency likeaikup. (The standalonecardano-init doctorcommand is not deferred: it’s a DX.02 deliverable.) - Plugin / lifecycle hooks for tools, if and when needed (e.g. “after scaffolding, run
devkit start”). - Config-file driven runs beyond flags, if needed.
See ROADMAP.md for sequencing.
6. Surfaces (all v1)
Both surfaces produce identical projects because the CLI is the single source of truth for generation.
| Surface | Primary persona | Description |
|---|---|---|
| One-shot CLI | Agent, experienced dev | Fully flag-driven, non-interactive, deterministic. Plus capability discovery (structured registry dump), --dry-run, and machine-readable errors. |
| Interactive CLI | Newcomer | Guided flow: explain the domain → multi-select roles → pick a tool per role (with recommendations) → set options → review summary + file tree → confirm. |
In all cases, there will be extensive explanation on each tooling role, language, when (and when not) to use it, mapping of usecases and common tooling, and other to help both newcommers and LLM agents to choose the right tool for the job without them having to do reasearch on the side.
7. Functional requirements
Priority: M = Must (v1), S = Should (v1 if affordable), C = Could (later).
Selection & roles
- FR-1 (M): The user can select one or more roles; they are not required to fill all roles. A single-role project (e.g. on-chain only) is valid.
- FR-2 (M): At most one tool per role, except infrastructure, which may have multiple tools simultaneously.
- FR-3 (M): The set of roles and tools is loaded from the embedded registry; no role or tool is hard-coded in CLI logic.
- FR-4 (S): During interactive selection, the tool surfaces recommendations for remaining roles based on choices made so far (e.g. tools that pair well / share a language).
Generation
- FR-5 (M): Generate a monorepo with role directories only for selected roles, plus a base layer present in every project: a top-level orchestrating
Justfile, a top-level README explaining the full architecture,.gitignore,.env(always: it seedsCARDANO_NETWORKand the standard chain connection vars), and theblueprint/directory (present for every project except infrastructure-only: so every blueprint-consuming role has a stable path and a user can drop in an externalplutus.json; theplutus.jsonfile itself is produced by on-chainbuild). - FR-6 (M): Every generated project includes a simple but complete, runnable example that demonstrates the selected components working together, not in isolation.
- FR-7 (M): Composition is generic. The pipeline wires components using only the set of present roles and the interface contract, with no per-tool-pair logic.
- FR-8 (M): Per-component
Justfiles expose standardized targets (build,test,clean; plus an optionaldevwhen the tool has a watch/daemon/devnet mode). The top level aggregates only the terminating, composable tasks —build,test,clean— delegating to each component (testbuilds the on-chain blueprint first, then runs each component’stestin role order);dev, where present, is run per-component by the developer and is not aggregated. - FR-9 (S): Optional Nix flake (
flake.nix) providing a dev shell with all required toolchains, opt-in at selection time. Without Nix, prerequisites are documented in the README. - FR-26 (S): Generate a top-level
spec/folder holding the protocol’s specification in plain, non-executable Markdown, pre-populated with a spec of the worked example (actors, assets, validators, transactions, invariants) so system design has a clear place in the project. Created by default; skipped with--no-spec.
Interface contract (mechanically enforced, see TECH_SPEC)
- FR-10 (M): On-chain templates produce the CIP-57 blueprint at the canonical path (
blueprint/plutus.json) duringbuildand other roles (e.g. off-chain, devnet, formal-methods) read it from the same path. - FR-11 (M): Whichever component provisions a local chain endpoint writes standardized connection details to
.env(e.g.INDEXER_URL) duringdev— an infrastructure service, or a local devnet in the devnet role (e.g. Yaci DevKit); consumers read from there and react only to the presence of the keys, not to which role wrote them. - FR-12 (M): Every template works independently (e.g., its
just buildsucceeds with no other roles present), and has to handle all sharedjustcommands. It can optionally add more to be ran on that role’s folder, but it can’t avoid handling the shared ones (it should, at least, print a message).
Agent affordances
- FR-13 (M): A capability-discovery command emits the registry (roles, tools, which roles each tool fills, languages, deps) in two forms derived from the same data: a human-readable listing (
--list) usable by people at the terminal, and a structured machine-readable form (e.g. JSON) for programmatic/agent consumption. - FR-14 (M): One-shot mode is fully non-interactive and deterministic (same inputs → same output), with stable flags and meaningful exit codes.
- FR-15 (M): Invalid selections produce actionable, machine-readable errors (offending input + valid alternatives) and never fall back to interactive prompting in non-interactive mode.
- FR-16 (M):
--dry-runprints exactly what would be generated and writes nothing.
Dependency doctor (v1: check + advise)
- FR-17 (M): After (or before) generation, detect which required
system_depsare missing on the user’s machine. - FR-18 (M): Detect the host OS and available package manager(s) and print the exact install commands for the missing dependencies.
- FR-19 (M): For the infrastructure role, the advised install path uses
cardano-upas the primary mechanism for provisioning infra tooling. Ifcardano-upitself is absent, v1 instructs the user to install it (auto-install is deferred, §5.3). - FR-20 (M): If dependencies cannot be satisfied, clearly tell the user which to install manually and state that the generated template is otherwise correct and ready once they do.
- FR-21 (C): Offer to run the installs with user consent (auto-install). Nice-to-have, targeted ROADMAP DX.05.
- FR-22 (S): Expose the doctor as a standalone
cardano-init doctorsubcommand runnable in an existing project. Targeted DX.02. - FR-25 (S): Edit an existing project’s role/tool composition via
cardano-init add/remove: reconstruct the current selection by detection, apply the change (add/remove/replace whole component folders, re-wire the shared top-level files), and never touch user code in a kept component. Guarded by a git-clean check (--forceto override) with a--dry-runpreview; reuses the init compatibility/experimental gates on the resulting selection. Not version management (§5.2). See TECH_SPEC §15.
Versioning & updates
- FR-24 (S): On startup, the tool performs a best-effort check for a newer
cardano-initrelease and, if one exists, notifies the user and suggests updating before generation (so they can update and regenerate rather than discovering it post-write). It is cached once/day, gated to interactive/human-TTY runs (no network for--format json/non-TTY), suggests the update command for how the tool was installed (falling back to the release link), and bounded: latency is hidden behind interactive selection, or capped at a short deadline (~1s) for human one-shot. It never blocks beyond that deadline, never alters generated output, and degrades silently when offline. It is the chosen mechanism for keeping templates fresh without runtime template fetching (see A-3; details in TECH_SPEC §10).
8. User stories & acceptance criteria
Format: As a … I want … so that …, followed by acceptance criteria.
US-1: Newcomer, guided setup
As a developer new to Cardano, I want a guided flow that explains the roles and recommends tools, so that I can create a working project without already knowing the ecosystem.
Acceptance:
- Running
cardano-initwith no arguments enters the interactive flow and explains what each role means before asking the user to choose. - The user can complete the flow selecting only the roles they want.
- A summary screen shows the full selection and the exact directory tree before anything is written; the user must confirm.
- On confirmation, the project is generated and the next steps (
cd …,just build) are printed.
US-2: Newcomer, it just works
As a newcomer, I want the generated project to build and pass tests immediately, so that I know my setup is correct and can see a working protocol.
Acceptance (SM-1, SM-2):
- With the required toolchains present,
just build && just testpasses with zero manual edits for any combination of tools. - The end-to-end example demonstrates the selected roles working together.
- TTFP is < 5 minutes for any selection.
US-3: Newcomer, missing toolchains
As a newcomer who doesn’t have the toolchains installed, I want the tool to tell me exactly what’s missing and how to install it, so that I’m not stuck deciphering errors.
Acceptance (FR-17–FR-20):
- The tool reports which
system_depsare missing. - It prints exact install commands for the user’s OS/package manager and links to relevant documentation.
- For infrastructure deps, it advises the
cardano-uppath. - If it cannot help, it lists what to install manually and confirms the template is otherwise ready.
US-4: Agent drives the CLI
As a coding agent, I want to discover available tools/roles as structured data and then generate a project non-interactively, so that I can scaffold on a user’s behalf reliably.
Acceptance (FR-13–FR-16):
- The agent can obtain the full registry as JSON in one command.
- The agent should have enough information to chose the tool or ask an informed decision to the developer based on what the developer asked it to build.
- A one-shot invocation with valid flags generates the project with no prompts and a zero exit code; identical inputs yield identical output.
- An invalid invocation exits non-zero with a machine-readable message naming the problem and the valid options, without prompting.
--dry-runreports the plan and writes nothing.
US-5: Agent continues in the project
As a coding agent, after scaffolding I want predictable structure and the contract documented in-repo, so that I can keep building without guessing conventions.
Acceptance (FR-5, FR-8, §5.2 docs):
- The directory layout matches the documented canonical structure.
- The interface contract (canonical paths, env vars, Justfile tasks) is discoverable from within the generated project.
- There are documentation and tools (README.md, AGENT.md, skills directory, MCPs, etc.) to provide the best support possible for the agent to iterate over the code.
US-6: Single-role project
As a developer who only needs validators, I want to scaffold an on-chain-only project, so that I’m not forced into components I don’t need.
Acceptance (FR-1, FR-12):
- Selecting only one role (e.g. on-chain) generates only the role’s directory (e.g., on-chain) plus base files.
just *succeeds with no other roles present.
US-7: Extending with a new tool
As a tool author, I want to add support for my tool by writing data and a template, so that it composes with every existing tool without combinatorial work.
Acceptance (FR-3, FR-7, §7):
- Adding a registry entry + template(s) + recompiling makes the tool available in all surfaces with no CLI code changes.
- A new tool that satisfies the contract composes with existing tools in other roles without per-pair logic.
- New tools ship with tests verifying contract compliance and that they build (SM-1).
9. Assumptions & dependencies
- A-1: Users have, or are willing to install, the native toolchains for their chosen tools (the tool advises but, in v1, does not install them automatically; Nix is the supported turnkey path).
- A-2:
cardano-upis the intended primary mechanism for provisioning infrastructure tooling; v1 assumes the user can installcardano-upwhen advised. - A-3: Generation is offline and deterministic: the registry and all templates are embedded in the binary at compile time and are the single source of truth for a given version, so the same binary + inputs always produce the same project (required by FR-14, SM-1, and agent trust). The tool does not fetch templates at runtime. Template/tool freshness is handled out-of-band by notifying the user when a newer
cardano-initis available (an explicit binary update), never by silently changing generation output. Network is only needed for installing toolchains/deps and for the version-update check (FR-24). - A-4:
justis the task runner for all generated projects.
cardano-init: Technical Specification
Status: Draft · Last updated: 2026-06-01 · Owner: Robertino Martinez
This document owns the exact contracts, schemas, algorithms, and edge cases. For the why/for whom see PRD.md; for the how/structure see ARCHITECTURE.md; for sequencing see ROADMAP.md. Where this spec describes behavior not yet in the code, it is marked (planned).
1. Conventions & versioning
- Schema version. All machine-readable (
--format json) output carries a single integerschema_version, starting at 1, global across every command. Additive fields do not bump it; removing/renaming/retyping a field or changing semantics does. Agents should tolerate unknown additive fields. - Embedded data is versioned with the binary. The registry (tools +
registry/deps.toml), templates, and the code-side installer table are compiled in; there is no on-disk schema-version negotiation. A given binary is the single source of truth for what it generates (PRD A-3). - Determinism (§11) is a hard contract, relied on by snapshot tests and agents.
2. CLI surface
2.1 Commands
cardano-init [INIT_FLAGS] # default: one-shot if --name given, else interactive
cardano-init doctor # check this project's dependencies + advise installs (§9)
cardano-init list [--table] [--format <fmt>] # capability discovery: roles + tools (§8)
cardano-init add [ROLE_FLAGS] # add/swap tools in the project in the cwd (see proposal)
cardano-init remove [ROLE_FLAGS] # remove a role / infra provider from the cwd project
The add/remove commands operate on the project in the current directory: they reconstruct its Selection by detection (no metadata file; §9.6), apply the change at the component-directory level, and re-wire the shared top-level files. They accept --dry-run (preview only), --force (update despite a dirty git tree), --ignore-warning, and --allow-experimental. add takes the same role flags as init (--on-chain, --off-chain, --fullstack, --infra (repeatable), --devnet, --formal-methods); remove takes bare role flags plus --infra <id>. The full algorithm and edge-case matrix are in §15.
--format human|json is a global flag; default human. json implies non-interactive: it never prompts; if required input is missing it errors instead.
2.2 Init flags (one-shot)
| Flag | Type | Notes |
|---|---|---|
--name <NAME>, -n | string | Presence selects one-shot mode. Validated per §3.5. |
--on-chain <TOOL_ID> (aliases --on, --onchain) | string | At most one. |
--off-chain <TOOL_ID> (aliases --off, --offchain) | string | At most one. |
--fullstack <TOOL_ID>, -f, -p (alias --protocol) | string | Sugar for --on-chain X --off-chain X. The tool must declare a [fullstack] template (§3.2). Mutually exclusive with --on-chain/--off-chain. |
--infra <TOOL_ID>, -i (alias --infrastructure) | string, repeatable | Multiple allowed (only multi-tool role). |
--devnet <TOOL_ID>, -d | string | At most one. |
--formal-methods <TOOL_ID> (aliases --formal, --formalmethods) | string | At most one. |
--nix | bool | Emit flake.nix + .envrc. |
--no-spec | bool | Skip the spec/ protocol-specification folder (§6.1). Generated by default; interactive mode asks (default yes). |
--allow-experimental, -e (alias --experimental) | bool | Opt in to experimental tools (§3.2.1). Required to select one in one-shot/JSON; pre-acknowledges the interactive confirm. |
--ignore-warning (alias --ignore-warnings) | bool | Scaffold an off-chain ↔ provider combination the compatibility gate flags as incompatible (§3.2.2). Downgrades the stop to a warning. |
--dry-run (alias --dryrun) | bool | Plan only; write nothing; exit 0. |
Short flags and aliases are input conveniences only: they parse to the same canonical flag, and errors, JSON contexts and docs always use the canonical long name. add/remove accept the same role-flag aliases and UpdateFlags aliases (-e, --dryrun, --ignore-warnings); --force deliberately has no short form.
Mode resolution: if --name is present → one-shot; else → interactive. Providing any one-shot flag without --name is a usage error (name_required).
--fullstack is pure CLI sugar resolved at the edge: --fullstack X expands to the two
assignments --on-chain X + --off-chain X. Combining it with an explicit --on-chain/--off-chain
is a usage error (fullstack_conflict); naming a tool without a [fullstack] template is
fullstack_unsupported. The core never sees a “fullstack” flag — the resulting same-tool-both-roles
pair is what triggers the collapse into a single protocol/ component (§3.2, §6.1).
2.3 Exit codes
| Code | Meaning | Examples |
|---|---|---|
0 | Success (incl. --dry-run, and interactive abort-by-choice) | generated |
2 | Usage / validation error | bad flag, unknown_tool, tool_role_mismatch, no_roles_selected, invalid_project_name, name_required |
1 | Runtime error | dir_exists (non-empty), registry load failure, render/IO error |
The fine-grained “what” is the JSON error.code (§2.5); exit code is only the category. Interactive abort (user declines the confirmation prompt) exits 0 with no error, and never occurs in json/non-interactive mode.
2.4 JSON envelope
Every --format json response is one of:
{ "schema_version": 1, "ok": true, "data": { /* command payload */ } }
{ "schema_version": 1, "ok": false, "error": { "code": "<stable>", "message": "<human>", "context": { /* structured */ } } }
Success and error are symmetric (same envelope). message is human-readable and may change; code and context keys are part of the contract.
2.5 Error catalog
Stable codes, their exit category, and the context they carry. The context is the agent-facing “how to fix” (PRD FR-15).
code | Exit | context fields |
|---|---|---|
name_required | 2 | { } |
invalid_project_name | 2 | { name, reason } |
unknown_tool | 2 | { tool_id, role, valid_tools: [..] } |
tool_role_mismatch | 2 | { tool_id, role, valid_roles: [..] } |
fullstack_conflict | 2 | { } (--fullstack combined with --on-chain/--off-chain) |
fullstack_unsupported | 2 | { tool_id, valid_tools: [..] } (tool has no [fullstack] template; valid_tools = fullstack-capable tools) |
no_roles_selected | 2 | { } |
experimental_not_allowed | 2 | { tools: [..], remedy: "--allow-experimental" } (an experimental tool was selected in one-shot/JSON without --allow-experimental; §3.2.1) |
incompatible_tools | 2 | { tools: [off_chain_id, provider_ids…], reason, compatible_providers: [..], compatible_off_chain: [..], remedy: "--ignore-warning" } (the off-chain tool and its selected providers share no seam; §3.2.2) |
dir_exists | 1 | { path } (exists and non-empty) |
project_unrecognized | 2 | { dirs: [..] } — add/remove/edit couldn’t identify a component directory; fatal in json/non-interactive (never guesses) |
slot_occupied | 2 | { role, dir } — a create target directory already exists and isn’t the recognized current component |
nothing_to_change | 2 | { } — the requested edit is a no-op against the detected selection |
worktree_dirty | 1 | { path } — uncommitted changes (or not a git repo) and no --force |
registry_load | 1 | { file?, detail } |
scaffold_error | 1 | { path?, detail } (asset-not-found, manifest-parse, render, io) |
These map 1:1 to the CliError/ScaffoldError/RegistryError variants; CliError::code()/context() attach the code + serializable context, routed through the presenter (ARCHITECTURE §7.2).
3. Core data model
Exact types; field-level source of truth is src/registry/types.rs.
3.1 Role
#![allow(unused)]
fn main() {
pub enum Role { OnChain, OffChain, Infrastructure, Devnet, FormalMethods }
}
| Variant | kebab (TOML/flag) | dir (contract::DIR_*) | Display | multiple |
|---|---|---|---|---|
| OnChain | on-chain | on-chain | On-chain | no |
| OffChain | off-chain | off-chain | Off-chain | no |
| Infrastructure | infrastructure | infra | Infrastructure | yes |
| Devnet | devnet | devnet | Devnet | no |
| FormalMethods | formal-methods | formal-methods | Formal methods | no |
Role::ALLlists variants in the table’s order = the canonical order (§11).from_kebabis the only parse path; unknown →UnknownRoleError→RegistryError::UnknownRole.- The enum is the sole role vocabulary; the registry references but cannot add roles (ARCHITECTURE §3.1).
3.2 Tool & registry TOML schema
[tool]
id = "aiken" # required, unique across registry, kebab
name = "Aiken" # required, human display
description = "…" # required, newcomer-facing
website = "https://…" # required
languages = ["aiken"] # required, ≥1 — except infra tools, which may use [] (no user-facing language)
system_deps = ["aiken"] # required (may be []); abstract dep ids → registry/deps.toml (§9)
nix_packages = ["aiken"] # optional (default []); nixpkgs attrs for the dev shell
nix_self_contained = false # optional (default false); tool ships its own component flake (§4.1)
experimental = false # optional (default false); true = unstable and/or not build-green, gated (§3.2.1)
[roles.on-chain] # ≥1 [roles.<kebab>] block; key validated against Role
template = "aiken/on-chain" # required; path under templates/
A tool that fills both on-chain and off-chain may additionally declare a [fullstack]
table. Its template is used when the same tool fills both roles, collapsing them into one unified
protocol/ component instead of two folders (§6.1). This is a tool capability, not a role —
protocol is not a Role, and Role::ALL stays at five.
[roles.on-chain] # required: a fullstack tool must fill both
template = "scalus/on-chain" # (used when scalus fills on-chain only, e.g. with a TS off-chain)
[roles.off-chain]
template = "scalus/off-chain" # (used when scalus fills off-chain only)
[fullstack]
template = "scalus/fullstack" # used when scalus fills BOTH → one protocol/ component
Validated at load: [fullstack] present ⇒ the tool must declare both [roles.on-chain] and
[roles.off-chain], else RegistryError::FullstackRolesMissing. The fullstack component still
honors the external interface contract — its build writes blueprint/plutus.json and it reads/writes
.env — so it composes with the other roles like any on-chain producer (§7). Fullstack privatizes
only the internal on-chain↔off-chain seam. (Half-composition still works: scalus on-chain + a
different off-chain tool, or vice-versa, each uses the corresponding [roles.*] template and the
normal blueprint-file seam.)
Tools filling the infrastructure role additionally require an [infra] table
(validated at load: [roles.infrastructure] present ⇒ [infra] required, else
RegistryError::InfraConfigMissing). It declares the cardano-up package and the
output→.env-key mappings the aggregated driver writes:
[roles.infrastructure]
template = "_infra/cardano-up" # the shared driver template (all infra tools use this)
[infra]
cardano_up_package = "kupo" # package id passed to `cardano-up install`
env = [{ from = "KUPO_URL", to = "INDEXER_URL" }] # cardano-up output → contract .env key
A tool may also declare an optional [compat] table describing the connection seam(s) it
speaks or serves, driving the off-chain ↔ provider compatibility gate (§3.2.2). All fields default
empty/false, so a tool without [compat] imposes no constraint.
[compat]
consumes = ["blockfrost", "kupo", "ogmios"] # off-chain: seam(s) it can consume (e.g. Evolution)
serves = ["blockfrost"] # devnet/infra: seam(s) it exposes (e.g. Yaci, Dingo)
self_contained_devnet = true # bundles its own devnet → needs no devnet role (e.g. Tx3)
Seam vocabulary (Seam enum, closed like Role; unknown value → RegistryError::UnknownSeam at
load): blockfrost, u5c, trp, ogmios, kupo.
system_deps is per-tool, flat (§9.1): it applies whenever the tool is selected for any role.
#![allow(unused)]
fn main() {
RoleConfig { template }
EnvMapping { from, to }
InfraConfig { cardano_up_package, env: Vec<EnvMapping> }
CompatConfig { consumes: Vec<Seam>, serves: Vec<Seam>, self_contained_devnet: bool }
ToolDef { id, name, description, website, languages, nix_packages, nix_self_contained: bool, detect, roles: HashMap<Role, RoleConfig>, infra: Option<InfraConfig>, fullstack: Option<RoleConfig>, experimental: bool, compat: CompatConfig }
}
3.2.1 Experimental tools
experimental = true marks a tool as not production-ready, for either — or both — of two
reasons:
- the upstream tool itself is experimental (pre-release, unstable, a work in progress — e.g. Blaster);
- its cardano-init integration is not yet build-green (a placeholder template, excluded from the build-green guarantees / smoke matrix).
Either reason is sufficient; the flag does not distinguish them, because the consequence for the user
is the same (rough edges, breaking changes). The tool still generates (so its role is present and
demoable). Default false, so a production-ready tool needs no flag. Maturity is a per-tool
property, not per-role: a future production-ready formal-methods tool would set experimental = false
even though today’s Blaster is true (ROADMAP Phase 0 formal-methods deliverable).
Experimental status is both surfaced and gated:
- Surfaced everywhere:
list/--helpadd an[experimental]tag and aStatus:line, the interactive picker tags the choice, and generation prints a prominent warning (in the pre-generation summary and on success). In JSON,list’stools[].experimentaland each generatedcomponents[].experimentalcarry the flag for agents (both additive; noschema_versionbump). - Gated by explicit opt-in, so a tool with rough edges is never scaffolded unknowingly. It does
not gate the tool out of existence — the role still generates for the all-five-roles demo — it
gates unacknowledged use:
- One-shot / JSON (non-interactive): selecting an experimental tool without
--allow-experimentalis the usage errorexperimental_not_allowed(§2.5), exit 2 — nothing is generated. A flag, not a prompt, so non-interactive mode stays non-interactive. - Interactive: choosing an experimental tool triggers an explicit confirm (default No);
declining drops just that tool and keeps the rest.
--allow-experimentalpre-acknowledges and skips the confirm.
- One-shot / JSON (non-interactive): selecting an experimental tool without
3.2.2 Off-chain ↔ provider compatibility
An off-chain tool reaches a chain over one or more seams (consumes); its providers — the
selected devnet plus every selected infra tool — each expose seams (serves). The gate
(registry::compat::check, pure) evaluates the single off-chain tool against the union of its
selected providers:
- Compatible when at least one selected provider (that declares a seam) serves a seam the off-chain tool consumes. So a compatible devnet still covers an otherwise-mismatched extra infra tool, and mixing providers is fine as long as one fits.
self_contained_devnet(e.g. Tx3, which bundles its own Dolos): a separately selected devnet is redundant → incompatible, regardless of seams. Infra is unaffected (it can still serve the tool).- Permissive when the off-chain tool declares no
consumes, or no selected provider declares aserves— supplementary infra (a bare node, a submit API) and un-annotated tools never fabricate a conflict, and an off-chain tool with no local provider falls back to a public one (§7). A seam is declared only when a tool can actually build the reference example over it — e.g. Dolos servesu5c, notblockfrost, because its minibf omits tx evaluation.
Enforcement mirrors the experimental gate (§3.2.1):
- One-shot / JSON: an incompatible selection is the usage error
incompatible_tools(§2.5), exit 2 — nothing is generated — unless--ignore-warningis passed, which downgrades it to a warning and proceeds. - Interactive: an incompatible devnet option is shown as
✗ unavailable — <reason>and can’t be picked (unless--ignore-warning). Infra is multi-select with union semantics, so individual infra picks aren’t disabled; the run-level gate covers them.
Load-time validation (registry/loader.rs), all fatal:
- unparseable TOML →
RegistryError::Parse { file }. - unknown role key →
RegistryError::UnknownRole { file, role }. - unknown seam in
[compat]→RegistryError::UnknownSeam { file, seam }. - duplicate
tool.id→RegistryError::DuplicateId { id }. [fullstack]without both on-chain and off-chain roles →RegistryError::FullstackRolesMissing { id }.- zero tools discovered →
RegistryError::Empty.
3.3 Selection
#![allow(unused)]
fn main() {
struct Selection { project_name: String, assignments: Vec<RoleAssignment>, network: Network, nix: bool, spec: bool }
struct RoleAssignment { role: Role, tool_id: String }
enum Network { Preview, Preprod, Mainnet } // Display = lowercase. Not a scaffold-time
// choice: always Preview. Switch via CARDANO_NETWORK in .env.
}
A Selection is valid by construction (ARCHITECTURE §3.3); there is no separate validation pass. Edges in §3.5/§12.
3.4 Role multiplicity & infra duplicates
- Non-infra roles: at most one tool (interactive allows one; one-shot flags are single).
- Infrastructure: ≥1 tools. Duplicate
--infra X --infra Xis de-duplicated (keep first occurrence) so the plan can’t emitinfra/X/twice. (Dedupe, not error: idempotent and harmless.) - Fullstack collapse: when the same
tool_idfills both on-chain and off-chain and that tool declares a[fullstack]template, the two assignments collapse into a singleprotocol/component at planning time (§6.1). TheSelectionstill carries bothRoleAssignments (the collapse is derived, keepingSelectionvalid-by-construction and the blueprint predicate unchanged, §6.2). Same tool for both roles with no[fullstack]template falls back to two separate folders (today’s behavior).
3.5 Project-name rules
Validated by oneshot::validate_project_name (also applied to interactive input):
- Non-empty.
- Must not start with
.. - Characters limited to
[A-Za-z0-9_-]. This rejects path separators, spaces, leading-dot/hidden,./...
Violations → invalid_project_name { name, reason }. (No length cap or OS-reserved-name check in v1; revisit if needed.)
4. Template system
4.1 Manifest schema
templates/<tool>/<role>/manifest.toml:
[manifest]
summary = "…" # shown in interactive mode when this template is highlighted
[[files]]
source = "Justfile.jinja" # path within the template dir
dest = "Justfile" # path within the role dir (see §4.4)
[[files]]
source = "flake.nix.jinja" # optional emission guard:
dest = "flake.nix" # emit only when the condition holds
when = "nix" # (currently the sole condition: `--nix`)
source+destrequired per file;whenoptional (absent ⇒ always emitted).- If file ends with
.jinja, it’s rendered (§4.2). _base/and_nix/layers are emitted by the planner directly (not via a manifest).whengates emission on the selection:when = "nix"emits the file only under--nix. A tool that ships its own component-local Nix flake this way should also setnix_self_contained = true. Then itsnix_packagesare not listed as bare attrs in the top-level shell (a plainmkShellcannot build them); instead the top-level flake references the component as apath:./<dir>input and composes its dev shell viainputsFrom, so the whole toolchain — andjust buildfor that component — works from the project root. Plinth uses this to ship the recommended haskell.nix flake fromIntersectMBO/plinth-template.
4.2 Render derivation (the .jinja rule)
A file is rendered through MiniJinja if its source ends with .jinja. The planner records this as FileEntry.render = source.ends_with(".jinja"). Authoring contract:
- Name a file
foo.ext.jinja→ it is rendered; setdest = "foo.ext"(drop.jinja). - Name it
foo.ext→ copied verbatim (bytes), may be binary. - Rendered templates must be valid UTF-8 (enforced; non-UTF-8
.jinjais a bug). Binary assets must therefore not use the.jinjasuffix.
4.3 Rendering contract
MiniJinja environment (renderer.rs config):
- Undefined = strict: referencing an undefined variable is a render error (caught at generation, not in the generated project). Authors guard optionals with
{% if has_* %}. - Autoescape off: output is code/config, not HTML, so no entity escaping.
- Newlines normalized to
\n(LF), UTF-8, for byte-identical cross-platform output (§11).
4.4 Path safety & destinations
destis resolved relative to the role dir (on-chain/,off-chain/,test/,formal-methods/); for infrastructure, relative toinfra/(the aggregated component — no per-tool subdir, §6.1).destMUST be relative and MUST NOT contain..or a leading/(no escaping the project root). Enforced + tested. (Manifests are first-party today, but the check is cheap insurance and required if templates ever become third-party.)- Base/optional layer dests are fixed (§6).
4.5 Executable bit
The writer sets no executable bits (and just doesn’t need them). Templates must invoke helper scripts through an interpreter (sh scripts/x.sh, node …, just …), never ./x.sh. Rationale: portability (exec bits don’t exist on Windows) + trivial writer. Documented as a template-authoring rule.
4.6 TemplateContext: the template-authoring API
The entire surface available to templates (scaffold/context.rs, Serialize):
#![allow(unused)]
fn main() {
struct TemplateContext {
project_name: String,
network: String, // "preview" | "preprod" | "mainnet"
has_on_chain: bool, has_off_chain: bool, has_infra: bool,
has_devnet: bool, has_formal_methods: bool,
has_fullstack: bool, // one tool fills both on-chain+off-chain (§3.4)
on_chain: Option<RoleContext>,
off_chain: Option<RoleContext>,
fullstack: Option<RoleContext>, // the fused protocol/ component; dir = "protocol".
// When Some, on_chain/off_chain are None (the two roles
// are represented by this single component instead).
infra_tools: Vec<InfraToolContext>, // 0..n, canonical order (§11); aggregated infra component
devnet: Option<RoleContext>,
formal_methods: Option<RoleContext>,
infra_context_name: String, // cardano-up context the infra driver targets (= project_name)
infra_env: Vec<EnvMapping>, // resolved, key-unique .env emissions for infra (proposal §5.4)
blueprint_path: String, // "blueprint/plutus.json" (contract constant)
env_vars: <ordered map>, // see §6.3; iterated in sorted-key order
nix: bool,
nix_packages: Vec<String>, // deduped union across selected tools, first-seen order; excludes nix_self_contained tools (composed via inputsFrom instead)
nix_component_flakes: Vec<String>, // dirs of nix_self_contained components; the top-level flake references each as path:./<dir> and pulls its dev shell in via inputsFrom
spec: bool, // the spec/ folder is generated (selection.spec)
spec_example: bool, // spec/giftcard.md is generated (spec && an on-chain/off-chain role is present)
}
struct RoleContext { tool_id, tool_name, language, dir } // language = tool.languages[0]
struct InfraToolContext { tool_id, tool_name, cardano_up_package, env: Vec<EnvMapping> }
struct EnvMapping { from, to } // cardano-up output var → contract .env key
}
This struct is the contract. Adding a field is additive; renaming/removing is a breaking template-API change.
5. Registry loading
Registry::load() iterates embedded registry/tools/*.toml, builds Vec<ToolDef> plus indexes by_id: HashMap<String, usize> and by_role: HashMap<Role, Vec<usize>>.
Accessors: get(id), tools_for_role(role), all_tools(). Immutable after load.
Determinism note: any consumer that emits tools/roles must sort (§11), since by_role order follows asset-iteration order.
6. Scaffolding pipeline contracts
6.1 Plan order (canonical)
planner::plan emits FileEntrys in exactly this order:
- Base layer (always):
Justfile,README.md,AGENTS.md(rendered),CLAUDE.md(static),.gitignore,.env.AGENTS.mdis the tailored agent brief (stack, interface contract,justworkflow, per-tool doc links, and the relevant cardano-dev-skills for the selection);CLAUDE.mdis a static@AGENTS.mdimport so Claude Code (which does not readAGENTS.mdnatively) picks it up. - Blueprint dir:
blueprint/.gitkeep, if any non-infrastructure role is present (§6.2). Source isTemplateSource::Inline(empty). - Role layers: assignments processed in
Role::ALLorder (not flag order). For each, read the template manifest and append its files (rendered per §4.2). Two roles aggregate instead of emitting per-assignment:- Fullstack collapses: when the same tool fills on-chain + off-chain and declares a
[fullstack]template (§3.4), the two assignments emit oneprotocol/component from the[fullstack]template, emitted once on the first of the pair (on-chain sorts first inRole::ALL), the off-chain assignment is skipped.has_on_chain/has_off_chainare false;has_fullstackis true. Because both assignments remain in the selection, the blueprint predicate (§6.2) is unchanged. - Infrastructure aggregates: all selected infra tools share one driver template (
_infra/cardano-up), emitted once atinfra/on the first infra assignment (the rest are contiguous after the canonical sort and skipped); they are still sorted bytool_idfor the renderedinfra_tools/infra_envorder (§11). All infra tools must resolve to the same template path, elseScaffoldError::InfraTemplateMismatch.
- Fullstack collapses: when the same tool fills on-chain + off-chain and declares a
- Optional layer: if
nix,flake.nix(rendered) +.envrc(rendered from_nix/envrc.jinja;use flake, plus — when the top-level flake composes anix_self_containedcomponent — aNIX_CONFIGpre-accept for itsnixConfigand a source-build heads-up when the IOG cache isn’t trusted). - Spec layer: if
spec(default; off under--no-spec),spec/README.md(rendered from_spec/README.md.jinja), plusspec/giftcard.md, a plain-Markdown specification of the gift-card worked example (actors, assets, validators, transactions, invariants), if an on-chain or off-chain role is present (the only components that implement it).spec/is human-readable intent, never executed. It is distinct from a formal-methods tool’s executable specs (e.g. Blaster’sformal-methods/spec/).
--dry-run returns this FilePlan (no rendering, no I/O).
6.2 blueprint/ predicate
blueprint_present ⇔ assignments.iter().any(|a| a.role != Role::Infrastructure)
The directory (via .gitkeep) exists for every project except infrastructure-only; the plutus.json file is produced by on-chain build and may be absent, so consumers must tolerate a missing file (§7). A fullstack project keeps the directory: the collapse is derived and both on-chain/off-chain assignments stay in the selection, so the predicate holds and the protocol/ component’s build writes the blueprint like any on-chain producer.
6.3 .env seeding
.env is always written (base layer), seeded by context.rs with:
CARDANO_NETWORK=<network>, and empty INDEXER_URL=, INDEXER_PORT=, NODE_SOCKET_PATH=, OGMIOS_URL=, TX_SUBMIT_URL=, DOLOS_GRPC_URL=, CARDANO_NODE_API_URL=. Whichever component provisions a local endpoint fills the connection vars at runtime during its dev (§7).
Emitted in sorted key order for determinism.
6.4 Write semantics & target dir
- Write is the only phase with side effects:
create_dir_all(parent)then write bytes. No chmod (§4.5). - Target directory policy: generate into
./<project_name>/. If it does not exist, create it. If it exists and is empty, proceed. If it exists and is non-empty, fail withdir_exists(exit 1). No--forcein v1. Never overwrites user files.
7. Interface contract (concrete)
Constants (contract.rs):
BLUEPRINT_PATH = "blueprint/plutus.json";- dirs
on-chain|off-chain|infra|devnet|formal-methods(plus the derivedprotocolfullstack dir); - env
INDEXER_URL,INDEXER_PORT,NODE_SOCKET_PATH,CARDANO_NETWORK, and the provider endpointsOGMIOS_URL,TX_SUBMIT_URL,DOLOS_GRPC_URL,CARDANO_NODE_API_URL.
Every component Justfile exposes build, test, clean and works standalone (its just build succeeds with no other roles present). A target that is a no-op for a tool still exists (may print a message). dev is optional: a component provides it only when it has a genuine watch/daemon/devnet mode — there are no no-op dev targets (it is not aggregated at the top level, §7.2, so an absent dev costs nothing).
- On-chain
buildwrites../blueprint/plutus.json. - Fullstack (
protocol/)buildalso writes../blueprint/plutus.json(it is the on-chain producer for its project) and reads/writes../.envlike an off-chain consumer. Its internal on-chain↔off-chain link may bypass the file (shared in-process types), but the external seam is mandatory so devnet/formal/infra still compose. - Off-chain / devnet / formal-methods read
../blueprint/plutus.jsonand../.envif present; degrade gracefully if absent. - The component that provisions a local chain endpoint writes the standard connection vars (
INDEXER_URL, …) into../.envduring itsdev. This is role-agnostic: it is typically an infrastructure service, but a local devnet such as Yaci DevKit in the devnet role does it too. The seam is the.envkeys, not the role — a tool’s role is its purpose, while writing.envis the orthogonal capability of exposing a local endpoint. Consumers react only to the presence ofINDEXER_URL, never to which tool/role set it (this is what keeps composition O(tools), ARCHITECTURE §1).
7.1 Top-level Justfile aggregation
The top level aggregates only the tasks that terminate and compose:
build: each present component’sbuild, on-chain first (so the blueprint exists for consumers), then off-chain/devnet/formal inRole::ALLorder. A fullstackprotocol/component takes the on-chain producer slot (built first).test: on-chainbuildfirst (produce the blueprint), then each present component’stestinRole::ALLorder — on-chain, off-chain, devnet, and formal-methods (verify). For fullstack,protocolbuilds then tests first.clean: each component’sclean, thenrm -f blueprint/plutus.json.
7.2 No top-level dev
There is no top-level dev target. Long-running / interactive tasks (watch modes, local devnets, REPLs) do not aggregate into one foreground command — that is exactly why a multi-service launcher is awkward — so they are per-component: the developer runs just -f <role>/Justfile dev (or just -f infra/Justfile dev for the aggregated infra stack) directly, documented in the README.
dev is optional per component (§7): a tool provides it only when it has a genuine watch/daemon/devnet mode. Because the top level never aggregates dev, a component without one costs nothing — and we don’t ship no-op dev targets just to fill the slot.
A component whose dev provisions a local endpoint (e.g. Yaci DevKit’s devnet) writes the standard connection vars into ../.env as part of that per-component dev; off-chain/devnet consumers then pick them up automatically (§7). Bringing such a service up is therefore a deliberate, per-component developer action, not a top-level orchestration step.
8. list subcommand schema
cardano-init list (human default) / cardano-init list --format json:
{ "schema_version": 1, "ok": true, "data": {
"roles": [
{ "id": "on-chain", "dir": "on-chain", "display": "On-chain", "multiple": false },
{ "id": "infrastructure", "dir": "infra", "display": "Infrastructure", "multiple": true }
/* … all Role::ALL, in canonical order … */
],
"tools": [
{ "id": "aiken", "name": "Aiken", "description": "…", "website": "https://…",
"community": [ { "name": "Discord (PRAGMA)", "url": "https://discord.gg/…" } ],
"languages": ["aiken"], "roles": ["on-chain"], "fullstack": false, "experimental": false,
"support": [ { "name": "VS Code extension", "url": "https://marketplace.visualstudio.com/…" } ] },
{ "id": "blaster", "name": "Blaster", "description": "…", "website": "https://…", "community": [],
"languages": ["blaster-spec"], "roles": ["formal-methods"], "fullstack": false, "experimental": true, "support": [] }
/* … tools sorted by id; each tool's roles sorted … */
]
}}
list renders from a shared model (registry::view: role_views() / tool_views()). The human output has two forms: the default (a Roles table plus a per-tool block), and a compact --table view — a full-grid matrix with one column per role (in Role::ALL order), each role’s tools stacked and experimental ones tagged 🧪. --table is a human-presentation flag only; it has no effect under --format json (the JSON already carries the same data). roles[].multiple is true only for infrastructure (Role::multiple). tools[].fullstack is true when the tool declares a [fullstack] template (i.e. --fullstack <tool> is valid); it is an additive field (no schema_version bump). fullstack is a capability, not a role — it never appears in tools[].roles or in the roles array. tools[].experimental is true for tools that are unstable and/or not yet build-green (§3.2.1); selecting one needs --allow-experimental — also additive. tools[].community lists the tool’s official help channels (Discord, GitHub Discussions, …) as { name, url }, from the optional community field in the tool’s TOML; empty when none (additive). The default human list prints them as a Community: line in each tool block; the standalone doctor shows these for each detected component under a “Get help” heading. tools[].support lists the tool’s supporting tools (editor extensions, language servers, playgrounds): the support links of every dep in its system_deps (§9), deduplicated by URL; empty when none (additive). The human list prints them as a Tooling: line.
9. Dependency doctor
Implemented for DX.02: the standalone cardano-init doctor command plus check-and-advise after generation. Both run the same pure resolver (§9.4) over the same catalog (§9.2).
Scope — what the doctor is. A dependency checker and advisor: it determines which required dependencies are present and, for missing ones, prints an OS-aware, ordered install plan (never just “go install it”). v1 prints the plan; v2 (DX.05) executes it with consent (same data, same resolver).
Out of scope — what the doctor is not.
- A project validator. The doctor does not verify that a component actually builds, type-checks, or is otherwise viable — that is the job of each component’s
just build/just test(the interface contract requires those to work standalone, §7). A directory that looks like a tool (matching signatures, §9.6) but isn’t wired up correctly will still pass the doctor; it will failjust test. Keeping these separate avoids duplicating the build system’s job in a fragile heuristic. - Alternative runtimes / package managers. Each template fixes its toolchain (e.g. the MeshJS/Yaci components invoke
npm/node/npxdirectly in their Justfiles), sonodeis the honest required dep. The doctor reflects what the template needs; it does not offer to satisfy that withbun/deno/etc. Making a component runtime-agnostic is a template decision, not a doctor feature (and cuts against the fixed-convention philosophy, ARCHITECTURE §3.1). - Version constraints. Presence only in v1 (§9.3); minimum-version checks are a later item (ROADMAP Phase 3).
9.1 Dependency sets (required vs recommended)
Pure functions of the selection. Two tiers:
required_deps = {"just"} // universal task runner
∪ (tool.system_deps for each selected tool) // unioned, deduped
recommended_deps = {} // see note
- Required deps gate the build/test acceptance bar (SM-1); their absence is reported prominently.
justis a base/derived required dep (every project needs the task runner). - Recommended deps improve the experience but are never required (soft notes only). The two-tier mechanism remains for future use, but there is currently no recommended dep: the former
process-composecase existed only to smooth a multi-service top-leveljust dev, and the top level no longer aggregatesdev(§7.2), so that rationale is gone. Long-running services are now started per-component.
just is a base/derived dep owned by no tool; it has an entry in registry/deps.toml like any dep. (cardano-up is reached as an installer and is itself a dep entry, rather than added to either set directly.)
9.2 Catalog = installers (code) + recipes (data)
The catalog is a small graph. An installer is itself a kind of dependency, so the two node types are: code-defined installers, and data-defined dep recipes that reference them.
Installers (code, installers.rs): a closed vocabulary. Per installer: the binaries that mean “available”, a command template, and a bootstrap list of dep ids (empty ⇒ terminal, i.e. detect-only/never auto-installed; non-empty ⇒ bootstrappable by installing any one of those deps, tried in order):
#![allow(unused)]
fn main() {
enum Installer { Brew, Apt, Dnf, Pacman, Winget, Nix, Go, Cargo, Npm, Aikup, CardanoUp, Curl, PowerShell }
struct InstallerDef {
detect: &[&str], // ["npm"]: installer available if one is on PATH
template: fn(arg: &str) -> String, // Brew → "brew install {arg}"; Curl → "curl -sSfL {arg} | sh"
bootstrap: &[&str], // dep ids that provide this installer; [] ⇒ terminal
}
}
| Installer | template ({arg}) | bootstrap |
|---|---|---|
Brew | brew install {arg} | [] (terminal) |
Apt | sudo apt install -y {arg} | [] |
Dnf/Pacman/Winget | native install of {arg} | [] |
Nix | nix profile install nixpkgs#{arg} | [] |
Curl | curl -sSfL {arg} | sh | [] |
PowerShell | powershell -c "irm {arg} | iex" | [] |
Npm | npm install -g {arg} | ["node"] |
Cargo | cargo install {arg} | ["rustup", "rust"] |
Go | go install {arg} | ["go"] |
Aikup | aikup install {arg} | ["aikup"] |
CardanoUp | cardano-up install {arg} | ["cardano-up"] |
Tx3up | tx3up install {arg} | ["tx3up"] |
The arg’s meaning is the installer’s: a package name for managers, an installer-script URL for Curl/PowerShell, a target for Aikup/CardanoUp. Adding an installer is a deliberate code change, only when a real recipe needs it (same discipline as roles).
Recipes (data, registry/deps.toml): keyed by dep id; install is an ordered list of single-key { installer = arg } methods (order = preference). Installer keys are validated against the Installer enum at load (unknown → load error):
[node]
binaries=["node"]
docs="https://nodejs.org/en/download"
install=[ {brew="node"}, {apt="nodejs"}, {winget="OpenJS.NodeJS"}, {nix="nodejs"} ]
[aikup]
binaries=["aikup"]
docs="https://aiken-lang.org/installation-instructions"
install=[ {npm="@aiken-lang/aikup"}, {curl="https://install.aiken-lang.org"}, {powershell="https://windows.aiken-lang.org"} ]
[aiken]
binaries=["aiken"]
docs="https://aiken-lang.org/installation-instructions"
install=[ {aikup=""}, {nix="aiken"} ]
support=[ {name="VS Code extension", url="https://marketplace.visualstudio.com/items?itemName=TxPipe.aiken"}, {name="Neovim plugin", url="https://github.com/aiken-lang/editor-integration-nvim"} ]
[just]
binaries=["just"]
docs="https://just.systems"
install=[ {brew="just"}, {apt="just"}, {cargo="just"}, {nix="just"} ]
[process-compose]
binaries=["process-compose"]
docs="https://f1bonacc1.github.io/process-compose/"
install=[ {brew="process-compose"}, {go="github.com/f1bonacc1/process-compose@latest"}, {nix="process-compose"} ]
#![allow(unused)]
fn main() {
struct DepRecipe { binaries: Vec<String>, docs: String, install: Vec<(Installer, String)>, support: Vec<SupportLink> } // ordered
struct SupportLink { name: String, url: String } // optional `support` list; defaults to empty
type DepCatalog = HashMap<String, DepRecipe>; // dep id → recipe (loaded from registry/deps.toml)
}
Installers (logic, closed vocab) are code; recipes (which installer + arg per dep) are data, so a new tool whose deps install via existing installers is pure data (ARCHITECTURE §8.1). Shared deps (node, jvm) are defined once and referenced by many tools’ system_deps.
9.3 Environment (impure probe)
#![allow(unused)]
fn main() {
struct Environment { os: Os, installers: HashSet<Installer> /* detected present */ }
enum Os { Linux, MacOs, Windows, Other }
}
probe.rs detects the OS and which installers are present (installer available if one of its detect binaries is on PATH). A dep is present if one of its binaries is on PATH. No execution, no version (v1).
9.4 Resolver (pure, recursive) & Report
resolve(dep_id, env, catalog, seen) -> Plan | Unresolved:
if dep_id ∈ seen: return Unresolved // cycle guard
rec = catalog[dep_id]
if any(rec.binaries on PATH): return Plan([]) // already present
// Pass 1 (preferred): the first method whose installer is usable right now.
for (installer, arg) in rec.install: // ordered preference
if installer ∈ env.installers: // usable right now
return Plan([ {installer, installer.template(arg)} ])
// Pass 2: no installer is directly available — bootstrap one, in order.
for (installer, arg) in rec.install:
for bdep in installer.bootstrap: // [] ⇒ skip (terminal)
sub = resolve(bdep, env, catalog, seen ∪ {dep_id})
if sub is Plan: return Plan(sub.steps + [ {installer, installer.template(arg)} ])
return Unresolved // → docs fallback
all_required_present = every required dep resolves to Plan([]) // i.e. already present
The two passes are what make a directly-usable installer win over bootstrapping an earlier-listed one: this is exactly why the nix path for aiken needs no aikup when nix is present (Pass 1 picks it in one step). When neither nix nor aikup is present, no method’s installer is directly available, so Pass 2 bootstraps the aikup installer (via node/npm, etc.), producing a multi-step plan. A single method is still chosen per dep. (A naive single-pass walk that tried bootstrapping each method before checking later methods’ installers would wrongly bootstrap aikup even when nix is present.)
{ "schema_version": 1, "ok": true, "data": {
"all_required_present": false,
"deps": [
{ "id": "node", "required": true, "present": true },
{ "id": "aiken", "required": true, "present": false,
"plan": [ { "installer": "npm", "command": "npm install -g @aiken-lang/aikup" },
{ "installer": "aikup", "command": "aikup install" } ],
"alternatives": [ { "installer": "nix", "command": "nix profile install nixpkgs#aiken", "available": false } ],
"docs": "https://aiken-lang.org/installation-instructions",
"support": [ { "name": "VS Code extension", "url": "https://marketplace.visualstudio.com/items?itemName=TxPipe.aiken" } ] }
]
}}
(The recommended/soft-note tier carries no members today — see §9.1 — so the example lists only required deps. A recommended dep, if reintroduced, would appear with "required": false and a reason.)
plan= the ordered, possibly multi-step install sequence the resolver produced for this host (empty when present; omitted/empty with onlydocswhen unresolved).alternatives(additive) = the recipe’s other install methods, so the user can pick a different installer. Each is{ installer, command, available }whereavailable= that installer is present on this host now. Ordered available-first, then those needing their installer installed; the method already used byplanis excluded. Omitted when the dep is present or the recipe offers no other method. The presenter lists available ones as plain commands and tags the restrequires <installer>.requireddistinguishes tiers;all_required_presentignores recommended deps. The presenter shows missing required deps prominently and recommended ones as a soft note withreason.docsis always available so advice is never empty (FR-20).support(additive) = the recipe’s supporting tools (editor extensions, language servers, inspectors) as{ name, url }. Unlikedocs, it is reported whether or not the dep is present (editor tooling matters after install too); omitted when the recipe declares none. It’s advice only: never installed or probed. The standalonedoctorlists it under a “Supporting tools” heading, andlistshows it per tool (§8); generation-time advice does not.- Doctor output is host-dependent by design (it reflects detected installers) and is not part of the byte-identical generation contract (§11). v1 prints the plan; v2 executes it (same data, same resolver).
9.5 Referential integrity (tests)
- Every
system_depsid (plus the base depjust) has aregistry/deps.tomlentry. - Every installer named in any recipe is an
Installerenum variant (also enforced at load). - Every dep id in any installer’s
bootstraplist has a recipe entry. - The dep graph resolves without infinite recursion (the resolver’s cycle guard is exercised by a test).
9.6 Project scan & tool detection (standalone doctor)
The standalone cardano-init doctor takes no flags describing the project: it derives the dependency set by scanning the current directory. There is no generated metadata file — the project’s structure is the source of truth (probe::scan_project, impure):
- For each role in
Role::ALL, look for its contract directory (contract::DIR_*:on-chain/,off-chain/,infra/,devnet/,formal-methods/). - For a present directory, the candidate tools are exactly those that declare that role (so
on-chain/is tested only against on-chain tools — this resolves the on-chain/off-chain ambiguity without per-pair logic). - A tool matches if any of its
detectsignatures matches. Exactly one match ⇒ the component is identified. On an ambiguous multiple, a definitive match wins: a bare-path (existence-only) signature is a tool-unique manifest (e.g.trix.toml), whereas acontainsneedle only proves a shared file mentions the tool (e.g. a Tx3 project’spackage.jsonpulls in@meshsdkas a library, tripping MeshJS’s needle). If exactly one candidate matched via a bare-path signature, that tool is identified; otherwise (zero matches, or still-ambiguous after this tiebreak) the directory is reported as unrecognized (renamed, modified, or a foreign project). A renamed directory simply isn’t found, so that role is absent. - The required set is
{just}∪ thesystem_depsof every identified tool (§9.1), fed to the resolver (§9.4).
No project. If the scan finds no components and no unrecognized dirs, the doctor reports “No Cardano project detected” (JSON: project_detected: false) with a hint to scaffold one, and omits the DEPS OK pill. That pill would only vouch for just. A missing just is still advised. With no required set, the environment panel’s tooling row (JSON: environment.tooling) instead lists all installed Cardano tooling (doctor::present_tooling): {just} ∪ every registered tool’s system_deps that is present on PATH. Inside a project, tooling lists only the project’s required deps that are present.
Infrastructure is the exception. The aggregated infra/ component has no per-tool subdirs (it’s the single cardano-up driver), so it is not matched against per-tool detect signatures. Instead the scan recognizes it by a driver marker — infra/Justfile referencing cardano-up — and reports a synthetic cardano-up component (doctor::INFRA_DRIVER_ID). Its contribution to the required set is the union of all registered infra tools’ system_deps ({docker, cardano-up}), data-driven from the registry. So infra tools carry detect = [].
Fullstack protocol/ is scanned like a normal component. protocol/ is not a Role::ALL directory, so it is handled by a dedicated branch: if protocol/ exists, it is matched against the detect signatures of the tools that declare a [fullstack] template (the same per-tool signatures used for the role dirs — a fullstack tool’s signatures must therefore be present in its fullstack template). Exactly one match ⇒ that component is identified; zero/ambiguous ⇒ protocol/ is unrecognized. Unlike infra, the identified tool is a real registry tool, so its system_deps feed the required set via the normal registry.get path (no synthetic id).
Detect signatures (detect in registry/tools/<tool>.toml). A list; each entry is either:
- a bare path (relative to the role dir) — matches if the file exists; or
- a table
{ file = "<path>", contains = "<substring>" }— matches if the file exists and its text contains the substring.
# Distinctive filenames need only existence:
detect = ["aiken.toml"]
# Generic filenames need content to avoid false positives:
detect = [{ file = "package.json", contains = "@meshsdk" }]
The contains form is what keeps detection honest without overreaching (per the scope note above): a from-scratch JS project (e.g. Next.js) has a package.json, but without @meshsdk it is not identified as MeshJS — it falls into the “unrecognized” bucket. This sharpens the label and the structure check; it does not attempt to prove the component is viable (still just test’s job). Signatures are tool-author data (no Rust), consistent with the registry’s extensibility promise.
10. Version-update check
Goal: surface “a newer cardano-init is available” before generation, so the user can update and regenerate with newer templates rather than discovering it post-write (and deleting/regenerating). Constraints: never block agents/CI, never alter generated output, bounded latency, offline-safe. Implemented in src/cli/version_check/ (a thin cli/ concern).
-
Scope. The generating commands:
init(interactive + one-shot, incl.--dry-run) andadd/remove. -
Rule. Each run compares this binary’s version (
CARGO_PKG_VERSION) with the latest GitHub release; a newer release → notice. -
Cached once/day.
update-check(JSON{ checked_at, latest }) under the OS cache dir:~/Library/Caches/cardano-init/(macOS),$XDG_CACHE_HOME|~/.cache/cardano-init/(Linux),%LOCALAPPDATA%\cardano-init\(Windows). Checked within 24h → cached result, zero network, zero latency; also keeps us far below GitHub’s unauthenticated rate limit (60 req/hour/IP). -
Gating. Runs only when stdout is a TTY and the format is
human. Disabled entirely whenCARDANO_INIT_NO_UPDATE_CHECKorCIis set.--format jsonand non-TTY runs never touch the network and report no update info. -
Surfaced before the write phase; latency hidden where possible:
- Interactive: the check fires on a background thread at process start and completes during tool selection; the notice (if any) shows before generation with no added latency.
- Human one-shot: the check is joined with a ≤1s deadline (measured from process start) behind a
Checking for updates…spinner before writing; on hit → notice then generate; on timeout/offline → proceed (worst case +1s, once/day).
-
Install-aware update command. The notice prints the command matching how the running binary was installed (from
current_exe), first match wins:Signal Method Command exe path has a _npxcomponentnpx npx cardano-init@latestexe path has a node_modulescomponentnpm npm install -g cardano-init@latestexe under /nix/storenix nix profile upgrade cardano-initcargo-dist receipt ( $XDG_CONFIG_HOME|~/.config/cardano-init/cardano-init-receipt.json; Windows%LOCALAPPDATA%\cardano-init\) whoseinstall_prefixcontains the exeshell / PowerShell installer the README installer one-liner cardano-initrecorded in$CARGO_HOME/.crates2.jsonand exe in$CARGO_HOME/bincargo cargo install --git https://github.com/input-output-hk/cardano-init --forceotherwise unknown none: the notice shows the releases link instead The receipt is checked before cargo because both installers place the binary in
$CARGO_HOME/bin. -
Banner. A yellow box (Flutter-style): “A new version of cardano-init is available! (current → latest)”, then the update command (or the releases link). When the box would wrap on a narrow terminal, the same rows print unframed so the command stays copy-pasteable.
-
Informational, not a gate. After the notice, generation continues with the current version (the user may Ctrl-C to update first). It never blocks beyond the deadline and never alters generated output (determinism, A-3).
-
Fail-silent. Best-effort GET of
releases/latestfrom the GitHub API (ureq, rustls); offline/timeout/parse error → no-op. Only plainmajor.minor.patchtags count; pre-releases are never offered. -
--dry-runwrites nothing, so the delete/regenerate concern doesn’t apply; the notice still shows (same gating).
11. Determinism & reproducibility
Identical (binary, Selection) ⇒ byte-identical tree. Rules:
- Plan order is fixed (§6.1): base → blueprint → roles in
Role::ALLorder → optional. - Assignments are reordered into
Role::ALLorder for emission (user/flag order does not affect output). - Infrastructure tools sorted by
tool_id. - Maps emitted in sorted-key order:
env_varsand anyHashMapreaching output use a sorted/canonical view (spec: backenv_varswithBTreeMapor sort at the boundary). nix_packages: dedup preserving first-seen order across assignments (already so).- Newlines LF, UTF-8, single trailing newline; no timestamps, no absolute paths, no host-dependent content in generated files.
- Snapshot tests over
--dry-runand rendered output for a fixed set of selections guard all of the above.
Implementation note: today
rolesis aHashMapandassignmentskeep flag order; realizing rules 2–4 (andenv_varsordering) is tracked work.
12. Edge-case matrix
| Situation | Behavior | Code / exit |
|---|---|---|
One-shot flags without --name | error | name_required / 2 |
--name invalid (empty, .-lead, space, /) | error | invalid_project_name / 2 |
| Unknown tool id | error, list valid tools for role | unknown_tool / 2 |
| Tool doesn’t fill the role | error, list tool’s valid roles | tool_role_mismatch / 2 |
| No roles selected | error | no_roles_selected / 2 |
--infra X --infra X | de-duplicated (keep first) | ok |
--fullstack X (X has [fullstack]) | one protocol/ component | ok |
--on-chain X --off-chain X (X has [fullstack]) | collapses to one protocol/ component | ok |
--on-chain X --off-chain X (X has no [fullstack]) | two separate folders (fallback) | ok |
--fullstack X (X has no [fullstack]) | error, list fullstack-capable tools | fullstack_unsupported / 2 |
--fullstack X --on-chain Y (or --off-chain Y) | error | fullstack_conflict / 2 |
| Infra-only selection | no blueprint/ dir | ok |
| Target dir absent | created | ok |
| Target dir empty | proceed | ok |
| Target dir non-empty | refuse | dir_exists / 1 |
--dry-run | print plan, write nothing | ok / 0 |
| Interactive: user declines confirm | abort, no write | / 0 |
| Registry empty / dup id / unknown role (build-time data) | fail load | registry_load / 1 |
| Manifest missing/malformed, asset missing, render fails | fail | scaffold_error / 1 |
json mode but interactive input needed | error, never prompt | usage / 2 |
13. Non-functional
- Language/edition: Rust 2024 edition (the code uses let-chains and
&[Role]consts). MSRV pinned inCargo.toml/CI to the stable that supports those (≥1.88). - Dependencies (current):
clap,dialoguer,minijinja,serde,serde_json,toml,rust-embed,console,indicatif,comfy-table,miette,thiserror;libc(Unix-only, SIGPIPE reset);tempfile(dev). Planned additions: a minimal HTTPS client for §10 (e.g.ureq), kept off the generation path. The generated project always depends onjust, plus thesystem_depsof whichever tools were selected. - Distribution: single statically-linked binary; generation works fully offline.
- Platforms: Linux, macOS, Windows. Exec-bit-free output (§4.5) and LF normalization (§11) keep behavior identical across them.
14. Open technical decisions
None currently open. (OD-1, the hosted-web strategy, is closed: the web front-end has been dropped — the CLI is the only surface.)
15. In-place project updates (add / remove)
cardano-init add/remove edit an already-generated project’s role/tool composition in the current directory: they add, remove, or replace whole component folders and re-wire the shared top-level files. This is deliberately not version management (PRD §5.2): it never pins, upgrades, or migrates the tooling itself, and it never rewrites the user’s code inside a component it keeps. The change is expressed as a mutation of the project’s Selection, then applied under a git safety net.
Modules (purity invariant intact, ARCHITECTURE §2): reconstruction is the impure edge in doctor::probe::reconstruct (reads the tree); the change-set is pure logic in scaffold::update; the disk side effect is scaffold::writer::apply_update; the CLI orchestration is cli::update. The validation gates are the same pure functions init uses (registry::compat::check, the experimental predicate), run on the resulting selection.
15.1 The model: components vs the shared layer
Every generated project is exactly two things, and the update engine treats them differently.
Component slots — one directory per contract role, plus the fused protocol/ slot (contract::DIR_*):
| Slot | Dir | Multiplicity | On change |
|---|---|---|---|
| on-chain | on-chain/ | one tool | replace dir |
| off-chain | off-chain/ | one tool | replace dir |
| infrastructure | infra/ | many tools, one aggregated dir | re-render in place |
| devnet | devnet/ | one tool | replace dir |
| formal-methods | formal-methods/ | one tool | replace dir |
| protocol (fused) | protocol/ | one fullstack tool | replace dir |
A component is self-contained and standalone (§7): its rendered content does not depend on which sibling roles are present. This is the load-bearing guarantee — a slot whose tool is unchanged is never touched (§15.4).
Shared layer — top-level files derived from the whole selection, re-rendered on any change: Justfile, .env, README.md, AGENTS.md, CLAUDE.md, .gitignore, and (under --nix) flake.nix / .envrc, plus the blueprint/.gitkeep marker.
spec/ is user-owned — neither a slot nor shared layer. Once generated it is the user’s design doc, so updates never rewrite or delete it.
The update is: reconstruct → confirm → mutate → validate → diff the slots → re-render the shared layer → write under a git safety net.
15.2 Reconstructing the current Selection (detection, not a manifest)
An update needs the project’s current Selection as its base state. Rather than persist a manifest at scaffold time, it reconstructs by detection — the same choice doctor makes (§9.6): the project’s structure is the source of truth, so there is no second source that can silently drift from the tree. Every field of Selection is recoverable, and the one historically “lossy” field — the infra provider set — is present verbatim in infra/Justfile.
reconstruct(root, registry) extends probe::scan_project and yields a best-effort selection plus the items it could not identify:
reconstruct(root, registry) -> Reconstructed {
selection: Selection, // best-effort
unrecognized: Vec<UnrecognizedDir>,
low_confidence: Vec<Field>, // fields we had to guess (e.g. network, infra)
unknown_infra: Vec<String>, // cardano-up packages not in the registry
}
- Roles & tools — directly from
scan_project. Each detected component becomes aRoleAssignment(a detectedprotocol/becomes the two assignments{on-chain→T, off-chain→T}the planner re-collapses). - Infrastructure providers — recovered by parsing
infra/Justfileforcardano-up install <package>lines, mapping each<package>back to a tool viaToolDef.infra.cardano_up_package. Unknown packages are surfaced, not silently dropped. - network — parsed from
CARDANO_NETWORK=in.env. nix —flake.nix/.envrcpresent. spec —spec/directory present. project_name — the root directory name (only ever used to render text; a wrong guess is git-recoverable).
Any field not cleanly recoverable is marked low_confidence.
15.3 Confirm — the trust boundary
Reconstruction is never trusted silently; this is what neutralizes detection’s one real risk (recovery logic coupled to generated template text). Before any mutation:
- Interactive — the reconstructed selection and any
unrecognized/low_confidenceitems are shown, and the user confirms or corrects. On a clean git tree the applied change is fully reviewable/revertible viagit diff, so it is written straight away (no confirm prompt — good agent DevX); the prompt appears only when--forceis overriding a dirty tree, where the change cannot be cleanly separated from existing edits. - Non-interactive /
--format json— no prompts. The reconstruction must be fully recognized: anyunrecognizeddir is a hard error (project_unrecognized, exit 2). The tool never guesses in automation.
15.4 Validate the mutated selection
The mutation produces S_new, run through exactly the same gates as init — no new validation logic:
- Role uniqueness — one tool per non-infra role; infra repeatable and deduped keep-first (§3.4), so “add an infra provider already present” is an idempotent no-op.
- At least one role — removing the last role is refused (
no_roles_selected); a project is never left empty. - Compatibility gate —
registry::compat::check(S_new.assignments, registry)(§3.2.2). The gate runs on the result, not the delta: adding Dolos to an Evolution project, or swapping MeshJS→Tx3 next to a Yaci devnet, must trip the same check a fresh scaffold would. Stops withincompatible_toolsunless--ignore-warning. - Experimental gate — if
S_newnewly includes an experimental tool, require--allow-experimental/ interactive confirm (§3.2.1;experimental_not_allowed).
Because Selection-validity is by construction (ARCHITECTURE §3.3), a validated S_new is indistinguishable from one init would have built.
15.5 The change set
Pure logic over S_old, S_new, and the registry (scaffold::update, beside the planner). For each slot, compare the tool in S_old vs S_new:
| Transition | Action |
|---|---|
| absent → present | CREATE: plan+render that component into its dir. Precondition: the dir must not already exist on disk; if it does (a foreign/unrecognized dir), abort (slot_occupied). |
| present → absent | REMOVE: rm -rf <dir> (removes user-added files in that dir too — intentional; git is the net). |
| present → present, tool changed | REPLACE: REMOVE then CREATE. |
| present → present, tool same (non-infra) | KEEP — never touched. |
| infra: provider set changed (dir stays) | RE-RENDER IN PLACE: overwrite infra/’s managed files (Justfile, README.md, scripts/write-env.sh) from the new provider set. |
| infra → empty | REMOVE infra/. |
Fusion boundary is just rows in this table — no special “migration”:
- two dirs (
on-chain+off-chain) → fullstack tool = REMOVE both + CREATEprotocol/(a full replace; nothing is carried over — the code in the two dirs is deleted, loudly, git-recoverable). protocol/→ separate tools = REMOVEprotocol/+ CREATE the new dir(s). “Drop the off-chain half of a fusedprotocol/” is expressed as replaceprotocol/with an on-chain tool — the user must name that tool; there is no in-place split of a fused codebase.
Shared layer is always recomputed from S_new and written per-file with a content diff — a file is only rewritten if its bytes change (so a slot swap that doesn’t alter, say, .gitignore leaves it alone). blueprint/.gitkeep is added/removed to match the predicate any(role != Infrastructure) (§6.2); flake.nix/.envrc follow the nix flag.
The “unchanged slot is safe” guarantee. Removing off-chain from {on-chain→aiken, off-chain→meshjs}: on-chain is KEEP (same tool), so on-chain/ is never in the write set; the standalone-component contract guarantees aiken’s render doesn’t depend on the removed sibling, so even the content diff shows no change. Only off-chain/ is removed and the shared layer re-wired.
15.6 Safety, write ordering & dry-run
init’s writer assumes an empty dir (§6.4, “no --force, never overwrites”). Updating writes into a populated, user-owned tree, so the update path adds guards around the writer rather than changing init’s policy:
- Git safety net (required). The update refuses to run on a dirty working tree so every create/overwrite/delete is reviewable and revertible via
git diff/git restore(worktree_dirty, exit 1). Overridable with--force. A non-git project is treated like a dirty tree: refuse unless--force. New projects are git-initialized with an initial commit at scaffold time so the net works immediately. - No merge engine. Managed/shared files are overwritten outright; the user reconciles any hand-edits through git. There is no three-way merge and no
.newshadow files. - Write ordering.
apply_update(1)rm -rfs theRemove/Replacedirs first — so aReplaceclears the old tool’s files before the new ones are written into the same path; then (2) writes the created/replaced/re-rendered component files; then (3) overwrites only the shared files whose bytes changed. There is nothing else to persist (no manifest). Kept components are never in the plan, so user work in an unchanged slot is untouched wherever a crash lands; a crash mid-rewrite of a changed slot is covered by the git safety net, and re-running is idempotent. --dry-run. Prints the change set (CREATE / REMOVE / REPLACE / RE-RENDER / shared-file overwrites) and writes nothing — the auditable plan for humans and agents.--format jsonemits the same as structured data. Each change row names the tool that moved (e.g.replace Aiken → Scalus,add Kupo, infra+Dolos/-Kupo).
15.7 CLI surface
cardano-init add --off-chain tx3 # add/replace a slot (same flag vocabulary as init)
cardano-init add --infra ogmios # repeatable; dedup keep-first
cardano-init remove --off-chain # drop a role (by role, not tool)
cardano-init remove --infra kupo # drop one infra provider
add/removereuseoneshot’s per-role flag parsing and validation verbatim.addtakes the same role flags as init (--on-chain,--off-chain,--fullstack,--infra(repeatable),--devnet,--formal-methods);removetakes bare role flags plus--infra <id>.- Global flags honored:
--dry-run,--ignore-warning,--allow-experimental,--format, plus--force. - Deliberately not a
swapverb: anadd --off-chain Xonto an occupied off-chain slot is the swap (REPLACE), reported as such in the diff so it is never silent. - An interactive
edit(re-open the selector seeded from the detected stack) was considered and dropped as low-value —add/remove(with--dry-run) already cover editing the stack.
15.8 Edge-case matrix (update path)
Error codes are defined in §2.5. This extends the init matrix (§12) for add/remove.
| # | Situation | Handling |
|---|---|---|
| 1 | Add a role not present | CREATE new dir + re-wire shared layer. |
| 2 | Add a tool to an occupied non-infra slot | REPLACE (remove+create); shown as a swap in the diff; destructive → git-gated. |
| 3 | Add an infra provider already present | Dedup keep-first → nothing_to_change (idempotent). |
| 4 | Remove a role that isn’t present | nothing_to_change. |
| 5 | Remove the last non-infra role | Allowed; blueprint/.gitkeep dropped (predicate flips). |
| 6 | Remove on-chain while consumers remain | Allowed; consumers already degrade gracefully per contract (§7) when blueprint/plutus.json is absent. |
| 7 | Remove the last remaining role | Refused — no_roles_selected. |
| 8 | Swap that breaks off-chain↔provider compat | incompatible_tools unless --ignore-warning (gate on S_new). |
| 9 | Add/swap in an experimental tool | experimental_not_allowed unless --allow-experimental. |
| 10 | Two dirs → fullstack tool (fuse) | REMOVE on-chain/+off-chain/, CREATE protocol/. Full replace, git-recoverable; not a merge. |
| 11 | protocol/ → drop off-chain half | REPLACE protocol/ with a user-named on-chain tool; no in-place split. |
| 12 | Same tool on both roles, no [fullstack] | Two separate dirs (not fused); each removable independently. |
| 13 | Infra provider set changes | RE-RENDER infra/ in place from the new set; other slots untouched. |
| 14 | Unchanged slot (e.g. Aiken while off-chain changes) | KEEP — provably untouched (§15.5). |
| 15 | Unrecognized/renamed/foreign component dir | Interactive: surfaced, then a confirm to proceed with the detected stack (the odd dir is left in place, ignored). Non-interactive: project_unrecognized (fatal). |
| 16 | Ambiguous detection (2+ tools match one dir) | Resolved by definitive-match tiebreak (§9.6): a single bare-path/manifest match wins over shared-file contains matches. Only if that leaves 2+ still tied is the dir treated as unrecognized. |
| 17 | infra/Justfile hand-edited so providers unparseable | Recovered set shown in confirm; user corrects; non-interactive → treat as unrecognized. |
| 18 | Unknown cardano-up package in infra/Justfile | Surfaced, not dropped; user confirms/removes. |
| 19 | .env missing/edited CARDANO_NETWORK | Field marked low-confidence; asked/defaulted in confirm. |
| 20 | Dirty git tree / not a git repo | worktree_dirty unless --force. |
| 21 | Crash mid-write | Creates-before-deletes ordering → no kept component lost; re-run is idempotent. |
| 22 | --dry-run | Prints the change set; writes nothing. |
| 23 | Slot swap that leaves a shared file byte-identical | Content diff skips it — no spurious rewrite. |
15.9 Determinism
Determinism (§11) is preserved: reconstruction + canonical planning are deterministic, so --dry-run output and the applied change set are reproducible for a given (binary, tree, mutation).
cardano-init: Roadmap
Status: Draft · Last updated: 2026-06-03 · Owner: Robertino Martinez
Sequencing and milestones. The why/for whom is in PRD.md; the how in ARCHITECTURE.md and TECH_SPEC.md. This roadmap is anchored on two external Developer Experience Initiative (DX) milestones tied to delivery; scope may shift after the DX.02 community/key-player review (that review exists precisely to pressure-test the specs before the RC).
Milestones at a glance
| Phase | Milestone | Date | Outcome |
|---|---|---|---|
| 0 | DX.02: Specs + POC | 31 Aug 2026 | A working, could-be-final tool showing all five roles, with specs/architecture/integration strategy, shown to the community + key players for feedback. |
| 1 | DX.05: RC ready | 30 Nov 2026 | Public Release Candidate: reduced-stack CLI that builds green, docs, a dependency install command (nice-to-have), hardened CI/CD + tests. |
| 2 | Post-RC / GA | TBD (post-Nov) | Stabilize RC → GA, widen the stack, promote auto-install to supported. |
| 3 | Later | Unscheduled | Plugin hooks, min-version checks, community-driven scope. |
Deliverables below are tracked as checklists ([ ] = not yet done).
Phase 0: DX.02 · Specs + POC (31 Aug 2026)
Milestone acceptance (verbatim): “A clear plan on how we’ll implement this tool with a POC. Repository with defined scope, specs, architecture, plugin/project integration strategy, and POC.”
Intent (beyond the letter): the POC should be good enough to pass as the final product, not a throwaway. It’s what we put in front of key players, so it must work. Breadth of the tooling story matters here: all five roles are present and generating.
Deliverables
Specs & strategy:
-
docs/PRD.md,docs/ARCHITECTURE.md,docs/TECH_SPEC.md,docs/ADDING_A_TOOL.md. - Plugin/project integration strategy = the interface contract (§4 TECH_SPEC) + the data-driven registry + the deps/installer model. This is the “how a tool plugs in” story the milestone asks for; make sure it reads as such.
The tool: all five roles present, four building green, formal-methods experimental:
- On-chain: Aiken; make the template genuinely
build+testgreen (blueprint at canonical path). - Off-chain: MeshJS + Tx3; both generate and build.
- Infrastructure: filled via
cardano-up— Kupo, Ogmios, Dolos, Tx Submit API, Cardano Node, Cardano Node API, and Dingo ship as selectable providers that aggregate into a singleinfra/component (one cardano-up context per project). Adding further providers is pure data (a registry TOML, no template). Usescardano-up’s released--contextflag (blinklabs-io#294). Yaci DevKit remains in the devnet role (it is a dev/test kit, never deployed). - Devnet: Yaci DevKit (local devnet — its
devstarts a Blockfrost-compatible devnet and writes the standard.envconnection vars, so off-chain connects to it automatically;testruns an integration smoke test). - Formal-methods: experimental; visible in the registry/UI as “experimental”; Blaster is a WIP upstream tool and its integration isn’t build-green yet (made real at DX.05). Registry
experimentalflag (per-tool, §3.2.1 — covers unstable-upstream and/or not-build-green) surfaced acrosslist/--help, interactive, and one-shot, and gated behind--allow-experimental/ an interactive confirm so it can’t be scaffolded unknowingly;list/component JSON carry it for agents.
Feature surface:
- Interactive CLI and one-shot CLI (polish; deterministic output).
-
listsubcommand + global--format human|jsonpresenter + machine-readable error codes (agent surface, PRD FR-13/14/15). -
cardano-init doctorstandalone command + check-and-advise during scaffolding (deps/installer graph,registry/deps.toml). (Pulled earlier than the docs’ original deferral.) -
--dry-run, optional Nix flake. - Determinism canonicalization (planner) + snapshot tests + contract-compliance tests per template.
Success criteria
- Repo presents defined scope, specs, architecture, and the integration strategy (above).
cardano-initgenerates projects for all five roles (four build-green, formal-methods experimental).- The feature surface above is demoable end-to-end and stable enough to show key players.
Cut-line (if behind in August)
Protect the all-five-roles; relax build-green under pressure. Keep every role visible and generating plus the feature surface demoable. If time is short, the harder templates (possibly Tx3) may ship as “generates but not yet fully green,” with SM-1 completion moved to DX.05. Floor that never slips: Aiken (on-chain) and MeshJS (off-chain) build green, and the project generates for every role.
Phase 1: DX.05 · RC ready (30 Nov 2026)
Milestone acceptance (verbatim): “First Release Candidate for the tool, with the website and docs, has been published. Users can already use this tool to create new Cardano projects with reduced tech stack options.”
- Repo beta deemed good enough to be an RC;
- Website with documentation, publicly accessible.
- CLI works with reduced stack already implemented but ready to be expanded, with expected non-critical defects.
Deliverables
Stack: widen each role + finish build-green:
- A couple more tools per role (exact picks chosen from community feedback).
- Finish SM-1 (build+test green) for everything shipped, including any DX.02 relaxations.
- Formal-methods made real (build-green), promoted from experimental.
Docs (publicly accessible):
- Docs site: Comprehensive documentation about usage for end users. It could be hosted on a dedicated website, or part of Cardano’s Developer Portal.
Dependency install command (nice-to-have, attempted):
-
cardano-initdependency install (auto-install): runs the doctor’s resolved plan with consent, across the installer graph (incl. bootstrappingaikup/cardano-up, etc.). Officially nice-to-have; first thing cut if it threatens the RC date.
Editing an existing project (#26):
-
cardano-init add/remove: reconstruct the current selection by detection, diff at the component-directory level, re-wire the shared files, write under a git-clean safety net. Never rewrites user code in a kept component. Reuses the init compat/experimental gates. New projects are git-initialized with an initial commit so the safety net works immediately. See TECH_SPEC §15 (PRD FR-25).
Engineering hardening:
- CI/CD pipeline improvements: per-tool build smoke tests (toolchains or Nix), snapshot/determinism gates, contract-compliance gates, release artifacts.
- Scheduled maintenance smoke run (weekly cron): re-runs the per-tool build+test matrix to catch generated projects breaking from a hardfork / upstream release / dependency bitrot between commits, opening a tracking issue on failure. Distinct from PR gates (ARCHITECTURE §10).
- Version-update check (pre-generation notice, §10 TECH_SPEC).
Success criteria
- Public repo at RC quality (non-critical defects acceptable).
- Public docs site reachable.
- CLI creates working projects across the reduced stack, expandable (adding a tool is data + template per ADDING_A_TOOL).
Cut order (if behind in November)
- Dependency auto-install command (explicitly nice-to-have).
- The extra (second/third) tools per role → ship the DX.02 set, expand post-RC. Never cut: the DX.02 build-green stack reaching full SM-1, and public docs.
Phase 2: Post-RC / GA (TBD, post-Nov 2026)
Direction (unscheduled; shaped by RC feedback):
- Stabilize RC → 1.0 GA (defect burndown, version/tagging scheme finalized).
- Widen the stack further: more tools across all roles, deeper coverage.
- Promote auto-install from nice-to-have to a supported path; cardano-up self-install when absent.
Phase 3: Later (unscheduled)
- Plugin/lifecycle hooks for tools (e.g. post-scaffold actions), reserved by the architecture, not yet needed.
- Min-version constraints in the doctor (version detection, not just presence).
- New roles, if a real need emerges (a deliberate code change, ARCHITECTURE §3.1).
Critical path & risks
- Per-tool build-green is the long pole. Every shipped tool must actually compile and pass tests (SM-1): that’s real, per-tool integration work (toolchains, the blueprint/env wiring). New templates (Tx3, Yaci DevKit) and making the existing prototypes genuinely green are the bulk of DX.02 effort.
- The doctor graph is new surface.
registry/deps.toml+ the installer table + the recursive resolver +cardano-init doctoris net-new for DX.02; the auto-install command (DX.05) builds on it. - Formal-methods tooling is thin: hence experimental at DX.02; the DX.05 “make it real” item carries the most uncertainty and may stay experimental.
- Spec churn after DX.02 is expected and healthy. The key-player review may change scope; Phase 1 picks (the extra per-role tools, formal-methods approach) are deliberately left to be informed by that feedback.
- Dates are fixed (payment-linked); scope flexes. The cut-lines above are the agreed release valves so the milestones land.
Releasing
cardano-init ships prebuilt binaries via dist
(cargo-dist). Releases are tag-driven: pushing a vX.Y.Z tag triggers
.github/workflows/release.yml, which builds every target, generates installers, and creates
the GitHub Release with the binaries attached.
Config lives in dist-workspace.toml
That file is the source of truth — the workflow is a compiled artifact of it:
targets— the 5 platforms we build (x86_64/aarch64 Linux, x86_64/aarch64 macOS, x86_64 Windows).installers—shell+powershellone-line install scripts.cargo-dist-version— pins the exactdistversion CI uses. Keep it equal to thedistversion you run locally, ordist generate --checkwill fail in CI.
Never hand-edit .github/workflows/release.yml. To change the pipeline, edit
dist-workspace.toml and re-run dist generate (see below). CI enforces this with
dist generate --check.
Cutting a release
- Bump
versioninCargo.toml(SemVer). - Commit the bump on a branch and merge it to
mainas usual. - Tag and push from
main:git tag v0.2.0 git push origin v0.2.0 release.ymlruns: builds all 5 targets, then creates the GitHub Release with the archives, installer scripts, and checksums attached. The release body is auto-generated (install + download sections) — there is no hand-maintained changelog.
A version with a prerelease suffix (e.g. v0.2.0-rc.1) is automatically marked as a
prerelease on GitHub.
Testing before you tag
Nothing here requires a push:
dist plan # what artifacts would be produced (resolves all targets)
dist generate --check # confirms release.yml matches dist-workspace.toml (the CI gate)
dist build --artifacts=local --target <your-host-triple> # real compile + archive locally
To exercise the full build matrix in CI without publishing, set pr-run-mode = "upload" in
dist-workspace.toml, dist generate, and open a PR — CI compiles all 5 targets and attaches
them to the workflow run (no tag, no Release). For a live end-to-end dry run, push a prerelease
tag to a fork and delete it afterward.
Regenerating the workflow
After any change to dist-workspace.toml (or when bumping the pinned dist version):
dist init # optional: interactive config update; also runs generate
dist generate # re-render .github/workflows/release.yml from config
Install a specific dist locally with cargo install cargo-dist --version <X> --locked.
Deferred distribution channels
Homebrew, npm, and crates.io publishing are intentionally off pending credentials. The exact
switches are documented in the comment block in dist-workspace.toml:
- Homebrew — add
"homebrew"toinstallers+ ataprepo (needs a tap repo + token). - npm — add
"npm"toinstallers+npm-scope(needs anNPM_TOKENsecret). - crates.io — add
publish-jobs = ["cargo"](needs aCARGO_REGISTRY_TOKENsecret).
Re-run dist generate after enabling any of them.