Skip to content
Open
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
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
*.local
package-lock.json
.pnp.*
**/.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Nested yarn state (e.g. `examples/<name>/.yarn/cache`) shouldn't ship.
# Examples install standalone; their cache regenerates from yarn.lock.
**/.yarn/cache
**/.yarn/install-state.gz
**/.yarn/build-state.yml
**/.yarn/unplugged

logs
log
Expand All @@ -22,10 +29,34 @@ result
dist/
gen/
managed/
# Compiler output. Regenerated by `compact-compiler` on every build, so it
# never gets committed — including under examples/, where the walkthrough
# expects you to compile the contract yourself before deploying.
artifacts/
midnight-level-db
compactc

# Deploy secrets — wallet seeds, signing keys, keystores. Match at any depth
# so nested deploy/ directories (e.g. under examples/) are covered too.
**/deploy/*.seed
**/deploy/*.signingkey
**/deploy/*.keystore.json

# Deployment records — the JSON the deployer writes after a successful
# deploy. Includes the contract signing key, so treat as a secret.
deployments/

# compact-deployer wallet-state cache (per-seed, per-network shielded snapshots).
.states/
**/.states/

# Third-party source pulled in for local experimentation (e.g. the
# midnight-node fork validation under vendor/midnight-node/ — see
# plans/tooling/compact-deploy-rust-fork.md). Never committed.
vendor/
target/
.toolkit-cache/

coverage
**/reports

Expand Down
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"scripts": {
"build": "turbo run build --log-prefix=none",
"test": "turbo run test --log-prefix=none",
"coverage": "turbo run coverage --log-prefix=none",
"lint": "biome check .",
"lint:fix": "biome check . --write",
"lint:ci": "biome ci . --no-errors-on-unmatched",
Expand All @@ -17,10 +18,20 @@
},
"devDependencies": {
"@biomejs/biome": "2.5.5",
"@openzeppelin/compact-deployer": "workspace:^",
"@types/node": "26.1.2",
"@vitest/coverage-v8": "4.1.9",
"pino": "^9.7.0",
"ts-node": "^10.9.2",
"turbo": "^2.9.18",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
},
"resolutions": {
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-protocol": "4.1.0",
"undici": "^6.24.0",
"glob": "^11.0.0",
"uuid": "^13.0.0"
}
}
30 changes: 22 additions & 8 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# @openzeppelin/compact-cli

CLI wrapper around [`@openzeppelin/compact-builder`](../builder).
Provides the `compact-compiler` and `compact-builder` binaries for use in
`package.json` scripts. Contains no programmatic API of its own. If you want
to call the compiler/builder from TypeScript, use the library package directly.
CLI wrapper around [`@openzeppelin/compact-builder`](../builder) and
[`@openzeppelin/compact-deployer`](../deployer). Provides the
`compact-compiler`, `compact-builder`, and `compact-deploy` binaries for use
in `package.json` scripts. Contains no programmatic API of its own. If you
want to call the compiler, builder, or deployer from TypeScript, use the
library package directly.

## Install

Expand All @@ -16,6 +18,7 @@ yarn add --dev @openzeppelin/compact-cli
```bash
yarn compact-compiler --help
yarn compact-builder --help
yarn compact-deploy --help
```

Typical `package.json` scripts (replace `<version>` with the Compact
Expand All @@ -34,9 +37,20 @@ toolchain release you want to pin, e.g. `+0.29.0`):

## Options

Both binaries accept the same compiler-side options (forwarded to the
underlying library); `compact-builder` additionally accepts dist-layout
options:
### `compact-deploy`

Deploys a compiled contract to a Midnight network. Options are documented
in full under [`@openzeppelin/compact-deployer`](../deployer); the common
ones are `--network`, `--config`, `--seed-file`, `--dry-run`, and `--json`.

```bash
compact-deploy <Contract> --network local
```

### `compact-compiler` and `compact-builder`

Both accept the same compiler-side options (forwarded to the underlying
library); `compact-builder` additionally accepts dist-layout options:

| Flag | Applies to | Description |
|---|---|---|
Expand All @@ -55,7 +69,7 @@ documentation, programmatic API, and behavioural details.

## Requirements

- Node.js >= 20
- Node.js >= 24 for `compact-deploy`, which uses explicit resource management (`await using` / `AsyncDisposableStack`), global only from Node 24. The compiler and builder binaries run on older releases.
- Midnight Compact toolchain installed and available in `PATH`

