# CLI
The command-line interface crate. Provides `camel run`, `camel new`, `camel plugin`, and other
subcommands for building and running Camel routes from the terminal.
## ADR-0012 log-policy sites
This crate keeps eleven ADR-0012 `error!` sites. All are class **system-broken**:
CLI/bootstrap/shutdown lifecycle failures where no `ErrorHandler` exists to own
the ERROR. Per the ADR-0012 taxonomy (§ Taxonomy, l.19) these fall under
letter-code **(d) CLI, bootstrap, application startup/shutdown**, distinct from
the route-lifecycle code **(c)** (`consumer_management.rs` / `route_controller.rs`).
Both codes share the `system-broken` class. `error!` is preserved and each call
site carries a `// log-policy: system-broken` annotation. Sites are cited by
enclosing symbol. Line numbers are current-HEAD positions and are secondary to
the symbol.
Five former `camel run` sites moved to the boot cascade crate (ADR-0069
section 10). The table lists them under `camel-bundles` with their new anchors.
See `crates/camel-bundles/CONTEXT.md`.
| Symbol | File:Line | Code / class | Description |
|--------|-----------|--------------|-------------|
| `fn maybe_instrument_routes` | `commands/bench_instrument.rs:99` | (d) system-broken | cannot open `BENCH_LATENCY_FILE` |
| `async fn run` | `commands/run.rs:367` | (d) system-broken | failed to add route definition |
| `async fn run` | `commands/run.rs:402` | (d) system-broken | route discovery failed (detail sites share the failure arm) |
| `async fn run` | `commands/run.rs:415` | (d) system-broken | `CamelContext` start failed |
| `async fn run` | `commands/run.rs:469` | (d) system-broken | file watcher failed |
| `async fn run` | `commands/run.rs:513` | (d) system-broken | `BootHandle` teardown failed (log-and-continue, exit code unchanged) |
| `async fn run_job` | `commands/job/mod.rs:450` | (d) system-broken | `camel job`: component cascade boot failed |
| `async fn run_job` | `commands/job/mod.rs:462` | (d) system-broken | `camel job`: route loading failed |
| `async fn run_job` | `commands/job/mod.rs:558` | (d) system-broken | `camel job`: failed to add route definition |
| `async fn run_job` | `commands/job/mod.rs:578` | (d) system-broken | `camel job`: `CamelContext` start failed |
| `async fn run_job` | `commands/job/mod.rs:639` | (d) system-broken | `camel job`: send apparatus failure (producer/endpoint, not a pipeline verdict) |
| `fn boot` | `crates/camel-bundles/src/lib.rs:306` | (d) system-broken | moved to camel-bundles (boot cascade): failed to initialize SQL bundle. See `crates/camel-bundles/CONTEXT.md` |
| `fn boot` | `crates/camel-bundles/src/lib.rs:329` | (d) system-broken | moved to camel-bundles (boot cascade): failed to initialize SurrealDB bundle. See `crates/camel-bundles/CONTEXT.md` |
| `BootHandle::shutdown_with_deadline` | `crates/camel-bundles/src/lib.rs:103` | (d) system-broken | moved to camel-bundles (boot cascade): shutdown error from `ctx.stop`. See `crates/camel-bundles/CONTEXT.md` |
| `BootHandle::shutdown_with_deadline` | `crates/camel-bundles/src/lib.rs:111` | (d) system-broken | moved to camel-bundles (boot cascade): JMS pool shutdown failed. See `crates/camel-bundles/CONTEXT.md` |
| `BootHandle::shutdown_with_deadline` | `crates/camel-bundles/src/lib.rs:123` | (d) system-broken | moved to camel-bundles (boot cascade): CXF pool shutdown failed. See `crates/camel-bundles/CONTEXT.md` |
## Signal handling contract
`camel run` treats SIGINT and SIGTERM with the same rules.
The signal streams are armed at the start of `run`, before boot. A signal
that arrives during boot (config load, component boot, route discovery,
context start) is buffered, not default-killed. The run finishes boot and
then shuts down gracefully (rc-z5zch TERM, rc-ukwlt INT).
The first SIGINT or SIGTERM, at any time, starts a graceful shutdown: the
file watcher is cancelled, the context stops, and the component pools tear
down. The exit code is 0.
A subsequent SIGINT or SIGTERM, after the first was consumed and graceful
shutdown has begun, force-exits the process with code 1 (rc-kz85m). This is
the escape hatch for a hung teardown. Systemd and `docker stop` resend the
stop signal after their grace period, so the escape hatch must accept both
signals. Identical signal bursts may coalesce before delivery (tokio
semantics), so strict signal counting is not guaranteed.
On non-unix platforms there is no SIGTERM stream; the portable Ctrl+C
listener is the first-signal handler, and a second Ctrl+C force-exits.
## camel job signal handling
`camel job` arms its signal streams at the first lines of `run_job`,
before config loading, so a signal arriving during boot (config load,
bundle cascade, route discovery, context start) is buffered by the runtime
and consumed by the send/drain race instead of hitting the default
disposition and killing the process (spec: signal during boot is buffered;
mirrors `camel run` entry registration). Arming is document-runs only: the
no-argument listing path never installs the streams, because handlers
whose streams are never consumed would swallow SIGINT/SIGTERM during
listing instead of letting the default disposition terminate the process.
The first SIGINT or SIGTERM cancels the
in-flight send or batch drain, runs bounded teardown, and reports outcome
`Interrupted` with exit code 2. The teardown budget is mode-dependent:
one-shot floors the wall-clock remaining to the overall deadline at
`MIN_SHUTDOWN_BUDGET` (5 s), while batch keeps the no-floor rule —
teardown cannot run past the overall deadline, so a spent deadline
computes a zero budget there. A `shutdown_error` is recorded only when
teardown had a non-zero budget; a zero-budget failure — whether on the
interrupted-batch path or the timeout path — is a foregone artifact
(stderr-only, the verdict keeps the report).
After the first signal is consumed, a force-exit guard owns the streams: a
second SIGINT or SIGTERM during teardown exits 1 immediately without
waiting for the bounded shutdown (rc-kz85m — orchestrators resend the stop
signal after their grace period). Identical signal bursts may coalesce
before delivery (tokio semantics), so strict signal counting is not
guaranteed. The wait race is signal-first: a `biased` select polls the
signal arm before the operation, so a signal ready at the same poll point
as send completion or deadline expiry wins deterministically (spec:
signal-first tie).
On Unix the SIGINT and SIGTERM streams are registered at entry, so the
buffered-during-boot guarantee holds for the whole boot stretch. On
non-Unix platforms there is no SIGTERM stream; the portable Ctrl+C
listener is awaited inside the wait race, which first runs at the
send/drain race — tokio installs the console handler only when the first
`ctrl_c()` future is polled, so a Ctrl+C during boot hits the default
disposition and terminates the process outright (no `Interrupted` report,
no exit 2). The covered stretch on non-Unix starts at the send/drain race.
## camel job failure modes
`camel job <doc>` runs one `*.job.yaml` document declaring a top-level `execute:` section (mode `one-shot`/`batch`, one `direct:`/`seda:` send, mandatory `timeout`, one family route source, optional `description:` for listing). A bare name resolves `{jobs.dir}/<name>.job.yaml` only (`[jobs].dir` in `Camel.toml`, default `jobs`, anchored at the Camel.toml root); an explicit path always wins. `camel job` with no argument lists the jobs directory — name plus description via a cheap probe parse, `(unparseable)` siblings tolerated, empty/absent dir exits 0. It boots the REAL composition root (the `camel run` seams: config, security context, bind acks, the `camel_bundles` cascade, ambient `${env:}` discovery), starts every document route and relies on the load-time consumer allowlist for side-effect safety, with the send target as the sole entry point, sends one exchange, tears down through `BootHandle::shutdown_with_deadline`, and emits a JSON report to stdout (or `--report`) for outcomes that reach the send (verdict, interruption, or timeout) plus shutdown failures after a verdict; early exit-2 classes are stderr-only. Seda targets are rewritten to `waitForTaskToComplete=Always` so the send is synchronous (verdict fidelity). Exit precedence mirrors `camel test`: `2 > 1 > 0`. Route side-effect safety is fail-closed: documents whose routes consume (`from:`) from any scheme outside `{direct, seda, log, mock}` are rejected at load; `to:` URIs are unrestricted. `mode` accepts `one-shot` and `batch`; other values are rejected at load. The general tracing layer writes to stdout, so a machine-parseable stdout report needs `log_level = "off"` or `--report`.
| Failure mode | Trigger | Exit code |
|--------------|---------|-----------|
| Doc load error | unreadable file, non-`*.job.yaml` suffix (a `*.test.yaml` declaring `execute:` gets rename guidance), missing `execute:`, mixed `scenario:`/unit-tier sections, serde/grammar errors, unsupported `mode` (accepted: `one-shot`, `batch`), missing/invalid `timeout`, non-`direct:`/`seda:` send target, route-source conflict, no `Camel.toml` ancestor for `routeFilesFromRoot`, bare-name miss in `jobs/` | 2 |
| Job-safety rejection | a discovered route consumes from a scheme outside the `{direct, seda, log, mock}` allowlist; or the send target has no matching consumer route, or its base is ambiguous across several; or the route source resolves zero routes | 2 |
| Boot failure | config load, context configure, security compile context, `camel_bundles::boot`, route discovery/parse, route registration, `ctx.start()` | 2 |
| Pipeline failure | the send's route pipeline failed (`PipelineOutcome::Failed` through the producer reply seam) | 1 |
| Overall timeout | the mandatory `timeout` expired before send+drain+teardown completed (report outcome `Timeout`) | 2 |
| Signal interruption | first SIGINT/SIGTERM cancelled the in-flight send or batch drain (report outcome `Interrupted`; teardown runs under the one-shot `MIN_SHUTDOWN_BUDGET` floor or the batch no-floor deadline) | 2 |
| Send apparatus failure | producer/endpoint creation failed past the 3 s startup-race window (not a pipeline verdict) | 2 |
| Shutdown failure | teardown failed or exceeded its budget after a recorded verdict (report `shutdown_error` carries the detail; `error` keeps the verdict) | 2 |
| Report write failure | `--report` path unwritable, or report serialization failed | 2 |
## Compiled artifacts (`camel compile`)
`camel compile <document> -o <artifact>` produces a self-contained native
Linux preview artifact: a copy of the current executable with a fixed 68-byte
EOF trailer appended (normalized pre-interpolation document payload +
operational manifest + BLAKE3 footer; ADR-0075). The payload is captured
before `${env:}` interpolation, so `${env:NAME}` expressions resolve from the
deployment environment at artifact runtime, never from the compile
environment. V1 supports one route document or one job document;
cross-compilation, AST/AOT serialization, compiled tests, multi-entry jobs,
compression, signing, and long-running route-server artifacts are deferred.
Compilation fails closed (exit 2, no output) for non-native targets and for
every unsupported compile-time asset: route-source fields (`routeFiles`,
`routeFilesFromRoot`, glob patterns), configuration fields (`Camel.toml`,
profiles, `includes`, `CAMEL_*` compile overrides), and asset-bearing
endpoint fields (certificates, private keys, CA files, WASM/plugin files,
XSLT/XSD, SQL files, static directories, literal secret files, dynamic
placeholders in those fields). Asset-bearing endpoint URI schemes (`wasm:`,
`xslt:`, `validator:`) are rejected in every URI-bearing field (`from`, `to`,
`wire_tap`, `poll_enrich`, `enrich`, `dead_letter_channel`,
`scatter_gather.endpoints`). Runtime endpoint URI paths (e.g. `file:`,
`kafka:`, `log:`), runtime `${env:}` expressions outside forbidden fields,
and deploy-side network/file I/O remain permitted.
The binary self-detects its trailer before Clap parsing (`fn
self_detect_artifact`): a trailer-free image keeps the normal CLI; marked
corruption exits 2 with an integrity diagnostic; a valid artifact accepts
only `--report <path>`, `--help`, `--version`, and `--manifest` (anything
else exits 2 naming the argument). `--manifest` prints the operational
manifest and exits 0 without boot. Route artifacts run the embedded document
through the existing `camel run` lifecycle with the default in-memory config,
virtual source identity `compiled://<source_name>`, and watch disabled; job
artifacts use the existing single-document job lifecycle with the embedded
document as their sole route source. No temporary extraction occurs, so
artifacts run from a read-only root; the only write is an explicitly
requested report.
Route `--report <path>` writes the exact JSON object
`{"kind":"route","status":"completed"|"failed","error":string|null}` after
boot/runtime completion or failure, with exit 0 for graceful completion and
2 for boot/discovery/report-write failure (1 stays reserved for job pipeline
failures). Job artifacts keep the existing job outcome report and exit
precedence (`2 > 1 > 0`). Boot still performs parse, interpolation, lowering,
component boot, and context start, so no boot-speed claim is made before P0
measurement.
## Metrics
Metrics instrumentation for CLI commands is limited to the jemalloc memory
sampler (`allocator_metrics.rs`): with the `jemalloc` feature, `camel run`
samples allocated/resident/active/mapped every 5 s and emits
`camel_allocator_memory_bytes{stat}` through the context's late-bound handle
(ADR-0066); read failures warn and retry, init failure disables the sampler.
`tikv-jemalloc-ctl` must stay in lockstep with `tikv-jemallocator`. Processor-crate
instrumentation is tracked separately.
## Heap profiling
The `jemalloc` feature also compiles in heap profiling (jemallocator
`profiling` feature). Profiling stays off by default. Enable it at process
start with `prof:true` through the allocator envvar; the tikv symbol prefix
makes that envvar `_RJEM_MALLOC_CONF` (the musl-jemalloc-verify workflow
sets both spellings). This is a startup opt-in: a pod restart enables or
disables it, and runtime switching through `prof.active` would need extra
machinery. While profiling runs, the jemalloc ctl option `prof.dump` writes
a heap dump. musl/aarch64 profiling and the k8s dump path are not verified
yet; the human-dispatched musl-jemalloc-verify workflow owns that check
(bd rc-i9f9).
## camel test failure modes
`camel test` runs each `*.test.yaml` document in-process and reports one `PASS`/`FAIL` line per endpoint or asserted reply (unit tier) or per scenario action (full tier), preceded by one `[lean]`/`[full]` tier annotation line per executed document, then a final `N passed, M failed` summary. The tier is content-derived (`camel-integration-test::derive_tier`; a `scenario:` section forces full). Exit-code precedence is `2 > 1 > 0`: any parse-error, misuse, or apparatus class forces 2, else any verdict failure forces 1, else 0. A document-level error is reported to stderr and execution continues with the next document. Directory arguments expand recursively to `*.test.yaml`/`*.test.yml` documents (sorted, with `target`/`.git`/`node_modules` skipped). Documents declaring `scenario:` dispatch to the scenario parser (`parse_scenario_document`); when the CLI is built with `integration-http` and every wired endpoint scheme is `direct`, `http`, or `fake`, the document runs through the embedded FULL-tier boot (one harness `HttpPartner` per `http` endpoint bound on `127.0.0.1:0`, each `bindVar` folded into the layered environment, the real composition root, whole-document run, then teardown — ADR-0069 sections 4-5, 10). A `fake:`-only document keeps the no-boot smoke path in any build; any other scheme (or `http`/`direct` without the feature) reports `infra-unavailable` naming the adapter.
| Failure mode | Trigger | Exit code |
|--------------|---------|-----------|
| Doc parse error | unreadable file, invalid YAML, `TestDocError` from `parse_test_document`, or `DocError` from `parse_scenario_document` (doc-validation class) | 2 |
| Boot failure | unit-tier `CamelContext` boot, route load, route start, or input delivery fails; full-boot scenario partner bind (`partner-bind-failure`), sealed config load, or composition-root boot (`full-boot-failure`) | 2 |
| Expansion error | zero-document directory argument or unreadable directory during walk | 2 |
| Settle timeout | traffic does not quiesce within the quiet window plus the 5s instability budget | 1 |
| Assertion failure | expectation mismatch reported by `MockEndpointInner::try_assert_satisfied` | 1 |
| Reply assertion failure | `expectReply` mismatch on a captured reply (FAIL reply line) | 1 |
| Scenario verdict failure | `receive-timeout`, `validation-mismatch` (FAIL action line) | 1 |
| Scenario apparatus failure | runtime `scenario-var-unresolved` (authoring bug, rc-whof), `action-transport-failure`, `partner-startup-failure`, `shutdown-failure` (FAIL action line, ADR-0069 §7); `partner-startup-failure` is reserved in v1 — no adapter separates bind from handler start, bind failures report `partner-bind-failure` on stderr | 2 |
| Infra unavailable | scenario endpoint scheme has no partner adapter in this build; stderr names the adapter | 2 |
| Harness wiring error | a `send`/`receive` endpoint escapes the harness-built adapter map (doc-validation class, never a silent `ReceiveTimeout`) | 2 |
| Tier filter collision | an explicitly named document derives the tier the `--unit`/`--integration` filter excludes | 2 |
| Tier flags misuse | `--unit --integration` together; rejected before any document is read | 2 |
| Zero-survivor misuse | a filter set admits no document (at least one filter given); stderr names the filters | 2 |
| JUnit write failure | `--junit` report cannot be written; stderr message names the path | 2 |
Precedence when classes mix: any parse-error, misuse, or apparatus class ⇒ 2, else any failed endpoint, reply, or scenario verdict ⇒ 1, else 0. The settle timeout, assertion, reply, and scenario verdict failures all surface as a `FAIL` line and count toward `failed`; parse-error, boot, and apparatus failures surface on stderr and do not count toward `passed`/`failed`. A `shutdown-failure` after a recorded verdict reports both and keeps exit 2.
The `intercepts` block in `*.test.yaml` maps source URIs to `skipTo` or `divertCopyTo` `mock:` targets before route load; see [Declarative camel test — Intercepts](../../docs/src/testing/index.md#intercepts) and the [route-interception spec](../../openspec/specs/route-interception/spec.md). The `beans:` block declares stub beans (echo, setBody, fail) for `bean:` steps; see [Declarative camel test — Bean stubs](../../docs/src/testing/index.md#bean-stubs). An input may declare `expectReply` to assert against the reply message the `direct:` producer returns; see [Declarative camel test — Reply assertions](../../docs/src/testing/index.md#reply-assertions). The `repositories:` block registers named `cache`, `idempotent`, and `claimCheck` repositories as in-memory stubs for the run; see [Declarative camel test — Repository stubs](../../docs/src/testing/index.md#repository-stubs). The `env:` block declares string fixture values consulted before inline defaults; see [Declarative camel test — Env fixtures](../../docs/src/testing/index.md#env-fixtures). The `--junit <FILE>`, `--filter-file <GLOB>`, `--filter-endpoint <NAME>`, `--unit`, and `--integration` flags shape a run for CI: the report path, the file glob, the endpoint name filter, and the symmetric tier filters (excluding the opposite tier by derived tier — silently for expanded documents, `tier-filter-collision` for explicitly named ones); the JUnit report carries a `<property name="tier">` row per suite; see [Declarative camel test — CI output and filters](../../docs/src/testing/index.md#ci-output-and-filters).
LEAN tier route loading resolves `${env:}` placeholders against the document `env:` map first, then each token's inline `:-default` (rc-l7m7t; ADR-0069 section 13.1). Route files load through `camel_dsl::load_from_file_with_env` with the document-env lookup, and inline `routes:` through the same `camel_dsl::parse_routes_with_env` seam (`fn load_routes`, `commands/test/runner.rs:166`) — the same parser `camel run` uses, including the 16 MiB cap and path-annotated errors. Typing has typed-probe boot parity (env-int-placeholder-typing): a whole-scalar `:-default` token at a string-valued position resolves to its default, a whole-scalar placeholder on an integer-typed field LOADS through the loader's typed probe (also when the document `env:` map supplies the value), and an unset variable without a default still fails the document naming the variable; boolean-typed positions and non-integer defaults still fail. The process environment is never consulted, so runs stay hermetic.
The unit-tier document identifier fields interpolate `${env:NAME:-default}` against the document `env:` map first, then inline defaults, through the camel-dsl scanner, for name-match parity with the interpolated route sources (rc-l7m7t; rc-4hexo, ADR-0069 section 4). These fields are the `repositories` and `beans` map keys, the `intercepts` source keys and action targets, the `mock:` references in `expects` keys and `sequence` entries, and the `inputs[].to` values. Interpolation runs right after deserialization and before the other document-validation guards, so the scheme, blank-name, and built-in-name checks see the resolved value. Assertion data never interpolates, and the path fields `routeFiles` and `routeFilesFromRoot` never interpolate: input `body` and `headers`, `expectReply` blocks, matcher contents, bean `methods` and `config` values, repository stub targets, and `settle` all keep the literal text. An identifier placeholder without a default fails document validation at exit 2 naming the variable and the field, mirroring the route-side wording; the ambient environment is never consulted, so the run stays hermetic.