Skip to content

CLI

@bounda-dev/cli installs the bounda command.

Terminal window
pnpm add -D @bounda-dev/cli typescript

TypeScript 7 or newer is an optional peer: the generator needs it only to infer the state of aggregates without state.ts.

Reads the project layout and writes .bounda/registry.ts, .bounda/register.d.ts, .bounda/types.ts and one +types/<name>.ts next to every module. Files whose content did not change are left alone; +types files whose module is gone are removed.

Terminal window
bounda generate
bounda generate --watch
bounda generate --root ./apps/shop --app-dir src --no-infer
Option Default Meaning
--root <dir> current directory The project root: where .bounda/ goes and tsconfig.json is looked for
--app-dir <dir> app The application directory under the root, with domain/ and read/
--tsconfig <file> <root>/tsconfig.json The TypeScript project used to infer state
--no-infer Do not start TypeScript; aggregates without state.ts get UnknownState
--watch Regenerate after each burst of changes under the application directory

Output lists every file written or removed, then a summary:

written .bounda/registry.ts
written app/domain/order/+types/order-placed.ts
1 aggregate, 1 read model, 10 files (2 written, 8 unchanged, 0 removed)

Warnings from state inference go to stderr and do not change the exit code:

warning: order: field "cancellation" (set by orderCancelled) has a type that is not visible
from .bounda/types.ts (Cannot find name 'Cancellation'.); it is typed as unknown. Export the type
or add state.ts
Code Meaning
0 Generated, possibly with warnings
1 The layout breaks a convention. Every problem is listed with its path; nothing is written
2 Something else failed, such as a file where .bounda/ should be

--watch regenerates after each burst of changes, 100 ms after the last one, and ignores changes to +types directories. A run that fails prints its problems and watching goes on. Ctrl-C ends it.

For an aggregate without state.ts, the generator writes a first pass in which the state is UnknownState, opens the project with TypeScript, reads the return type of every event’s apply and unions the fields it finds. Each field is optional, since a fresh aggregate has none:

export type OrderState = {
readonly customerId?: string;
readonly lines?: readonly import("../app/domain/order/order-placed.ts").Line[];
readonly status?: "cancelled" | "paid" | "placed";
};

Types exported from your modules are referenced through import(...). A type that is not exported cannot be named from .bounda/types.ts; that field becomes unknown and a warning says which field and which events set it. Export the type, or add state.ts, to fix it. Without TypeScript installed, or when it cannot open the project, the state stays UnknownState and the warning says so.

Everything the command does is exported from @bounda-dev/cli:

import { generate } from "@bounda-dev/cli";
const report = await generate({ root: process.cwd() });
report.written; // absolute paths written this run
report.warnings; // inference warnings, per aggregate

discoverProject, emitProject, inferStates, watchProject and runCli are the pieces generate and the binary are made of.