```bash
Expand Down
19 changes: 14 additions & 5 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "@openzeppelin/compact-cli",
"description": "CLI for compiling and building Compact smart contracts",
"description": "CLI for compiling, building, and deploying Compact smart contracts",
"version": "0.0.2",
"keywords": [
"compact",
"cli",
"compiler",
"builder",
"deployer",
"testing"
],
"author": "OpenZeppelin Community <maintainers@openzeppelin.org>",
Expand All @@ -19,35 +20,43 @@
"type": "module",
"exports": {
"./run-builder": "./dist/runBuilder.js",
"./run-compiler": "./dist/runCompiler.js"
"./run-compiler": "./dist/runCompiler.js",
"./run-deploy": "./dist/runDeploy.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=20"
"node": ">=24"
},
"bin": {
"compact-builder": "dist/runBuilder.js",
"compact-compiler": "dist/runCompiler.js"
"compact-compiler": "dist/runCompiler.js",
"compact-deploy": "dist/runDeploy.js"
},
"scripts": {
"build": "tsc -p .",
"types": "tsc -p tsconfig.json --noEmit",
"test": "yarn vitest run",
"coverage": "yarn vitest run --coverage",
"clean": "git clean -fXd"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.3",
"@types/node": "26.1.2",
"@types/ws": "^8.5.10",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
},
"dependencies": {
"@openzeppelin/compact-builder": "workspace:^",
"@openzeppelin/compact-deployer": "workspace:^",
"chalk": "^5.6.2",
"ora": "^9.0.0"
"ora": "^9.0.0",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"ws": "^8.16.0"
}
}
70 changes: 70 additions & 0 deletions packages/cli/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import pino, { type Logger } from 'pino';

/**
* Pino factory for the three CLI modes: `--json` (raw JSON to STDERR, no
* transports — STDOUT is reserved for the single result object), default
* (pretty `info+`), `--verbose` (pretty `info+` to stdout AND `debug+`
* mirrored to `.compact/logs/<ts>.log` so the transcript survives spinner
* overwrites).
*/
export interface CreateLoggerOptions {
verbose: boolean;
json: boolean;
logDir?: string;
}

export function createLogger(opts: CreateLoggerOptions): Logger {
if (opts.json) {
// fd 2 = STDERR; keeps STDOUT carrying only the final JSON result.
return pino(
{ level: opts.verbose ? 'debug' : 'info' },
pino.destination(2),
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (opts.verbose) {
const dir = opts.logDir ?? join(process.cwd(), '.compact', 'logs');
mkdirSync(dir, { recursive: true });
const file = join(
dir,
`${new Date().toISOString().replace(/[:.]/g, '-')}.log`,
);
return pino(
{ level: 'debug' },
pino.transport({
targets: [
{
target: 'pino/file',
options: { destination: file },
level: 'debug',
},
{
target: 'pino-pretty',
options: {
destination: 1,
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
level: 'info',
},
],
}),
);
}

return pino(
{ level: 'info' },
pino.transport({
target: 'pino-pretty',
options: {
destination: 1,
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
}),
);
}
72 changes: 72 additions & 0 deletions packages/cli/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { stderr, stdin } from 'node:process';

/**
* Prompt for a keystore passphrase with terminal echo suppressed; falls back
* to plain line-read off a TTY. Prompt text and the trailing newline go to
* STDERR so `--json` (and any piped) callers keep a clean single-object STDOUT.
*/
export async function promptPassphrase(label: string): Promise<string> {
stderr.write(`Passphrase for ${label}: `);
return readMaskedLine();
}

function readMaskedLine(): Promise<string> {
return new Promise((resolveFn, rejectFn) => {
let buffer = '';
const isTTY = stdin.isTTY === true;

const cleanup = () => {
if (isTTY) stdin.setRawMode(false);
stdin.pause();
stdin.removeListener('data', onData);
stdin.removeListener('end', onEnd);
stdin.removeListener('error', onError);
stderr.write('\n');
};

// Without these, a non-interactive stdin that closes without a trailing
// newline (e.g. piped input, or a closed pipe) would never settle the
// promise and the CLI would hang.
const onEnd = () => {
cleanup();
if (buffer.length > 0) resolveFn(buffer);
else rejectFn(new Error('Aborted'));
};

const onError = (err: Error) => {
cleanup();
rejectFn(err);
};

const onData = (chunk: Buffer) => {
const s = chunk.toString('utf8');
for (const ch of s) {
const code = ch.charCodeAt(0);
if (code === 0x03) {
cleanup();
rejectFn(new Error('Aborted'));
return;
}
if (code === 0x0d || code === 0x0a) {
cleanup();
resolveFn(buffer);
return;
}
if (code === 0x7f || code === 0x08) {
// Split by code point: `slice(-1)` on a string would strip half a
// surrogate pair, leaving a lone surrogate in the passphrase.
buffer = [...buffer].slice(0, -1).join('');
continue;
}
buffer += ch;
}
};

if (isTTY) stdin.setRawMode(true);
stdin.resume();
stdin.setEncoding('utf8');
stdin.on('data', onData);
stdin.on('end', onEnd);
stdin.on('error', onError);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
25 changes: 1 addition & 24 deletions packages/cli/src/runBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,7 @@ import { CompactBuilder } from '@openzeppelin/compact-builder';
import chalk from 'chalk';
import ora from 'ora';

/**
* Executes the Compact builder CLI.
* Builds projects using the `CompactBuilder` class with provided options, including compilation and additional steps.
*
* Compiler options (forwarded to `compact-compiler`):
* - `--dir <directory>` - Compile specific subdirectory within srcDir
* - `--src <directory>` - Source directory (default: src)
* - `--out <directory>` - Output directory for artifacts (default: artifacts)
* - `--hierarchical` - Preserve source directory structure in BOTH the
* compiler artifacts output AND the builder's
* .compact copy into dist/ (default off: flat in both)
* - `--exclude <glob>` - Skip .compact files matching pattern, in BOTH the
* compiler's file discovery AND the builder's
* .compact copy (repeatable). When unset, the builder
* falls back to ['Mock*', '*.mock.compact']; the
* compiler defaults to no excludes.
* - `+<version>` - Use specific toolchain version
*
* Builder-only options (control dist/ layout):
* - `--clean-dist` - rm -rf dist before building (default off)
* - `--copy <path>` - copy an extra file into dist/ for distribution (repeatable; e.g. package.json)
*
* See `packages/cli/README.md` for usage examples.
*/
/** `compact-builder` CLI shell. See `packages/cli/README.md` for options. */
async function runBuilder(): Promise<void> {
const spinner = ora(chalk.blue('[BUILD] Compact Builder started')).info();

Expand Down
Loading
Loading