The desktop app (Story: “Launch a text-only render from the desktop app”) integrates against
this contract, not against EnvelopeRenderer.Cli's internals. Treat it as the stable interface
between the two; changing it is a cross-cutting decision, not a local one.
Implementation: src/EnvelopeRenderer.Cli.
All three are required. There is no short form and no = form in this MVP — keep the surface
intentionally small per the story's conversation notes.
| Argument | Value | Notes |
|---|---|---|
--template <path> |
Local or UNC path to the XML template | Must exist and be readable. |
--csv <path> |
Local or UNC path to the CSV data file | Must exist and be readable. |
--output <path> |
Local or UNC path to write the rendered PDF to | Parent directory must already exist; the CLI does not create directories. |
-h, --help |
— | Prints usage to stdout and exits 0. Overrides everything else on the command line. |
Unknown arguments, a flag given without a value, a flag given twice, or an empty value are all
usage errors (exit 2).
--help text (human-readable by exception,
since it's explicitly for a human at a terminal), and — once argument/path validation has
succeeded and rendering begins — one line-oriented PROGRESS event per line (see “Progress
events” below). The desktop app should be able to treat stdout as parseable and never need to
filter noise out of it.ERROR: . Multiple problems (e.g.
two missing arguments) are each reported as their own line rather than stopping at the first.
A render-stage failure still writes its ERROR: line(s) here exactly as before — the stdout
failure progress event described below supplements this, it never replaces it, and it never
changes the exit code.Once argument parsing, input-path validation, and output-path validation have all succeeded
(i.e. the CLI is past every exit-2/3/4 case and has started the actual render), it emits
one line per event to stdout in this fixed, space-separated, key=value format:
PROGRESS <kind> elapsedMs=<integer> completed=<integer> [reason=<text to end of line>]
<kind> is one of startup, render, complete, failure.elapsedMs — milliseconds since the render phase started, as an integer. Always present.completed — number of CSV records successfully rendered to a page so far. Always present
(0 for startup and for a failure that happened before any page was rendered).reason — present only on failure; a short, human-readable description of what went wrong.
It is always the last field on the line and takes everything remaining on the line
verbatim (embedded newlines are stripped to a single space first), so it never needs quoting
or escaping and a consumer can safely Split(' ', 5)-style parse the fixed fields first.Why this shape instead of one JSON object per line: it stays trivially parseable with a plain
string split (no JSON library dependency needed in the desktop shell), it stays readable at a
terminal or in a redirected log file — matching the plain-text style already used for stderr's
ERROR: lines — and every numeric field is a culture-invariant integer, so there's no
decimal-separator ambiguity across locales. Implementation:
Progress/ProgressEventFormatter.cs.
Event kinds, each emitted at most as documented:
| Kind | When | Count per run |
|---|---|---|
startup |
Immediately when the render phase begins, before template parsing, CSV header validation, or Debenu setup. | Exactly 1 |
render |
After a record is successfully rendered to a page. Throttled to at least once per second — see below — not once per row. | 0 or more |
complete |
The run finished successfully; completed is the final record count. |
Exactly 1, only on success |
failure |
The run failed at any point during the render phase (template parse, CSV read, Debenu setup, or the per-record render/save loop); completed is however many records were successfully rendered before the failure, and reason summarizes the same problem reported to stderr. |
Exactly 1, only on failure |
A run therefore always starts with exactly one startup line and ends with exactly one
complete or failure line, with zero or more render lines in between. Argument parsing and
path-validation failures (exit 2/3/4) produce no progress events at all — that failure
mode is entirely a pre-render validation error, unaffected by this contract.
render events are throttled to at least once per second of wall-clock time, not once per CSV
row, so a fast render of thousands of rows doesn't flood stdout with one line per row. The first
render event after startup is always written immediately (so a slow run shows something
right away); after that, an event is only written once at least 1000ms have passed since the
last one that was actually written. startup, complete, and failure are never throttled —
each happens exactly once, so there's nothing to throttle. Implementation:
Progress/ConsoleProgressReporter.cs.
| Code | Meaning |
|---|---|
0 |
Success — PDF written to --output. |
1 |
Template, CSV, or render error: malformed/invalid template XML, a template column not present in the CSV header row, an empty CSV, a missing/unresolvable font, or a Debenu render/save failure (including a missing or invalid license key — see below). Read stderr for which. |
2 |
Invalid usage — missing, unknown, duplicate, or empty-valued argument. |
3 |
Input not found — --template or --csv path does not exist. |
4 |
Output path invalid — output directory does not exist, or the output path is itself an existing directory. |
Validation runs in this order and stops at the first failing stage: usage → input paths →
output path → render. A caller can rely on, e.g., never seeing an output-path error (4) while
an input path is still missing.
The text-only template format (--template) is documented separately in
TEMPLATE_FORMAT.md.
Rendering (not argument validation) requires a Debenu Quick PDF Library 10.13 license key at
runtime, read from the DEBENU_LICENSE_KEY environment variable. Without it, every render fails
at the save step with exit 1 — confirmed directly against the vendor DLL: page creation, font
embedding, and text drawing all succeed, but SaveToFile/SaveToString both return error code
999 regardless of content. A working key for local development is kept at the project root in
key.txt (untracked — never commit it or hardcode it into source).
Local and UNC paths are both accepted and validated identically — System.IO.File.Exists /
Directory.Exists handle UNC paths natively, so there's no special-case code. What is not
yet defined is timeout/retry behavior against a slow or unreachable UNC share; that's an open
impediment (logs/impediment_log.md, logged 2026-09-04) carried as an explicit risk into the
Debenu render story, not solved here.
Run from code/:
$ dotnet run --project src/EnvelopeRenderer.Cli -- --help
(usage text)
exit=0
$ dotnet run --project src/EnvelopeRenderer.Cli --
ERROR: Missing required argument '--template'.
ERROR: Missing required argument '--csv'.
ERROR: Missing required argument '--output'.
(usage text on stderr)
exit=2
$ dotnet run --project src/EnvelopeRenderer.Cli -- --template nope.xml --csv sample-data/wilson.csv --output out.pdf
ERROR: Template not found: 'nope.xml'.
exit=3
$ dotnet run --project src/EnvelopeRenderer.Cli -- --template envelope.xml --csv sample-data/wilson.csv --output nosuchdir/out.pdf
ERROR: Output directory does not exist: 'nosuchdir'.
exit=4
$ dotnet run --project src/EnvelopeRenderer.Cli -- --template sample-data/sample-envelope-template.xml --csv "sample-data/87700 - 999999 - Wilson Township.csv" --output out.pdf
# stdout:
PROGRESS startup elapsedMs=0 completed=0
PROGRESS render elapsedMs=802 completed=1
PROGRESS render elapsedMs=1804 completed=392
PROGRESS failure elapsedMs=1805 completed=392 reason=Failed to save PDF to 'out.pdf' (error code 999).
# stderr:
ERROR: Failed to save PDF to 'out.pdf' (error code 999).
exit=1
# (DEBENU_LICENSE_KEY not set — see "Debenu license key" above. Note the throttling in action:
# 392 records were each individually reported to RenderEngine, but only 2 `render` lines were
# actually written to stdout because the whole render took under 2 seconds.)
$ DEBENU_LICENSE_KEY="$(cat ../key.txt)" dotnet run --project src/EnvelopeRenderer.Cli -- --template sample-data/sample-envelope-template.xml --csv "sample-data/87700 - 999999 - Wilson Township.csv" --output out.pdf
exit=0
# stdout (illustrative — not re-captured for this story since no license key was available in
# this dev environment; the render loop that produces `startup`/`render` is identical to the
# failure example above, a valid key only changes the final Save() outcome, so the only actual
# difference is the last line):
PROGRESS startup elapsedMs=0 completed=0
PROGRESS render elapsedMs=802 completed=1
PROGRESS render elapsedMs=1804 completed=392
PROGRESS complete elapsedMs=<final> completed=392
Powered by TurnKey Linux.