Command-line interface
Version stamp
Applies to:
renvor0.0.0 · framework source 7d0816a · MSRV 1.94.0 · documentation set pre-releaseThis stamp is a single shared partial (
docs/_stamp.mdx) imported by every prose page and the API reference. It binds this documentation snapshot to the immutable framework commit it describes. The framework remains unpublished and no release compatibility promise applies.
renvor is not on crates.io, so there is no published or supported installation path. This
page documents the contract the executable already implements, because the specification requires
the command surface, exit codes, and JSON shape to be published before the work counts as
complete — not merely written down in a specification nobody outside the project reads.
Everything below is implemented and tested. None of it has a supported installation path yet.
The executable is renvor
The crate is renvor-cli; the executable it installs is renvor. Those are separate facts and
both are deliberate. The executable name is a compatibility promise — it is the string you
type and your shell resolves — and the build asserts it, with a control that fails if the name
is changed.
Commands
| Command | What it does |
|---|---|
renvor new [NAME] | Create a project, from prompts or from flags. |
renvor doctor | Report what is installed, what version, and what is missing. |
renvor check [PATH] | Validate a project's renvor.toml without building it. |
renvor dev | Run the local development loop. |
renvor docker up|down|status|logs | Container development controls. |
renvor tls trust | The consent boundary for a trust-store change. Issues nothing. |
renvor routes [PATH] | Show the routes a project would serve, by asking the project's own binary. |
renvor openapi [PATH] | Show the OpenAPI 3.2.0 description a project would publish, by asking the project's own binary. |
Commands that later phases will add — generate, migrate, seed — are absent, not
stubbed. A command that exits zero without doing the work reports success for something that did
not happen.
openapi left that list in Phase 005 and is now implemented.
renvor openapi asks the application, and bounds the asking
It does not parse your source, and it does not read a second manifest. It runs the project's own declared binary through its build tool with a documented flag, and the binary prints the description built from the same registry that builds its router — then exits, before it starts anything.
Six ways it can fail, each with its own name and a non-zero exit:
| Failure | Reason |
|---|---|
| The project declares no Renvor dependency | no_renvor_dependency |
| The binary could not be started | invocation_failed |
| The binary did not answer within the deadline — it is killed | invocation_timed_out |
| The binary exited non-zero, or reported a failure | dump_failed |
| The answer was not one JSON document, or carried no description | dump_unreadable |
The answer declared a protocol version this renvor does not understand | protocol_unsupported |
None of them prints an empty description and exits zero. An empty description and "this project cannot be asked" are entirely different facts, and a consumer cannot tell them apart if both arrive as success.
No Renvor crate is published, so no project the generator produces depends on the framework, and
renvor routes and renvor openapi therefore succeed against none of them.
The relays are implemented and are tested end to end against a real binary answering through the real library. What is zero is their reach across generated projects, and it is zero because nothing is published for those projects to depend on.
Global options
| Option | Effect |
|---|---|
--output human|json | Result format on stdout. Defaults to human. |
--yes | Accept the review screen. Never waives validation, and never grants trust-store consent. |
--dry-run | Compute and report; write nothing. Honoured by every command that can change anything. |
--no-color | Disable styling. See Terminal presentation for every other way styling turns itself off. |
Exit codes
Every command uses this taxonomy, and every error code maps to exactly one exit code.
| Exit | Meaning |
|---|---|
0 | Success. |
1 | Unclassified or internal failure. A defect — please report it. |
2 | Usage error. The invocation was malformed. |
3 | Validation failure. The invocation was well formed and the request was refused. |
4 | Cancelled by the operator. |
5 | Environment failure. |
1 is reserved. An unclassified failure is a bug, and a taxonomy that folds it into a
general error code hides that. Rust's own default is to exit 101 on a panic; renvor
installs a panic hook so that a panic exits 1 — and still emits one JSON document.
The machine-readable contract
With --output json, stdout carries exactly one JSON document, for success and for
failure alike. Not zero on failure: the moment a consumer most needs a parseable answer is the
moment something went wrong.
{
"schemaVersion": 2,
"status": "success",
"command": "new",
"result": { "destination": "…", "dryRun": false, "manifest": [], "templateVersion": "1" }
}
{
"schemaVersion": 2,
"status": "failure",
"command": "new",
"error": {
"code": "destination_exists",
"message": "…",
"details": { "destination": "…", "rule": "destination_absent", "found": "directory" }
}
}
schemaVersionis an integer, and is bumped if any name below changes. It is2: on 2026-08-18destination_not_emptywas retired anddestination_exists,project_verification_failed,container_controls_missing, andstaging_failedwere added. If you matchdestination_not_empty, you are matching a code renvor no longer emits.resultanderrorare exclusive: exactly one is present.detailsvalues are always strings. Match oncode, never onmessage.
This holds even when the command line itself cannot be parsed. renvor new demo --nonsense --output json produces a usage document rather than a bare parser error, because the format
has to be known while the command line is still unparseable.
Streams
stdout carries only the result. Progress, warnings, prompts, and diagnostics all go to
stderr. A pipeline needs no filtering, and a closed stdout is handled rather than fatal.
Styling never changes any of this, and never appears in a JSON document. See Terminal presentation.
Error codes
code | Exit | When |
|---|---|---|
usage | 2 | The invocation was malformed or an answer nothing determines was missing. |
unsupported_value | 3 | A flag value outside the supported set. details.supported lists them. |
unsupported_combination | 3 | Individually valid choices that conflict. Both are named. |
reserved_for_later_phase | 3 | A flag, value, or operation a later phase will support. details.phase names it. |
invalid_project_name | 3 | Empty, not a usable package name, or a reserved device name. |
destination_exists | 3 | Something is already at the destination. details.rule is always destination_absent; details.found is directory, file, symlink, other, or unknown. |
destination_rejected | 3 | The destination failed a path-boundary rule. details.rule names which — including destination_unverifiable, when renvor could not read the destination's state at all. |
destination_parent_missing | 3 | The destination's parent does not exist or does not resolve. |
manifest_invalid | 3 | renvor.toml failed validation. The field and the constraint are named. |
project_verification_failed | 3 | A project's own checks failed, or could not be run. details.check names the check. |
transport_not_wired | 3 | Route inspection could not obtain the project's route registry, because the project declares no Renvor transport wiring. |
container_controls_missing | 3 | The project has no compose.yaml; generate it with --container. |
render_failed | 3 | Template rendering failed. The destination is untouched. |
bound_exceeded | 3 | A documented bound was exceeded. details.bound and details.limit say which and what. |
staging_failed | 3 | The staging directory could not be created — usually the destination's parent is not writable. Nothing was staged. |
placement_failed | 3 | The final move could not be performed. |
cancelled | 4 | You cancelled, declined, or interrupted. |
tool_missing | 5 | A required tool is absent or too old. |
container_runtime_unavailable | 5 | details.reason is not_installed, not_running, not_responding, or command_failed. |
internal | 1 | Unclassified. A defect. |
Terminal presentation
renvor styles its human output when it is talking to a terminal, and never otherwise. The full
rules are in the versioned contract; the parts worth knowing before you run it are these.
Colour turns itself off, for five separate reasons
| Condition | Styling |
|---|---|
--output json | never |
| the stream is not a terminal — a pipe, a file, a CI log | never |
TERM=dumb | never |
--no-color | never |
NO_COLOR set to a non-empty value | never |
Any one of these is enough. There is no condition that turns styling on — it is what remains when nothing has switched it off.
An explicit refusal wins. --no-color and NO_COLOR beat CLICOLOR_FORCE, and they beat it in
the prompts as well as in the ordinary output, because renvor decides once and tells its prompt
library the answer instead of letting it consult the environment separately.
NO_COLOR follows no-color.org exactly: it counts when it is present and
not empty, whatever its value. NO_COLOR= is how a script cancels an inherited request, and it
works.
The decision is made per stream. renvor doctor > report.txt writes a plain file even though
your terminal is still attached to stderr.
Colour is never the only signal
Every state carries a word — INFO, WARN, ERROR, DONE, and on a table row OK, TOO OLD,
MISSING, ABSENT. Pipe the output, run it under TERM=dumb, or read it with a screen reader,
and you lose the decoration and nothing else.
There are no emoji, in any mode.
Rows adapt to your terminal, and never truncate
A label and a value are joined by leader dots and aligned to the right edge. Widths are measured in columns, so CJK text and combining marks line up correctly. When a row cannot fit, it stacks — the label on one line, the value indented beneath it — rather than shortening the value, because a truncated path is a path that looks like a different path.
Prompts
Questions are drawn as a connected sequence. The live question is highlighted; an answered one
recedes while its answer stays readable. Ctrl-C and Escape both cancel with exit 4 and leave the
destination untouched, and the cursor is restored on every path out — including a crash.
Confirmations submit on the keypress. y answers yes and n answers no with no Enter after
them; arrow keys move the selection and Enter takes it. If you type y then Enter out of habit,
the stray Enter falls through to the next question and accepts its default.
Progress
renvor new verifies the project it generated by running five cargo checks against it, and
their output is captured. A progress indicator on stderr names the check that is running, so a
cold compile looks like work rather than like a hang. It is absent in --output json and absent
whenever stderr is not a terminal.
On a terminal that cannot be redrawn — TERM=dumb, or TERM not set at all — you get one
static line naming the operation instead of the live indicator. The rule is that you are told
the work is happening, not that a spinner appears.
renvor new is transactional
Generation runs in this order, and the ordering is the guarantee:
validate → stage → render → verify → manifest → review → place → report
-
Validation completes before anything is written. The destination boundary is checked, the name is checked, and cross-choice constraints are checked, all before a directory is created.
-
Rendering happens in a staging directory created inside the destination's parent — never the system temporary directory, so the final step can be a rename rather than a copy across filesystems.
-
The generated project is verified while it is still in staging: its own
cargo fmt --check,cargo clippy -- -D warnings,cargo build,cargo test, andcargo runmust all pass. A project that does not build is a generation failure reported asproject_verification_failed, not something you discover later. -
Placement is a single rename. It never falls back to a copy.
-
The destination must not exist, and
renvornever deliberately deletes one. An empty directory, a non-empty directory, a file, and a symbolic link are all refused withdestination_exists, before anything is staged. If renvor cannot even tell what is there, it refuses rather than guessing. Nothing you already have is deleted, renamed,chmod-ed, replaced, or restored by any decision renvor makes.One exception, and it is the operating system's rather than renvor's. On Unix,
rename(2)silently replaces an empty directory. If another process creates one at your destination in the moment between renvor's last check and the move, that directory is replaced. There is no portable way to prevent it: the Linux-only call that would is not available elsewhere, and the obvious workaround fails on Windows.Changed on 2026-08-18. Until then an existing empty directory was accepted and then removed and recreated, so a deliberate
chmod 700came back as755and the inode changed. If you were relying on generating into a directory you had already made, create the parent and letrenvormake the last component. -
A failure anywhere before placement leaves the destination exactly as it was, and leaves no staging directory behind.
If a run is killed outright — SIGKILL runs no destructor — the staging directory survives on
purpose. It is named .renvor-staging-<pid>-…, sits beside the destination rather than
inside it, and renvor doctor reports it. doctor will not delete it: renvor cannot tell an
abandoned staging directory from one belonging to a renvor new running in another terminal
right now.
Prompts and flags produce the same project
Every wizard question has an equivalent flag, both interfaces resolve to one validated configuration value, and the review screen prints the exact command that reproduces your answers — a command that is executed verbatim by the test suite, not merely printed.
When stdin is not a terminal, the wizard does not prompt and does not block. Defaults that are
documented in --help still apply; an answer that nothing you typed determines is refused rather
than invented.
What this phase deliberately does not do
- No certificate is issued and no trust store is modified.
--local-httpsrecords the intent and nothing else.renvor tls trustexists to build the consent boundary — it describes exactly what would change, requires explicit consent, and then declines, because certificate issuance and trust-store integration are not implemented. The non-interactive consent flag is named--i-understand-this-modifies-my-system-trust-store, and--yesdeliberately does not grant it. - No archive is ever extracted. Every template is embedded in the executable. The build asserts that no archive-extraction crate is reachable from the binary.
- The generated skeleton requires no network access. It declares no dependencies, so there is nothing to resolve while generating it. Commands that invoke Cargo for an arbitrary project may still resolve that project's dependencies.