Skip to content

Upgrade Repo to Latest Package / Compact Versions#364

Draft
emnul wants to merge 12 commits intomainfrom
update-simulator-compiler
Draft

Upgrade Repo to Latest Package / Compact Versions#364
emnul wants to merge 12 commits intomainfrom
update-simulator-compiler

Conversation

@emnul
Copy link
Contributor

@emnul emnul commented Feb 3, 2026

  • Merges latest changes from @openzeppelin/compact-tools
  • Upgrades compact-runtime -> 0.14.0
  • Upgrades ledger -> ledger-v7
  • Fixes compact turbo script in package.json
  • Updates setup workflow to use compact 0.28.0
  • Upgrades compact contracts to 0.28.0
  • Removes archive contracts from compact turbo task

Summary by CodeRabbit

  • New Features

    • Added CLI options for customizable source and output directories (--src, --out).
    • Added --hierarchical flag for flexible artifact output structures.
    • Published comprehensive documentation for CLI tools and programmatic API.
  • Dependency Updates

    • Updated Compact compiler to v0.28.0.
    • Updated @midnight-ntwrk/compact-runtime and ledger dependencies.
  • API Improvements

    • Restructured compiler and builder configuration for better usability.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Walkthrough

This PR updates compiler toolchain versions (0.26.0 → 0.28.0), refactors Compiler and Builder APIs to accept options objects instead of positional parameters, updates runtime dependencies (@midnight-ntwrk/compact-runtime, @midnight-ntwrk/ledger-v7), modifies simulator internals to use new context structures, removes legacy test artifacts, and upgrades sample contracts to language version 0.20.0.

Changes

