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.4.16",
"@openzeppelin/compact-deployer": "workspace:^",
"@types/node": "25.9.1",
"@vitest/coverage-v8": "4.1.8",
"pino": "^9.7.0",
"ts-node": "^10.9.2",
"turbo": "^2.9.14",
"typescript": "^6.0.3",
"vitest": "^4.1.6"
},
"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"
}
}
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": "25.9.1",
"@types/ws": "^8.5.10",
"typescript": "^6.0.3",
"vitest": "^4.1.6"
},
"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',
},
}),
);
}
70 changes: 70 additions & 0 deletions packages/cli/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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) {
buffer = buffer.slice(0, -1);
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