Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/tidy-eagles-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@cartesi/cli": minor
---

Add a `defineConfig` function, and configure applications from
`cartesi.config.ts` instead of `cartesi.toml`.

An application is now configured by a `cartesi.config.ts` file that exports its
configuration through `defineConfig`, imported from `@cartesi/cli/config`. The
helper does nothing at runtime, and exists so the configuration is type checked
and completed by the editor. A configuration file may also export a function,
which receives the command being run and the mode, so a project can configure
itself differently for `cartesi build` and `cartesi run`.

Applications not written in TypeScript or JavaScript describe the same
configuration as plain data, in a `cartesi.config.json`, `cartesi.config.yaml`
or `cartesi.config` file.

The configuration gained a `run` section with the project defaults of the local
development environment (`epochLength`, `services`, `blockTime`, `forkUrl`,
...), so they do not have to be repeated on every `cartesi run`. Command line
options take precedence over it.

Nvrams, the accounts drive markers, the `withdrawal` overrides and the `prt`
nodes are configured the same way, with camelCase keys. As in the TOML parser, a key that
no section reads is an error, so a misspelled option is reported rather than
silently ignored.

`cartesi.toml` keeps working, and is read when a project has no other
configuration file, but it is deprecated and now prints a warning.

`resolveConfig` is now asynchronous, since a configuration file has to be
imported, and the `config` option of the API functions accepts a configuration
written inline, in the same shape a `cartesi.config.ts` file exports.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The CLI build pipeline (`apps/cli`): `clean` → `codegen` (wagmi ABI generation
- **`builder/`** — Drive builder implementations (directory, docker, tar, empty, none). Each builder produces ext2 or SquashFS filesystems for Cartesi Machine drives.
- **`compose/`** — Docker Compose service definitions generated as TypeScript objects (anvil, node, bundler, database, paymaster, proxy, explorer, etc.).
- **`exec/`** — Wrappers around subprocess execution (cartesi-machine, rollups) using `execa`.
- **`config.ts`** — Parses `cartesi.toml` (TOML-based project config) into typed `Config` objects. Defines drive configs, machine configs, and SDK versions.
- **`config.ts`, `config/`** — `config.ts` is the barrel the rest of the CLI imports; `config/` holds the types, defaults, errors and the parsers. An application is configured by a `cartesi.config.ts` file exporting `defineConfig({ ... })`, or by the same configuration as plain data in `cartesi.config.json`, `cartesi.config.yaml` or `cartesi.config`; `config/load.ts` finds and reads it, `config/normalize.ts` validates it, and `config/toml/` parses the deprecated `cartesi.toml`.
- **`contracts.ts`** — Generated contract addresses and ABI bindings (via `@wagmi/cli`).
- **`wallet.ts`** — Wallet utilities using `viem` for Ethereum interaction.

Expand Down
133 changes: 128 additions & 5 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,121 @@ cartesi --help

More documentation at [https://docs.cartesi.io](https://docs.cartesi.io).

## Configuration

An application is configured by a `cartesi.config.ts` file at its root, which
exports its configuration through `defineConfig`:

```ts
import { defineConfig } from "@cartesi/cli/config";

export default defineConfig({
drives: {
root: { builder: "docker", dockerfile: "Dockerfile" },
data: { builder: "empty", size: "64Mi", mount: "/mnt/data" },
},
machine: {
entrypoint: "dapp",
ramLength: "256Mi",
},
run: {
epochLength: 10,
services: ["explorer"],
},
});
```

`defineConfig` does nothing at runtime: it exists so the configuration is type
checked and completed by the editor, without any annotation. A configuration
file is free to compute its configuration, and to export a function instead of
an object when it depends on what is being run:

```ts
export default defineConfig(async ({ command, mode }) => ({
machine: { envFile: `.env.${mode}` },
run: { epochLength: command === "run" ? 10 : 720 },
}));
```

`mode` comes from `CARTESI_ENV` (or `NODE_ENV`), and defaults to
`development`.

The `run` section holds the defaults of the local development environment
started by `cartesi run`, so a project does not have to repeat the same
options on every invocation. Command line options always take precedence over
it.

Nvrams, the drive or nvram that holds the accounts of emergency withdrawal,
and the PRT nodes of `cartesi run --prt`, are configured the same way:

```ts
export default defineConfig({
nvrams: {
accounts: { accountsDrive: true, size: "4Ki" },
state: { size: "8Ki", shared: true },
},
withdrawal: { account_size: 64 },
prt: {
nodes: {
first: { type: "sling", sentry: true },
second: { type: "sling", sentry: true },
},
},
});
```

An application has no obligation to have a configuration file: without one, it
is built from a `Dockerfile` of its directory, which is what most applications
need.

### Applications not written in TypeScript

The very same configuration can be written as plain data, for applications not
written in TypeScript or JavaScript. The keys are the ones above, plus
`$schema`, which points an editor at a schema and is otherwise ignored:

```yaml
# cartesi.config.yaml
machine:
entrypoint: dapp
ramLength: 256Mi
drives:
data:
builder: empty
size: 64Mi
mount: /mnt/data
run:
epochLength: 10
```

The configuration file of a project is the first of these that exists:

| File | Format |
| ---------------------------------------------------------- | ---------------------------------------- |
| `cartesi.config.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs` | module exporting `defineConfig({ ... })` |
| `cartesi.config.json` | JSON |
| `cartesi.config.yaml`, `cartesi.config.yml` | YAML |
| `cartesi.config` | YAML, which accepts JSON as well |
| `cartesi.toml` | deprecated |

TypeScript configuration files are read by the runtime itself, so there is no
build step and no extra dependency: the standalone binaries run on Bun, and
node strips the types from version 24, which this package requires.

A key that no section reads is an error rather than a typo that silently does
nothing, in every format, which is why a `cartesi.config.json` may carry
`$schema` but nothing else the CLI does not know.

### Migrating from `cartesi.toml`

`cartesi.toml` still works, and is read when a project has no other
configuration file, but it is deprecated and prints a warning. The new formats
describe the same configuration, with one difference: keys are camelCase, as
the resolved configuration already is, so `extra_size` becomes `extraSize`,
`boot_args` becomes `bootargs` and `accounts_drive` becomes `accountsDrive`.
The keys of the `withdrawal` section keep the names the `cartesi-rollups-cli`
expects (`account_size`, `withdrawal_output_builder`).

## Library

Every command of the CLI is also available as a function, so applications can be
Expand Down Expand Up @@ -57,9 +172,15 @@ The following functions are available: `addressBook`, `build`, `clean`,
A few things to keep in mind:

- functions operate on the current working directory, just like the CLI, and
read `cartesi.toml` from it by default. Functions that take a configuration
accept a path, a list of paths (merged in order), or an already parsed
`Config` object;
look the configuration file of the project up in it by default. Functions
that take a configuration accept a path, a list of paths (merged in order),
or the configuration written inline, in the same shape a `cartesi.config.ts`
file exports:

```ts
await build({ config: { machine: { ramLength: "256Mi" } } });
await build({ config: "cartesi.config.production.ts" });
```
- functions are silent, and never write to the terminal. Pass
`progress: "default"` (or `"verbose"`) to get the same output as the CLI;
- functions throw on error, and never terminate the process;
Expand All @@ -73,5 +194,7 @@ A few things to keep in mind:
`DepositError` (`InsufficientBalanceError`, `InvalidAmountError` or
`TokenNotFoundError`).

The package is typed, and the types of the configuration file (`Config`,
`DriveConfig`, `MachineConfig`, ...) are exported as well.
The package is typed, and the types of the configuration file (`UserConfig`,
`Config`, `DriveConfig`, `MachineConfig`, `RunConfig`, ...) are exported as
well, along with `defineConfig`, `loadConfig`, `findConfigFile`,
`normalizeConfig` and `mergeConfig`.
2 changes: 1 addition & 1 deletion apps/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ await Bun.build({

await Bun.build({
define,
entrypoints: ["./src/lib.ts"],
entrypoints: ["./src/lib.ts", "./src/defineConfig.ts"],
minify: true,
outdir: "dist",
sourcemap: true,
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"types": "./dist/types/lib.d.ts",
"default": "./dist/lib.js"
},
"./config": {
"types": "./dist/types/defineConfig.d.ts",
"default": "./dist/defineConfig.js"
},
"./package.json": "./package.json"
},
"repository": "cartesi/cli",
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/api/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ export const build = async (
): Promise<BuildResult> => {
const { debug = false, drivesOnly = false, progress = "silent" } = options;

// get application configuration, from a Config object or 'cartesi.toml'
const config = resolveConfig(options.config);
// get application configuration, written inline or read from a file
const config = await resolveConfig(options.config, "build");

// destination directory for image and intermediate files
const destination = path.resolve(getContextPath());
Expand Down
Loading
Loading