Cohort / File(s) Summary
GitHub Actions & Build Configuration
.github/actions/setup/action.yml, turbo.json
Updated compact compiler action reference and bumped compiler version from 0.26.0 to 0.28.0; removed compact:archive dependency from turbo task graph.
Root & Package Dependencies
package.json, packages/compact/package.json, packages/simulator/package.json
Updated @midnight-ntwrk/compact-runtime from ^0.9.0 to 0.14.0; replaced @midnight-ntwrk/ledger with @midnight-ntwrk/ledger-v7 at version 7.0.0; made packages/compact public by removing private flag and added proper description/keywords.
Compiler API Refactoring
packages/compact/src/Compiler.ts, packages/compact/src/Builder.ts, packages/compact/src/versions.ts
Refactored CompactCompiler constructor to accept single options object (CompilerOptions) instead of positional parameters; added static parseArgs/fromArgs factory methods; replaced FileDiscovery and CompilerService to support configurable src/output directories and hierarchical output; removed hardcoded version constants.
Compiler CLI & Build Tools
packages/compact/src/runCompiler.ts, packages/compact/src/runBuilder.ts
Updated CLI tooling to use new CompactBuilder.fromArgs factory and ts-specific import paths; expanded CLI documentation with new --src, --out, --hierarchical options and artifact output structure examples.
Compiler Documentation & Tests
packages/compact/README.md, packages/compact/test/Compiler.test.ts, packages/compact/test/runCompiler.test.ts
Added comprehensive README for @openzeppelin/compact-tools-cli; refactored all compiler tests to use new options-based API with testOptions getter instead of individual property getters.
Simulator Core Refactoring
packages/simulator/src/core/AbstractSimulator.ts, packages/simulator/src/core/CircuitContextManager.ts, packages/simulator/src/core/ContractSimulator.ts
Changed circuit context from transactionContext/originalState to currentQueryContext; updated getContractState return type from ContractState to StateValue; refactored context initialization to use createConstructorContext and ChargedState.
Simulator Factory & Typing
packages/simulator/src/factory/SimulatorConfig.ts, packages/simulator/src/factory/createSimulator.ts
Added TContract generic parameter to SimulatorConfig and createSimulator; improved circuit proxy typing with ContextlessCircuits, ExtractPureCircuits, ExtractImpureCircuits; switched default contract address from sampleContractAddress to dummyContractAddress; updated state/context access paths.
Simulator Utilities & Helpers
packages/simulator/src/utils/CircuitContextUtils.ts, packages/simulator/src/types/Simulator.ts
Updated CircuitContext utility functions to use ChargedState and currentQueryContext instead of contractState/transactionContext; changed public API return types from ContractState to StateValue.
Sample Contracts
packages/simulator/test/fixtures/sample-contracts/SampleZOwnable.compact, packages/simulator/test/fixtures/sample-contracts/Simple.compact, packages/simulator/test/fixtures/sample-contracts/Witness.compact
Upgraded language version from 0.18.0 to 0.20.0; converted generic imports to named imports from CompactStandardLibrary; added public ledger declarations (_counter, _instanceSalt) to SampleZOwnable.
Test Artifacts Cleanup
packages/simulator/test/fixtures/test-artifacts/SampleZOwnable/*, packages/simulator/test/fixtures/test-artifacts/Simple/*, packages/simulator/test/fixtures/test-artifacts/Witness/*
Removed legacy test-artifacts (CommonJS contract files, TypeScript definitions, compiler metadata, and zkir circuit files); these are replaced by regenerated artifacts in artifacts/ directory.
Test Integration & Setup
packages/simulator/test/integration/SampleZOwnable.test.ts, packages/simulator/test/integration/SampleZOwnableSimulator.ts, packages/simulator/test/integration/SimpleSimulator.ts, packages/simulator/test/integration/WitnessSimulator.ts, packages/simulator/test/setup.ts, packages/simulator/vitest.config.ts
Updated integration tests to import from new artifact paths (.js instead of .cjs); added new generics to createSimulator calls; added vitest global setup script to auto-compile contracts before tests; removed _transferOwnership public method from SampleZOwnable simulator.
Test Utilities & Fixtures
packages/simulator/test/fixtures/utils/address.ts, packages/simulator/test/fixtures/sample-contracts/witnesses/*, packages/simulator/test/unit/core/StateManager.test.ts
Updated ledger import paths to v7; modified address encoding to truncate hex before prefixing; updated artifact import paths; refactored state management tests to use ChargedState and currentQueryContext; replaced coin info types with shielded variants.
Documentation
packages/simulator/README.md
Updated import paths in documentation examples from .cjs to .js artifacts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • 0xisk
  • andrew-fleming

Poem

🐰 Options objects replace parameters old,
Contexts refined, contexts retold,
From scattered artifacts, clean slates arise,
New generic typing helps tests standardize,
Ledger-v7 marches the toolchain forward,
Each change a step toward future reward.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR: upgrading repository packages and the Compact compiler to their latest versions, which aligns with the extensive changes across package.json files, Compiler.ts, and related dependencies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-simulator-compiler

Comment @coderabbitai help to get the list of available commands and usage tips.

@emnul emnul changed the title Merge tooling changes from compact-tools Upgrade Repo to Latest Package / Compact Versions Feb 3, 2026
@emnul emnul marked this pull request as ready for review February 4, 2026 03:42
@emnul emnul requested review from a team as code owners February 4, 2026 03:42
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 21-25: Check whether the dependency
"@midnight-ntwrk/ledger-v7"@7.0.0 actually exists by running npm view
`@midnight-ntwrk/ledger-v7`@7.0.0 (or checking the upstream repo/tags); if the
version is not published, update package.json to a published, supported version
(e.g., the documented Ledger 4.0.0) or remove/replace the dependency accordingly
and ensure the string "@midnight-ntwrk/ledger-v7" in package.json is changed to
the verified version; commit the corrected package.json and update lockfiles if
applicable.

In `@packages/compact/README.md`:
- Around line 9-15: Update the local-path examples in the README so they point
to the actual package directory (packages/compact) instead of packages/cli:
change the install example that uses
"@openzeppelin/compact-tools-cli@file:./compact-tools/packages/cli" to use the
correct local file path for the compact package, and update the direct
invocation example that calls "node
compact-tools/packages/cli/dist/runCompiler.js" to reference the correct
runCompiler.js location under packages/compact (or the correct relative path to
dist/runCompiler.js); ensure the package name `@openzeppelin/compact-tools-cli`
and the runCompiler.js invocation remain consistent with the package's actual
build output.
- Line 132: Docs claim --skip-zk is not accepted but CompactBuilder.fromArgs
delegates to CompactCompiler.parseArgs which still accepts it; update code to
match docs by rejecting --skip-zk: add validation in CompactBuilder.fromArgs to
detect the --skip-zk flag (or the parsed option from CompactCompiler.parseArgs)
and throw or exit with a clear error, or alternatively strip/ignore the flag
before calling CompactCompiler.parseArgs so the build always includes ZK proofs;
reference CompactBuilder.fromArgs, CompactCompiler.parseArgs and the --skip-zk
flag when making the change.

In `@packages/compact/src/Builder.ts`:
- Around line 13-18: The constructor for class Builder currently accepts
CompilerOptions (constructor(options: CompilerOptions = {})) which allows a
runtime flags field to be passed through even though BuilderOptions omits it;
fix by sanitizing the incoming options before storing/forwarding: in the Builder
constructor destructure or create a new object that explicitly omits the flags
property (e.g., const { flags, ...safeOptions } = options) and assign
this.options = safeOptions (type BuilderOptions) and pass safeOptions into
CompactCompiler, or alternatively change the constructor signature to accept
BuilderOptions instead of CompilerOptions so flags can never be provided.
- Around line 101-107: The constructor for CompactBuilder currently accepts
CompilerOptions allowing disallowed flags; change the CompactBuilder constructor
signature to accept options: BuilderOptions = {} (using the BuilderOptions type
that excludes flags) and update fromArgs to call CompactCompiler.parseArgs(args,
env) then strip or ignore any flags (e.g., remove a flags property or filter out
--skip-zk/SKIP_ZK) before passing the sanitized options into new
CompactBuilder(...); ensure references to CompactBuilder, BuilderOptions,
CompactCompiler.parseArgs, and fromArgs are used so the constructor and factory
path enforce the BuilderOptions constraint.

In `@packages/simulator/test/setup.ts`:
- Around line 26-85: The catch in compileContract currently uses isSpawnSyncRet
(which checks SpawnSyncReturns) but execAsync/exec errors use an ExecException
with a code property; update the catch in compileContract to detect exec errors
by checking err is an object and has a code (e.g. (err as any).code === 'ENOENT'
or numeric 127) instead of relying on isSpawnSyncRet, and when detected throw
the friendly '`compact` not found...' Error; leave other errors re-thrown.
Reference: compileContract, execAsync, isSpawnSyncRet, and use the error.code
check for ExecException handling.
🧹 Nitpick comments (1)
packages/compact/src/Compiler.ts (1)

609-647: Consider adding validation for + version flag without a value.

The argument parsing handles --dir, --src, and --out flags with value validation (throwing if no value follows), but the version flag (+<version>) at line 639 doesn't validate that there's actually a version string after the +. If a user passes just + with nothing after, version would be set to an empty string.

This is a minor edge case, but for consistency with other flag validation:

💡 Optional: Add validation for empty version string
       } else if (args[i].startsWith('+')) {
-        options.version = args[i].slice(1);
+        const ver = args[i].slice(1);
+        if (!ver) {
+          throw new Error('+<version> flag requires a version number (e.g., +0.28.0)');
+        }
+        options.version = ver;
       } else {

Comment on lines +9 to +15
```bash
# As a local dependency
yarn add @openzeppelin/compact-tools-cli@file:./compact-tools/packages/cli

# Or invoke directly after building
node compact-tools/packages/cli/dist/runCompiler.js
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix local-path examples if the package lives under packages/compact.

The README resides in packages/compact, but the local install and dev instructions point to packages/cli. If the package directory is packages/compact, these paths will mislead users.

✏️ Suggested doc fix
- yarn add `@openzeppelin/compact-tools-cli`@file:./compact-tools/packages/cli
+ yarn add `@openzeppelin/compact-tools-cli`@file:./compact-tools/packages/compact

- node compact-tools/packages/cli/dist/runCompiler.js
+ node compact-tools/packages/compact/dist/runCompiler.js

- cd packages/cli
+ cd packages/compact

Also applies to: 218-231

🤖 Prompt for AI Agents
In `@packages/compact/README.md` around lines 9 - 15, Update the local-path
examples in the README so they point to the actual package directory
(packages/compact) instead of packages/cli: change the install example that uses
"@openzeppelin/compact-tools-cli@file:./compact-tools/packages/cli" to use the
correct local file path for the compact package, and update the direct
invocation example that calls "node
compact-tools/packages/cli/dist/runCompiler.js" to reference the correct
runCompiler.js location under packages/compact (or the correct relative path to
dist/runCompiler.js); ensure the package name `@openzeppelin/compact-tools-cli`
and the runCompiler.js invocation remain consistent with the package's actual
build output.

compact-builder [options]
```

Accepts all compiler options except `--skip-zk` (builds always include ZK proofs).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Builder CLI docs conflict with current parsing behavior.

CompactBuilder.fromArgs delegates to CompactCompiler.parseArgs, so --skip-zk is still accepted. Either disallow it in code or update the docs to match current behavior.

✏️ Suggested doc fix
-Accepts all compiler options except `--skip-zk` (builds always include ZK proofs).
+Accepts all compiler options (including `--skip-zk`), since the builder delegates to the compiler.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Accepts all compiler options except `--skip-zk` (builds always include ZK proofs).
Accepts all compiler options (including `--skip-zk`), since the builder delegates to the compiler.
🤖 Prompt for AI Agents
In `@packages/compact/README.md` at line 132, Docs claim --skip-zk is not accepted
but CompactBuilder.fromArgs delegates to CompactCompiler.parseArgs which still
accepts it; update code to match docs by rejecting --skip-zk: add validation in
CompactBuilder.fromArgs to detect the --skip-zk flag (or the parsed option from
CompactCompiler.parseArgs) and throw or exit with a clear error, or
alternatively strip/ignore the flag before calling CompactCompiler.parseArgs so
the build always includes ZK proofs; reference CompactBuilder.fromArgs,
CompactCompiler.parseArgs and the --skip-zk flag when making the change.

Comment on lines +13 to +18
/**
* Configuration options for the Builder CLI.
* Inherits from CompilerOptions but excludes `flags` (which would allow --skip-zk).
* Builds should always include ZK proofs.
*/
export type BuilderOptions = Omit<CompilerOptions, 'flags'>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type mismatch allows flags to leak through despite BuilderOptions excluding them.

