CLI reference
The koine CLI is the whole compiler. It has six subcommands — build, check, fmt, init, watch, and lsp — plus --version. Everything you do with Koine, from emitting C# to scaffolding a project, formatting .koi, or gating a CI pipeline on breaking changes, runs through this one tool.
In this repo you invoke it via dotnet run:
dotnet run --project src/Koine.Cli -- <subcommand> [args]Everything after -- is passed to koine. The examples below use koine for brevity; prefix them with dotnet run --project src/Koine.Cli -- when running from source.
Want to try the compiler without installing anything? The browser-based Koine Studio runs the same parser, validator, and emitters as the CLI.
At a glance
Section titled “At a glance”koine --versionkoine build <file.koi|dir> [--target csharp|typescript|python|php|rust|java|glossary|docs|openapi] [--out <dir>] [--glossary <file.md>] [--config <file>]koine watch <file.koi|dir> [--target …] [--out …] [--config <file>] # rebuild on every changekoine fmt <file.koi|dir> [--check] # canonically format .koi (--check: verify only)koine init [dir] [--force] # scaffold a starter projectkoine check <file.koi|dir> --baseline <dir> # flag breaking changes vs a published baselinekoine lsp # Language Server (stdio) for editor diagnosticsRun koine with no arguments (or koine --help) to print this usage.
A path is a file or a directory
Section titled “A path is a file or a directory”Every subcommand that takes a model accepts either a single .koi file or a directory. Given a directory, the CLI reads every .koi file under it recursively, in deterministic (ordinal) order, and compiles them as one model. This is how cross-file imports, the contextmap, and integration events resolve across files — they all see one another in a single pass.
# one filekoine build templates/pizzeria/menu.koi
# the whole context map, compiled togetherkoine build templates/pizzeria --out demo/Pizzeria.Domain/Generatedkoine build
Section titled “koine build”Parses and validates the model, then — if you ask for output — emits files.
| Flag | Default | What it does |
|---|---|---|
| (positional) | — | The .koi file or directory to compile. Required. |
--target | csharp | The emitter: csharp, typescript, python, php, rust, java, glossary, docs, or openapi. |
--layers <list> | domain | Comma-separated layers to emit: domain (the model + application contracts) and/or infrastructure (a runnable realization — EF Core for C#; a dependency-light in-memory realization for TypeScript & Python). infrastructure implies domain. See C# infrastructure layer and TypeScript & Python infrastructure. |
--out <dir> | (none) | Write the emitted files under this directory. Omit to only validate. |
--layers <list> | domain | Comma-separated output layers (domain, application). application implies domain. C# only. See the Application layer. |
--app-mediatr | (off) | Application layer: emit the MediatR request/handler shape + validation/transaction pipeline behaviors instead of plain handlers. |
--app-mapping <mode> | plain | Application layer: DTO/read-model mapping strategy — plain (hand-rolled) or mapperly (reserved). |
--regex-match-timeout-ms <ms> | (config / 1000) | Override the C# matches-invariant ReDoS-guard timeout for this invocation (ms). Wins over targets.csharp.regexMatchTimeoutMs in koine.config. Must be a positive integer. |
--glossary <file.md> | (none) | Also write a Markdown ubiquitous-language glossary to this file. |
--config <file> | (discovered) | Read build defaults from this koine.config instead of the discovered one. See koine.config. |
--target and --out may also be supplied by a koine.config beside the model — an explicit flag always wins. See koine.config below.
Validate only
Section titled “Validate only”With no --out, build parses, validates, and reports diagnostics — but writes nothing. It’s the fastest way to check a model is well-formed:
koine build templates/pizzeria# OK: templates/pizzeria parsed and validatedExit code is 0 when the model is valid, 1 when there is any error diagnostic.
Emit C#
Section titled “Emit C#”Point --out at a directory to write the generated C#:
koine build templates/pizzeria --out demo/Pizzeria.Domain/Generated# wrote 91 files to demo/Pizzeria.Domain/GeneratedThe emitter lays types out by context into namespace-mirroring folders (Menu/, Ordering/Order.cs, …). Before writing, build deletes the top-level folders it owns under --out and regenerates them, so a type you renamed or removed never leaves a stale orphan behind. It only touches the namespace roots it produced this run — hand-written files elsewhere in the directory are left alone.
The C# Application layer
Section titled “The C# Application layer”By default the C# target emits the application contracts only — IUnitOfWork, the I<Service>
use-case interfaces, read-model projections, query objects and IQueryHandler<,> — with no
implementations. Add application to --layers to also emit the layer that fills them in:
koine build templates/pizzeria --target csharp --layers domain,application --out GeneratedPer aggregate command and factory it emits a request record, a handler that loads the
aggregate via its IUnitOfWork repository, invokes the behavior and calls SaveChangesAsync, and a
FluentValidation validator whose rules are rendered from the same invariants the domain enforces.
Per query it emits a concrete IQueryHandler<,> (a single result keyed by the root’s identity
loads + projects via the To<ReadModel> mapper). It also emits an
Add<Context>Application(this IServiceCollection) DI extension registering them all.
Plain handlers (no runtime dependency beyond FluentValidation) are the default. Two opt-in sub-options:
--app-mediatr— emit the MediatR shape (IRequest/IRequest<T>,IRequestHandler<,>, and validation + transactionIPipelineBehavior<,>s) instead of plain handlers.--app-mapping plain|mapperly— mapping strategy (mapperlyreserved for source-generated mapping).
With the layer off (the default), the emitted C# is byte-identical to before. Koine usecase
declarations carry no binding to a specific aggregate behavior, so the generated I<Service>
implementation throws NotImplementedException until wired — the generated command/factory handlers
are the real entry points. The sub-options can also be set in koine.config
(targets.csharp.layers, targets.csharp.application.mediatr, targets.csharp.application.mapping).
C# infrastructure layer (--layers)
Section titled “C# infrastructure layer (--layers)”By default the C# target emits the domain layer: value objects, entities, aggregates, smart enums, events, and the persistence-ignorant application contracts (IRepository, IUnitOfWork). Add --layers domain,infrastructure to also emit a runnable EF Core realization of those contracts, regenerated from the model on every build:
koine build templates/pizzeria --out Generated --layers domain,infrastructurePer bounded context, the infrastructure layer adds (under an Infrastructure/ folder):
- a
<Context>DbContext : DbContextwith oneDbSetper aggregate root; - an
IEntityTypeConfiguration<Root>per aggregate — value objects → owned types (a scalar value object →OwnsOne; a value-object collection →OwnsMany, backed by a mutableList<T>withPropertyAccessMode.Fieldand a surrogate key so it round-trips), theversionedtoken →IsRowVersion(), smart enums →HasConversionvalue converters, strongly-typed IDs → key converters; - a concrete
<Root>Repository : I<Root>Repositoryand aUnitOfWork : IUnitOfWork; - a transactional
OutboxMessagetable + anIntegrationEventDispatcher(only when the context publishes an integration event); - an
Add<Context>Infrastructure(this IServiceCollection, Action<DbContextOptionsBuilder>)DI extension — you supply the database provider, so the emitter stays provider-agnostic.
Every persisted aggregate materializes (insert → query round-trips), not only collection owners: scalar-only roots, roots that own a scalar value object (OwnsOne), versioned aggregates, and nested value objects all round-trip. Each persisted root — and any value object that owns a value object — gets a private parameterless persistence constructor EF Core builds through (an owned navigation can never bind as a constructor parameter), and plain scalar root properties are mapped explicitly so EF persists the read-only auto-property via its backing field.
--layers domain (or omitting the flag) keeps the output byte-identical to before. EF Core is the only backend in v1.
TypeScript & Python infrastructure (--layers)
Section titled “TypeScript & Python infrastructure (--layers)”The same --layers infrastructure selector applies to the TypeScript and Python targets (issue #241). Instead of a bundled ORM, each emits a dependency-light realization of the domain contracts — runnable in tests out of the box, productionized by swapping in a persistent store:
koine build templates/pizzeria --target typescript --out Generated --layers domain,infrastructurekoine build templates/pizzeria --target python --out GeneratedPy --layers domain,infrastructurePer bounded context with at least one entity-rooted aggregate, the infrastructure layer adds (under an infrastructure/ folder):
- a concrete repository over an injectable
AggregateStorewith a zero-dependency in-memory default; declarative finders compile to concrete lookups; - a concrete unit of work realizing the per-context contract;
- a transactional outbox (
OutboxMessage+ a drainableIntegrationEventDispatcher) — only when the context publishes an integration event; - validation + transaction pipeline behaviors (the idiomatic analogue of the C# MediatR decorators);
- a composition-root factory (TypeScript
create<Context>Infrastructure) / provider helper (Pythoncreate_<context>_infrastructure) — the analogue of C#‘sAdd<Context>Infrastructure.
The shared primitives live once in an emitted infrastructure-runtime.ts / koine_infrastructure.py. The layer is off by default, so an unconfigured emit stays byte-identical; the generated TypeScript is tsc --strict-clean and the Python is mypy --strict-clean.
Emit a glossary
Section titled “Emit a glossary”There are two ways to get a Markdown glossary. The --glossary flag writes one to a named file independently of --target/--out, so you can emit C# and a glossary in a single run:
koine build templates/pizzeria --out Generated --glossary pizzeria.glossary.md# wrote glossary to pizzeria.glossary.md# wrote 91 files to GeneratedAlternatively, --target glossary makes the glossary the primary output (paired with --out). Either way the glossary is grouped by context (each heading shows its version), then by type, listing fields, derived fields, and business rules.
Emit an OpenAPI spec
Section titled “Emit an OpenAPI spec”--target openapi projects the model’s API surface as an OpenAPI 3.1 document — one <Context>/openapi.yaml per bounded context, so a multi-context model never collides on a single file:
koine build templates/starters/billing/billing.koi --target openapi --out ./api# wrote 1 files to ./apiThe output is deterministic (stable ordering, no timestamps) and self-contained. The mapping rules:
- Schemas. Value objects, read models, and enums become
components/schemas. Primitives map to their JSON types (Int→integer/int32,Decimal→number,Instant→string/date-time, …); nested value objects / enums / read models become$refs;List/Setbecomearrays (aSetaddsuniqueItems);Mapbecomes an object withadditionalProperties; an optional member becomes an OpenAPI-3.1["…", "null"]type union (a$refis wrapped inanyOf). A smart enum lowers to a stringenumof its member names. ID value objects inline asstring/uuid. - Paths. An entity command becomes a
POSTwhose JSON request body is built from its parameters; a query becomes aGETwhose criteria become queryparametersand whose200response references the result schema. Operation paths are kebab-cased. - Invariants. Static value-object invariants lower, best-effort, to JSON-Schema validation keywords —
code.length <= 12→maxLength,amount >= 1→minimum,matches /…/→pattern. Anything non-static (a guarded or transformed invariant) is silently dropped: the schema is an approximation of the model, never the reverse.
Set KOINE_OPENAPI_VALIDATE (with an OpenAPI validator such as openapi-spec-validator, swagger-cli, or redocly on PATH) to have the test suite validate the emitted documents against the spec; absent the tool the conformance check is skipped.
koine.config
Section titled “koine.config”A koine.config file supplies defaults for the build/watch flags so you don’t repeat --target/--out on every invocation. It is a tiny key = value format — one pair per line, # starts a comment:
# koine.config — build defaults for this domain model.target = csharpout = generatedDiscovery. Unless you pass --config <file> explicitly, build and watch look for a file literally named koine.config, in order:
- the directory of the input path (or the input directory itself, when you point the command at a folder), then
- the current working directory.
The first one found wins; if none is found, no defaults are applied. A flag on the command line always overrides the config.
Keys read today. The flat keys target (csharp, typescript, python, php, rust, java, glossary, docs, or openapi) and out (the output directory) are honoured, plus these per-target keys:
targets.csharp.layers(e.g.domain,infrastructure) — the config equivalent of--layers, overridden by an explicit--layersflag.targets.<target>.regexMatchTimeoutMs(default1000for C#) — the per-call match-timeout budget in milliseconds for thematches-invariant ReDoS guard: set it tighter for hostile-input value objects or looser for an expensive pattern on trusted batch input. The neutral key reaches every code target, each honoring it as its runtime allows — C# and Python honor it as a real per-call timeout, TypeScript plumbs it into the advisoryregexMatchseam, PHP substitutes the PCRE-limit note (see the per-target table). It must be a positive integer —0or a negative value is rejected at build time. For a single-invocation override without editing the config, use the--regex-match-timeout-ms <ms>flag — the flag wins over this key.targets.csharp.regexMode(inline|sourceGenerated, defaultinline) — selects how amatches-invariant regex guard is evaluated in emitted C#. The default (inline) emits the boundedRegex.IsMatch(…)call, byte-identical to the historical output.sourceGeneratedopts in to the cached, allocation-free[GeneratedRegex]partial-method form (the .NET source generator), which compiles the pattern once ahead of time. Both modes carry the same pattern,RegexOptions.None, and match-timeout, so match behaviour is identical; only the evaluation strategy differs. Requires C# 11+/.NET 7+ (Koine targetsnet10.0, so it is always satisfied). An unrecognised value (e.g. a typo likesourcegen) is rejected at build time.
Every other key is silently ignored, which keeps the file forward-compatible — older tooling tolerates a newer config.
koine fmt
Section titled “koine fmt”Canonically formats .koi source. The formatter is a deterministic, idempotent token-stream reprinter: running it twice produces the same bytes as running it once. Point it at a file or a directory (every .koi underneath is formatted).
koine fmt templates/pizzeria# formatted templates/pizzeria/menu.koi# formatted 1 of 7 file(s)| Flag | Default | What it does |
|---|---|---|
| (positional) | — | The .koi file or directory to format. Required. |
--check | off | Verify only. Writes nothing; reports each file that is not already formatted and exits non-zero if any need formatting. Use it in CI. |
Without --check, fmt rewrites each unformatted file in place and prints what it changed; files already in canonical form are left untouched (OK: N file(s) already formatted).
With --check, nothing is written. Each unformatted file is reported to stderr (path: not formatted); if any file would change, it prints an error: N file(s) need formatting message (telling you to run koine fmt) and exits 1. A clean tree prints OK: N file(s) already formatted and exits 0 — drop it straight into a CI step.
Exit codes
Section titled “Exit codes”| Exit | Meaning |
|---|---|
0 | Files formatted (or all already formatted; with --check, all already formatted). |
1 | Path not found, no .koi files found, a usage error — or, with --check, at least one file needs formatting. |
koine init
Section titled “koine init”Scaffolds a starter Koine project: a domain.koi model, a koine.config with build defaults, and a README.md. The scaffold builds end-to-end out of the box.
koine init my-domain# initialized koine project in my-domain# domain.koi starter model# koine.config build defaults# README.md project notes# next: koine build my-domain/domain.koi| Flag | Default | What it does |
|---|---|---|
| (positional) | . | The target directory (created if missing). Defaults to the current directory. |
--force | off | Overwrite existing scaffold files. Without it, init refuses if any of domain.koi, koine.config, or README.md already exist. |
Without --force, init will not clobber your work: if any scaffold file already exists it prints error: refusing to overwrite existing file(s): … (use --force) and exits 1.
Exit codes
Section titled “Exit codes”| Exit | Meaning |
|---|---|
0 | Project scaffolded. |
1 | A scaffold file already exists and --force was not given, or a usage error. |
koine watch
Section titled “koine watch”Like build, but it stays running and re-emits on every change. It watches the input’s directory (recursively) for *.koi edits, debounces a burst of saves, and runs a fresh build for each batch — fast feedback while you model. It accepts the same flags as build (--target, --out, --config); --out makes it re-emit C# on save, while omitting it gives you continuous validation.
koine watch templates/pizzeria --out demo/Pizzeria.Domain/Generated# watching templates/pizzeria for *.koi changes — press Ctrl+C to stopPress Ctrl+C to stop; watch unwinds cleanly and exits 0. Like build, --target/--out fall back to a discovered (or --config) koine.config when omitted.
Exit codes
Section titled “Exit codes”| Exit | Meaning |
|---|---|
0 | Stopped cleanly with Ctrl+C. |
1 | A usage error before the watch loop starts (e.g. missing path, unknown option). |
A failing build within a watch session prints its diagnostics but keeps watching — watch doesn’t exit on a per-build error, so you fix and save again.
koine check
Section titled “koine check”Backward-compatibility gate. It compiles a current model and a baseline model, then reports every change to a published surface — and fails if any change is breaking. This is what you run in CI to stop a breaking change from shipping.
koine check <current-file-or-dir> --baseline <baseline-dir>| Flag | Required | What it does |
|---|---|---|
| (positional) | yes | The current model (file or dir). |
--baseline <dir> | yes | The previously published model to compare against. |
“Published surfaces” means only the parts of the language other teams depend on: integration events, shared-kernel types, and open-host value objects. Internal refactors — renaming a private field, restructuring an aggregate’s commands — are invisible to check. See Versioning & evolution for what counts as published and which changes are breaking.
A worked example
Section titled “A worked example”A complete before/after lives under examples/versioning/. The v2 contract adds an optional field (fine) and removes a published field (breaking):
context Sales version 2 {
integration event OrderPlaced { orderId: OrderId total: Decimal // coupon removed -> BREAKING (PublishedFieldRemoved) @since(2) note: String? // added optional field -> backward-compatible }
publishes OrderPlaced}koine check examples/versioning/v2 --baseline examples/versioning/v1# breaking KOI1511: field 'coupon' of published integration event 'OrderPlaced' was removed.# non-breaking: field 'note' of published integration event 'OrderPlaced' was added.# error: 1 breaking change(s) to published surfacesBreaking changes print to stderr prefixed breaking <CODE>:; non-breaking changes print to stdout prefixed non-breaking:. The breaking-change codes live in the KOI15xx range (PublishedTypeRemoved, PublishedFieldRemoved, PublishedFieldTypeChanged, PublishedFieldNowRequired, PublishedRequiredFieldAdded).
Exit codes
Section titled “Exit codes”| Exit | Meaning |
|---|---|
0 | No breaking changes. Prints OK: no breaking changes to published surfaces. |
1 | At least one breaking change — or either model failed to parse. |
Because a parse failure in either model also exits 1, a green koine check proves both models compiled and the evolution is safe. Drop it straight into a CI step.
koine lsp
Section titled “koine lsp”Starts the Koine Language Server over stdio. It powers in-editor diagnostics, hover, and go-to-definition (including cross-file workspace navigation). You don’t run it by hand — your editor’s Koine extension launches koine lsp and speaks LSP to it over stdin/stdout. See the editor tooling guide.
koine lsp # blocks, reading LSP messages on stdinkoine --version
Section titled “koine --version”Prints the compiler version and exits 0. -v is an alias.
koine --version# 1.0.0.0Diagnostics
Section titled “Diagnostics”Every diagnostic build (and the parse step of check) emits uses a single, MSBuild- and Roslyn-parseable format:
file:line:col: severity KOIxxxx: messageFor example, a typo’d type name:
koine build templates/pizzeria# menu.koi:14:18: error KOI0101: unknown type 'Currancy' — did you mean 'Currency'?Three things make these diagnostics worth reading:
- Stable codes. Every diagnostic carries a
KOIxxxxcode that never changes and is never reused, so you can search, suppress, or document against it. Codes are grouped by area —KOI00xxsyntax,KOI01xxdeclarations,KOI02xxexpressions,KOI12xxapplication layer,KOI15xxversioning, and so on. - Multi-error recovery. The compiler doesn’t stop at the first error. It recovers and reports as many independent problems as it can in one run, so you fix a batch per build instead of one-at-a-time.
- “Did you mean…?” When a name doesn’t resolve — an unknown type, field, member, or enum case — the compiler suggests the nearest declared name (
— did you mean 'Currency'?) when there’s a close match.
Diagnostics go to stderr; success messages (OK: …, wrote N files to …) go to stdout. Any error diagnostic makes build exit 1.
Exit codes, summarised
Section titled “Exit codes, summarised”| Code | When |
|---|---|
0 | Success — model valid, files written, formatted (or already formatted), project scaffolded, no breaking changes, or watch stopped cleanly. |
1 | Usage error, file/path not found, a parse/validation error, an unknown command, a breaking change from check, a --check formatting failure, or init refusing to overwrite. |
See also
Section titled “See also”- Installation — building the CLI and wiring
koine buildinto a.csprojvia MSBuild. - Versioning & evolution —
version,@since, and whatkoine checkenforces. - Reading the generated C# — what
koine buildemits, file by file. - Editor tooling — what
koine lsppowers.