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
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"`
58 changes: 58 additions & 0 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,61 @@ cartesi --help
```

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

## 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
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;
- 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 (`Config`,
`DriveConfig`, `MachineConfig`, ...) are exported as well.
10 changes: 9 additions & 1 deletion apps/cli/build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// build for npm package
// build for npm package: the CLI entrypoint (executable) and the library entrypoint
await Bun.build({
banner: "#!/usr/bin/env node",
entrypoints: ["./src/index.ts"],
Expand All @@ -8,6 +8,14 @@ await Bun.build({
target: "node",
});

await Bun.build({
entrypoints: ["./src/lib.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
14 changes: 11 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
"type": "module",
"homepage": "https://github.com/cartesi/cli",
"license": "Apache-2.0",
"exports": "./dist/index.js",
"main": "./dist/lib.js",
"exports": {
".": {
"types": "./dist/types/lib.d.ts",
"default": "./dist/lib.js"
},
"./package.json": "./package.json"
},
"repository": "cartesi/cli",
"files": [
"/dist"
Expand Down Expand Up @@ -62,11 +69,12 @@
"typescript": "^5.9.2"
},
"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 @@ -75,5 +83,5 @@
"node": ">=20.0.0"
},
"bugs": "https://github.com/cartesi/cli/issues",
"types": "dist/index.d.ts"
"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 };
195 changes: 195 additions & 0 deletions apps/cli/src/api/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import chalk from "chalk";
import fs from "fs-extra";
import { Listr, type ListrTask } from "listr2";
import path from "node:path";
import tmp from "tmp";
import { getContextPath } from "../base.js";
import {
buildDirectory,
buildDocker,
buildEmpty,
buildNone,
buildTar,
} from "../builder/index.js";
import type { Config, DriveConfig, ImageInfo } from "../config.js";
import { bootMachine } from "../machine.js";
import {
type ConfigOptions,
listrRenderer,
type ProgressOptions,
resolveConfig,
} from "./types.js";

export type BuildOptions = ConfigOptions &
ProgressOptions & {
/**
* Keep intermediate files, used for debugging.
* @default false
*/
debug?: boolean;

/**
* Only build the drives, do not boot the machine.
* @default false
*/
drivesOnly?: boolean;
};

export type BuildResult = {
/** Application configuration used by the build. */
config: Config;

/** Directory where the drives and the machine snapshot were written to. */
destination: string;

/** Information of the docker image used to build the root drive, if any. */
imageInfo?: ImageInfo;
};

// context for Listr build tasks
type BuildContext = {
config: Config;
debug: boolean;
destination: string;
imageInfo?: ImageInfo;
};

const buildDriveTask = (
name: string,
drive: DriveConfig,
): ListrTask<BuildContext> => ({
title: `Building drive ${chalk.cyan(name)}`,
task: async (ctx, task) => {
const { config, debug, destination } = ctx;
const sdk = config.sdk;
const reporter = (line: string) => {
task.output = line;
};

switch (drive.builder) {
case "directory": {
await buildDirectory(
name,
drive,
sdk,
destination,
debug,
reporter,
);
break;
}
case "docker": {
const imageInfo = await buildDocker(
name,
drive,
sdk,
destination,
debug,
reporter,
);
if (imageInfo && name === "root") {
// only set image info for root drive
ctx.imageInfo = imageInfo;
}
break;
}
case "empty": {
await buildEmpty(name, drive, sdk, destination);
break;
}
case "tar": {
await buildTar(name, drive, sdk, destination, reporter);
break;
}
case "none": {
await buildNone(name, drive, destination);
break;
}
}
task.title = `Build drive ${chalk.cyan(name)}`;
},
});

/**
* Build the application, by building the Cartesi machine drives, configuring a
* machine and booting it, so a machine snapshot is created at `.cartesi/image`.
*
* Drives are built relative to the current working directory, so are the
* configuration files, if provided as paths.
*
* @param options build options
* @returns configuration used and location of the build artifacts
*/
export const build = async (
options: BuildOptions = {},
): Promise<BuildResult> => {
const { debug = false, drivesOnly = false, progress = "silent" } = options;

// clean up temp files we create along the process
tmp.setGracefulCleanup();

// get application configuration, from a Config object or 'cartesi.toml'
const config = resolveConfig(options.config);

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

// prepare context directory
await fs.emptyDir(destination); // XXX: make it less error prone

// build context
const ctx: BuildContext = {
config,
debug,
destination,
imageInfo: undefined,
};

// tasks to build drives
const driveTasks = Object.entries(config.drives).map(([name, drive]) =>
buildDriveTask(name, drive),
);

const builds = new Listr(
[
{
title: "Build drives",
task: async (_ctx, task) => {
return task.newListr(driveTasks, {
concurrent: true,
rendererOptions: {
collapseSubtasks: false,
},
ctx,
});
},
},
],
{ ctx, renderer: listrRenderer(progress) },
);
const result = await builds.run();

// if only build drives, quit here
if (drivesOnly) {
return { config, destination, imageInfo: result.imageInfo };
}

// create machine snapshot
await bootMachine(
config,
result.imageInfo,
{
finalHash: true,
store: "image",
},
{
cwd: destination,
stdio: progress === "silent" ? "pipe" : "inherit",
},
);

// make snapshot readable by all users, because cartesi-machine sets to 600
await fs.chmod(path.join(destination, "image"), 0o755);

return { config, destination, imageInfo: result.imageInfo };
};
10 changes: 10 additions & 0 deletions apps/cli/src/api/clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import fs from "fs-extra";
import { getContextPath } from "../base.js";

/**
* Delete all cached build artifacts of the application, by emptying the
* `.cartesi` directory of the current working directory.
*/
export const clean = async (): Promise<void> => {
await fs.emptyDir(getContextPath());
};
Loading
Loading