The documentation states that BuilderOptions omits flags because "Builds should always include ZK proofs." However, there's an inconsistency:

  • Line 66: private readonly options: BuilderOptions (excludes flags)
  • Line 89: constructor(options: CompilerOptions = {}) (includes flags)

Since the constructor accepts CompilerOptions, users can pass flags: '--skip-zk', which would then be passed to CompactCompiler at line 118. The assignment this.options = options implicitly drops the flags field due to the BuilderOptions type, but the object still contains it at runtime.

Consider either:

  1. Explicitly omitting flags when storing options, or
  2. Changing the constructor parameter type to BuilderOptions
🛡️ Option 1: Explicitly omit flags
-  constructor(options: CompilerOptions = {}) {
-    this.options = options;
+  constructor(options: CompilerOptions = {}) {
+    const { flags: _ignored, ...builderOptions } = options;
+    this.options = builderOptions;
   }
🛡️ Option 2: Change parameter type
-  constructor(options: CompilerOptions = {}) {
+  constructor(options: BuilderOptions = {}) {
     this.options = options;
   }

Also applies to: 66-66, 89-91

🤖 Prompt for AI Agents
In `@packages/compact/src/Builder.ts` around lines 13 - 18, The constructor for
class Builder currently accepts CompilerOptions (constructor(options:
CompilerOptions = {})) which allows a runtime flags field to be passed through
even though BuilderOptions omits it; fix by sanitizing the incoming options
before storing/forwarding: in the Builder constructor destructure or create a
new object that explicitly omits the flags property (e.g., const { flags,
...safeOptions } = options) and assign this.options = safeOptions (type
BuilderOptions) and pass safeOptions into CompactCompiler, or alternatively
change the constructor signature to accept BuilderOptions instead of
CompilerOptions so flags can never be provided.

Comment on lines +101 to 107
static fromArgs(
args: string[],
env: NodeJS.ProcessEnv = process.env,
): CompactBuilder {
const options = CompactCompiler.parseArgs(args, env);
return new CompactBuilder(options);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find usage of fromArgs and CompactBuilder initialization
rg -n "CompactBuilder\.fromArgs|new CompactBuilder" --type ts -B2 -A5

Repository: OpenZeppelin/compact-contracts

Length of output: 2204


🏁 Script executed:

#!/bin/bash
# Find the CompactCompiler.parseArgs implementation
fd "CompactCompiler" --type ts | head -20

Repository: OpenZeppelin/compact-contracts

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Check for skip-zk handling
rg -n "skip-zk|SKIP_ZK" --type ts -B2 -A2

Repository: OpenZeppelin/compact-contracts

Length of output: 37816


🏁 Script executed:

#!/bin/bash
# Find BuilderOptions interface definition
rg -n "BuilderOptions|interface.*Options" packages/compact/src/Builder.ts -B2 -A5

Repository: OpenZeppelin/compact-contracts

Length of output: 671


🏁 Script executed:

#!/bin/bash
# Check CompactBuilder constructor in Builder.ts
sed -n '85,95p' packages/compact/src/Builder.ts

Repository: OpenZeppelin/compact-contracts

Length of output: 448


Constructor type signature violates BuilderOptions design.

The BuilderOptions type (line 18) explicitly excludes flags to prevent --skip-zk, but the constructor (line 89) accepts CompilerOptions, allowing flags through. The fromArgs method passes CompactCompiler.parseArgs() output directly without filtering, meaning SKIP_ZK=true or --skip-zk arguments can bypass the intended constraint.

Fix: Change constructor parameter to options: BuilderOptions = {} and filter flags in fromArgs before instantiation.

🤖 Prompt for AI Agents
In `@packages/compact/src/Builder.ts` around lines 101 - 107, The constructor for
CompactBuilder currently accepts CompilerOptions allowing disallowed flags;
change the CompactBuilder constructor signature to accept options:
BuilderOptions = {} (using the BuilderOptions type that excludes flags) and
update fromArgs to call CompactCompiler.parseArgs(args, env) then strip or
ignore any flags (e.g., remove a flags property or filter out --skip-zk/SKIP_ZK)
before passing the sanitized options into new CompactBuilder(...); ensure
references to CompactBuilder, BuilderOptions, CompactCompiler.parseArgs, and
fromArgs are used so the constructor and factory path enforce the BuilderOptions
constraint.

Comment on lines +26 to +85
function isSpawnSyncRet(
err: unknown,
): err is SpawnSyncReturns<string | Buffer> {
if (typeof err !== 'object' || err === null) {
return false;
}

const typedErr = err as Partial<SpawnSyncReturns<string | Buffer>> &
Record<string, unknown>;

const okErr = typedErr.error instanceof Error;
const okStdout =
typeof typedErr.stdout === 'string' || Buffer.isBuffer(typedErr.stdout);
const okStderr =
typeof typedErr.stderr === 'string' || Buffer.isBuffer(typedErr.stderr);
const okStatus =
typeof typedErr.status === 'number' || typedErr.status === null;

return okErr && okStdout && okStderr && okStatus;
}

async function compileContract(contractFile: string): Promise<void> {
const inputPath = join(SAMPLE_CONTRACTS_DIR, contractFile);
const contractName = contractFile.replace('.compact', '');
const outputDir = join(ARTIFACTS_DIR, contractName);
const contractArtifact = join(outputDir, 'contract', 'index.js');

// Skip if artifact already exists and is newer than source
if (existsSync(contractArtifact) && existsSync(inputPath)) {
const artifactTime = statSync(contractArtifact).mtime;
const sourceTime = statSync(inputPath).mtime;
if (artifactTime >= sourceTime) {
console.log(`✓ ${contractFile} (already compiled)`);
return; // Already compiled and up to date
}
}

if (!existsSync(inputPath)) {
throw new Error(`Contract file not found: ${inputPath}`);
}

// Ensure output directory and keys subdirectory exist
mkdirSync(outputDir, { recursive: true });
mkdirSync(join(outputDir, 'keys'), { recursive: true });

const command = `compact compile --skip-zk "${inputPath}" "${outputDir}"`;
try {
await execAsync(command);
} catch (err: unknown) {
if (!isSpawnSyncRet(err)) {
throw err;
}

if (err.status === 127) {
throw new Error(
'`compact` not found (exit code 127). Is it installed and on PATH?',
);
}

throw err;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/simulator/test/setup.ts | head -100

Repository: OpenZeppelin/compact-contracts

Length of output: 3831


🌐 Web query:

Node.js child_process exec error handling ExecException code status command not found

💡 Result:

In child_process.exec(cmd, cb), “command not found” is usually not a Node error—it’s the shell returning a non‑zero exit code, and Node surfaces that as an Error (often referred to as an ExecException) in the callback. The key fields to inspect are:

  • error.code: process exit code (non‑zero means failure)
  • stderr: shell’s error text (often contains “command not found”)
  • error.signal: if it was terminated by a signal (otherwise typically null) (r2.nodejs.org)

Typical cases

1) POSIX (macOS/Linux): command not found → exit code 127

const { exec } = require('node:child_process');

exec('does-not-exist', (error, stdout, stderr) => {
  if (error) {
    console.log('exit code:', error.code);   // often 127
    console.log('stderr:', stderr);          // "... command not found"
    return;
  }
});

Exit code 127 for “command not found” is commonly observed when running via a POSIX shell. (codestudy.net)

2) spawn / execFile: missing executable → ENOENT (not a numeric exit code)

If you use spawn()/execFile() and the binary can’t be located, Node can fail before a child process even starts, producing:

  • error.code === 'ENOENT'
  • error.syscall === 'spawn'

Common causes: binary not on PATH, wrong working directory, or on Windows calling something that exists only as a .bat/.cmd without going through cmd.exe. (nodejs.org)

3) Windows gotchas (.bat/.cmd and PATH)

  • .bat / .cmd files generally need a shell (exec()), or explicitly cmd.exe /c your-script.cmd. (nodejs.org)
  • If standard commands (like ipconfig) “aren’t found”, it can be a broken PATH (e.g., missing System32). (stackoverflow.com)
  • Some commands resolve in cmd.exe interactively (like jekylljekyll.bat), but Node spawn/execFile won’t auto-resolve that; you may need to call the .bat directly (or use exec). (matthew-jackson.com)

Practical error-handling pattern (exec)

exec(command, (error, stdout, stderr) => {
  if (!error) return;

  // Shell-level failure (non-zero exit code)
  console.error({ code: error.code, signal: error.signal });
  console.error('stderr:', stderr);

  // If you expected "command not found":
  // - POSIX: code often 127
  // - Windows: message often "'xxx' is not recognized..."
});

If you share your OS + the exact error object (all fields) + the command string, I can tell which of the above cases you’re hitting and the most direct fix.

Citations:


🌐 Web query:

Node.js exec() function what error object properties code exit

💡 Result:

For child_process.exec(command, cb), the callback is cb(error, stdout, stderr).

error (when non-null) commonly used properties

  • error.code: the process exit code (number). By convention, anything other than 0 indicates an error. [1]
  • error.signal: the signal name (string) that terminated the process (if it was killed by a signal). [1]

Also, since it’s an Error, it has the usual error.message, error.name, error.stack, etc.

If you want “exit code” without using exec’s error

Listen to the returned ChildProcess:

  • 'exit' event handler receives (code, signal):
    • code is the exit code (or null if terminated by signal)
    • signal is the signal (or null if exited normally) [2]

Promisified exec

If you use util.promisify(exec), a rejection gives the same error plus extra error.stdout and error.stderr. [1]

Sources: Node.js child_process docs (exec + promisified exec) [1], Node.js ChildProcess 'exit' event docs [2].


Fix exec error handling to detect "command not found."

The current code uses isSpawnSyncRet() to check for SpawnSyncReturns (from spawnSync()), but exec() errors have a different structure. Promisified exec() errors have an error.code property, not error.status, and don't have a nested error.error property. The guard will fail for all exec() errors, causing them to be re-thrown before the status === 127 check executes.

🐛 Proposed fix (use error.code instead of error.status)
-import { exec, type SpawnSyncReturns } from 'node:child_process';
+import { exec } from 'node:child_process';
@@
-function isSpawnSyncRet(
-  err: unknown,
-): err is SpawnSyncReturns<string | Buffer> {
+function isExecError(
+  err: unknown,
+): err is Error & { code?: number | string; stdout?: string | Buffer; stderr?: string | Buffer } {
   if (typeof err !== 'object' || err === null) {
     return false;
   }
-
-  const typedErr = err as Partial<SpawnSyncReturns<string | Buffer>> &
-    Record<string, unknown>;
-
-  const okErr = typedErr.error instanceof Error;
-  const okStdout =
-    typeof typedErr.stdout === 'string' || Buffer.isBuffer(typedErr.stdout);
-  const okStderr =
-    typeof typedErr.stderr === 'string' || Buffer.isBuffer(typedErr.stderr);
-  const okStatus =
-    typeof typedErr.status === 'number' || typedErr.status === null;
-
-  return okErr && okStdout && okStderr && okStatus;
+  return 'code' in err;
 }
@@
   } catch (err: unknown) {
-    if (!isSpawnSyncRet(err)) {
+    if (!isExecError(err)) {
       throw err;
     }
 
-    if (err.status === 127) {
+    if (err.code === 127) {
       throw new Error(
         '`compact` not found (exit code 127). Is it installed and on PATH?',
       );
     }
🤖 Prompt for AI Agents
In `@packages/simulator/test/setup.ts` around lines 26 - 85, The catch in
compileContract currently uses isSpawnSyncRet (which checks SpawnSyncReturns)
but execAsync/exec errors use an ExecException with a code property; update the
catch in compileContract to detect exec errors by checking err is an object and
has a code (e.g. (err as any).code === 'ENOENT' or numeric 127) instead of
relying on isSpawnSyncRet, and when detected throw the friendly '`compact` not
found...' Error; leave other errors re-thrown. Reference: compileContract,
execAsync, isSpawnSyncRet, and use the error.code check for ExecException
handling.

@emnul emnul marked this pull request as draft February 4, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant