Skip to main content

Command-line interface

Version stamp

Applies to: renvor 0.0.0 · framework source 7d0816a · MSRV 1.94.0 · documentation set pre-release

This 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.

Nothing is published

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

CommandWhat it does
renvor new [NAME]Create a project, from prompts or from flags.
renvor doctorReport what is installed, what version, and what is missing.
renvor check [PATH]Validate a project's renvor.toml without building it.
renvor devRun the local development loop.
renvor docker up|down|status|logsContainer development controls.
renvor tls trustThe 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:

FailureReason
The project declares no Renvor dependencyno_renvor_dependency
The binary could not be startedinvocation_failed
The binary did not answer within the deadline — it is killedinvocation_timed_out
The binary exited non-zero, or reported a failuredump_failed
The answer was not one JSON document, or carried no descriptiondump_unreadable
The answer declared a protocol version this renvor does not understandprotocol_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.

Currently succeeds against no generated project

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

OptionEffect
--output human|jsonResult format on stdout. Defaults to human.
--yesAccept the review screen. Never waives validation, and never grants trust-store consent.
--dry-runCompute and report; write nothing. Honoured by every command that can change anything.
--no-colorDisable 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.

ExitMeaning
0Success.
1Unclassified or internal failure. A defect — please report it.
2Usage error. The invocation was malformed.
3Validation failure. The invocation was well formed and the request was refused.
4Cancelled by the operator.
5Environment 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" }
}
}
  • schemaVersion is an integer, and is bumped if any name below changes. It is 2: on 2026-08-18 destination_not_empty was retired and destination_exists, project_verification_failed, container_controls_missing, and staging_failed were added. If you match destination_not_empty, you are matching a code renvor no longer emits.
  • result and error are exclusive: exactly one is present.
  • details values are always strings. Match on code, never on message.

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

codeExitWhen
usage2The invocation was malformed or an answer nothing determines was missing.
unsupported_value3A flag value outside the supported set. details.supported lists them.
unsupported_combination3Individually valid choices that conflict. Both are named.
reserved_for_later_phase3A flag, value, or operation a later phase will support. details.phase names it.
invalid_project_name3Empty, not a usable package name, or a reserved device name.
destination_exists3Something is already at the destination. details.rule is always destination_absent; details.found is directory, file, symlink, other, or unknown.
destination_rejected3The 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_missing3The destination's parent does not exist or does not resolve.
manifest_invalid3renvor.toml failed validation. The field and the constraint are named.
project_verification_failed3A project's own checks failed, or could not be run. details.check names the check.
transport_not_wired3Route inspection could not obtain the project's route registry, because the project declares no Renvor transport wiring.
container_controls_missing3The project has no compose.yaml; generate it with --container.
render_failed3Template rendering failed. The destination is untouched.
bound_exceeded3A documented bound was exceeded. details.bound and details.limit say which and what.
staging_failed3The staging directory could not be created — usually the destination's parent is not writable. Nothing was staged.
placement_failed3The final move could not be performed.
cancelled4You cancelled, declined, or interrupted.
tool_missing5A required tool is absent or too old.
container_runtime_unavailable5details.reason is not_installed, not_running, not_responding, or command_failed.
internal1Unclassified. 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

ConditionStyling
--output jsonnever
the stream is not a terminal — a pipe, a file, a CI lognever
TERM=dumbnever
--no-colornever
NO_COLOR set to a non-empty valuenever

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, and cargo run must all pass. A project that does not build is a generation failure reported as project_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 renvor never deliberately deletes one. An empty directory, a non-empty directory, a file, and a symbolic link are all refused with destination_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 700 came back as 755 and the inode changed. If you were relying on generating into a directory you had already made, create the parent and let renvor make 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-https records the intent and nothing else. renvor tls trust exists 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 --yes deliberately 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.