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 and the `withdrawal` overrides 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.
5 changes: 5 additions & 0 deletions .changeset/tidy-moons-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": minor
---

expose the deposit commands as library functions: `depositEther`, `depositErc20`, `depositErc721`, `depositErc1155` and `depositErc1155Batch`
5 changes: 5 additions & 0 deletions .changeset/wild-crabs-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": minor
---

expose the CLI as a library, so `build`, `run`, `hash` and the other commands can be called programmatically from a script: `import { build } from "@cartesi/cli"`
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
175 changes: 175 additions & 0 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,178 @@ 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, and the drive or nvram that holds the accounts of emergency
withdrawal, are configured the same way:

```ts
export default defineConfig({
nvrams: {
accounts: { accountsDrive: true, size: "4Ki" },
state: { size: "8Ki", shared: true },
},
withdrawal: { account_size: 64 },
});
```

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
built, run and inspected from a script, without going through the command line:

```shell
npm install @cartesi/cli
```

```ts
import { build, depositErc20, hash, run, send } from "@cartesi/cli";

// build the application, same as `cartesi build`
await build();

// read the template hash of the machine snapshot, same as `cartesi hash`
console.log(await hash());

// start a local node and deploy the application to it, same as `cartesi run`
const node = await run({ epochLength: 10 });
console.log(`running at ${node.url}, deployed at ${node.deployment?.address}`);

// send an input to the application, same as `cartesi send`
await send({ input: "hello" });

// deposit tokens to the application, same as `cartesi deposit erc20`
await depositErc20({ amount: "1.5" });

await node.stop();
```

The following functions are available: `addressBook`, `build`, `clean`,
`create`, `depositErc20`, `depositErc721`, `depositErc1155`,
`depositErc1155Batch`, `depositEther`, `doctor`, `hash`, `logs`, `run`, `send`,
`shell` and `status`.

A few things to keep in mind:

- functions operate on the current working directory, just like the CLI, and
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;
- `run` returns a handle of the environment, which keeps running in the
background until `stop()` is called. Use `deploy()` to redeploy the
application after a rebuild;
- `send` and the `deposit*` functions resolve the application address, the
sender and the RPC URL from the running project, and never prompt for them.
Amounts are given in the base unit of the asset as a `bigint`, or in its
display unit as a string (`"1.5"`). A deposit that cannot be made throws a
`DepositError` (`InsufficientBalanceError`, `InvalidAmountError` or
`TokenNotFoundError`).

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`.
11 changes: 10 additions & 1 deletion apps/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const define = {
),
};

// build for npm package
// build for npm package: the CLI entrypoint (executable) and the library entrypoint
await Bun.build({
banner: "#!/usr/bin/env node",
define,
Expand All @@ -18,6 +18,15 @@ await Bun.build({
target: "node",
});

await Bun.build({
define,
entrypoints: ["./src/lib.ts", "./src/defineConfig.ts"],
minify: true,
outdir: "dist",
sourcemap: true,
target: "node",
});

// build bun binaries for all supported platforms
const targets: Bun.Build.CompileTarget[] = [
"bun-darwin-arm64",
Expand Down
28 changes: 22 additions & 6 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,28 @@
"type": "module",
"homepage": "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/cartesi/cli",
"license": "Apache-2.0",
"main": "./dist/lib.js",
"exports": {
".": {
"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",
"files": [
"/dist"
],
"dependencies": {
"execa": "^10.0.1",
"semver": "^7.8.5",
"smol-toml": "^1.9.0",
"viem": "^2.57.3"
},
"devDependencies": {
"@cartesi/wagmi-plugin": "1.0.0-alpha.7",
"@commander-js/extra-typings": "^15.0.0",
Expand All @@ -29,7 +47,6 @@
"cli-table3": "^0.6.5",
"commander": "^15.0.0",
"dotenv": "^18.0.5",
"execa": "^10.0.1",
"fs-extra": "^11.4.1",
"get-port": "^7.2.0",
"listr2": "^11.1.1",
Expand All @@ -38,18 +55,16 @@
"ora": "^9.4.1",
"p-retry": "^8.0.1",
"rimraf": "^6.1.3",
"semver": "^7.8.5",
"smol-toml": "^1.9.0",
"typescript": "^7.0.2",
"viem": "^2.57.3",
"yaml": "^2.9.1"
},
"scripts": {
"build": "run-s clean codegen compile",
"build": "run-s clean codegen compile types",
"clean": "rimraf dist",
"codegen": "run-p codegen:wagmi",
"codegen:wagmi": "wagmi generate",
"compile": "bun build.ts",
"types": "tsc -p tsconfig.build.json",
"lint": "biome lint",
"posttest": "bun lint",
"test": "bun test",
Expand All @@ -59,5 +74,6 @@
"engines": {
"node": ">=24.0.0"
},
"bugs": "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/cartesi/cli/issues"
"bugs": "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/cartesi/cli/issues",
"types": "./dist/types/lib.d.ts"
}
24 changes: 24 additions & 0 deletions apps/cli/src/api/address-book.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type AddressBook, getAddressBook, getProjectName } from "../base.js";

export type AddressBookOptions = {
/**
* Name of the project (used by docker compose and cartesi-rollups-node).
* @default basename of the current working directory
*/
projectName?: string;
};

/**
* Get the addresses of all smart contracts deployed to the runtime environment
* of the application, indexed by contract name.
* @param options address book options
* @returns map of contract name to contract address
*/
export const addressBook = async (
options: AddressBookOptions = {},
): Promise<AddressBook> => {
const projectName = getProjectName(options);
return getAddressBook({ projectName });
};

export type { AddressBook };
Loading
Loading