Skip to content

Merge v0.4.11 into master - #487

Merged
RetricSu merged 7 commits into
masterfrom
develop
Jul 31, 2026
Merged

Merge v0.4.11 into master#487
RetricSu merged 7 commits into
masterfrom
develop

Conversation

@RetricSu

Copy link
Copy Markdown
Collaborator

No description provided.

humble-little-bear and others added 7 commits July 29, 2026 20:48
The devnet ckb.toml template enables the Terminal RPC module, which only
exists since CKB v0.205.0 (nervosnetwork/ckb#4989). Older binaries abort
at startup with an opaque serde "unknown variant `Terminal`" error, and
migrateLegacyDevnetRpcConfig re-added the module on every `offckb node`
start even after users removed it by hand — an unbreakable crash loop for
anyone pinned to an old CKB.

- initChainIfNeeded takes the effective CKB version: fresh chains for a
  pre-0.205.0 binary are initialized from the template with Terminal
  stripped (tcp_listen_address, which predates 0.205.0, stays), and the
  legacy-config migration no longer re-adds Terminal for such binaries.
- nodeDevnet resolves the effective version (managed binaries know it;
  a custom --binary-path is probed via getVersionFromBinary) and fails
  fast with an actionable error when the existing config enables Terminal
  but the binary is too old, instead of letting CKB dump the serde error.
- A custom binary whose version cannot be probed keeps the historical
  behavior; if it then crashes with the tell-tale "unknown variant
  `Terminal`" stderr, the startup error now points at the actual cause.
- README's status section notes the CKB >= 0.205.0 requirement for the
  TUI's system-metric panels.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(status): bump ckb-tui to v0.1.4 and reinstall stale binaries

ckb-tui v0.1.4 fixes a divide-by-zero panic in the dashboard data-sync
thread when the connected node has no peers (Officeyutong/ckb-tui#13) —
the normal state of a single-node devnet — which permanently froze the
Overview, Mempool, Peers, and Blockchain panels seconds after opening
offckb status.

- Bump the default ckb-tui version to v0.1.4 and pin the SHA-256
  digests of its release assets.
- Pin digests of the extracted binaries as well and verify the
  installed binary against them in ensureInstalled(): ckb-tui's
  --version output lags its release tag (both v0.1.3 and v0.1.4
  binaries report 0.1.2), so a presence-only check would keep the
  panic-affected v0.1.3 binary installed forever. A stale or foreign
  binary is now removed and reinstalled; versions without a pinned
  binary digest keep the previous presence-only behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): keep the previous ckb-tui binary until the reinstall succeeds

Address review feedback on #476:

- ensureInstalled no longer deletes the installed binary up front.
  installSync already stages the download/verify/extract in a temp
  directory and publishes with an atomic rename, so a failed reinstall
  now leaves the previous binary untouched instead of stranding the
  user with no executable.
- installedBinaryMatches treats missing, unreadable, or non-regular
  paths (e.g. a directory at the binary location) as a mismatch and
  flows into the reinstall path instead of throwing raw fs errors out
  of ensureInstalled.
- Tests: the install spy now publishes the binary path like a real
  installSync, so the ensureInstalled return contract is asserted;
  added regression tests for failed-reinstall preservation and for
  non-regular paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): set aside directory targets and strict-check the ckb-tui binary

Second round of CodeRabbit review on the ckb-tui v0.1.4 bump:

- A directory occupying the install path made every reinstall fail with
  EISDIR (a file rename cannot replace a directory). publishExtractedBinary
  now sets the directory aside with a plain rename — its contents are never
  deleted — publishes the verified binary, and restores the directory if
  publishing fails.
- installedBinaryMatches required a regular, readable file only on some
  paths: the pinned branch hashed whatever was there, so a FIFO at the
  install path blocked ensureInstalled indefinitely, and the unpinned
  fallback accepted unreadable regular files. It now checks isFile() and
  R_OK up front for both branches.
- The successful install test spy now publishes a real regular file (and
  assertions verify type/content), with new regression tests for directory
  publish, restore-on-failure, FIFO, and unreadable binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): don't let post-publish cleanup mask a successful ckb-tui install

Follow-up to the re-review of 0e1dd83:

- renameIntoPlace no longer unlinks the staged source after the publish
  rename succeeds: a failure there would have reported a correctly
  published binary as a failed install. The source lives in the temp
  directory, which installSync's finally block removes regardless.
- The FIFO regression test now mocks readFileSync to throw, so a
  regression fails fast (and asserts the FIFO is never opened) instead of
  hanging the Jest worker on a synchronous blocking read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…de (#478)

* feat: unify devnet logging with offckb logs and a quiet foreground node

Add offckb logs [node|script|miner|rpc] [-f] [--grep] [--tail], reading
the log files CKB always writes (run.log/miner.log) plus a new proxy
event log, so logs are reachable in every run mode and pipe/agent
friendly.

A foreground offckb node is quiet by default: lifecycle events, live
contract script debug output (via the node's TCP log subscription, the
same channel ckb-tui uses), send_transaction hashes, and RPC errors
still print; --verbose restores the raw stdout relay.

The RPC proxy drops per-request lines to debug, warns on JSON-RPC
errors in responses, and appends everything to data/logs/proxy.log
(viewable via offckb logs rpc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address PR review findings on logs command and proxy events

- node: sanitize relayed script log entries (CSI/OSC/C0/C1) via
  cleanChildOutput so crafted debug! output cannot inject terminal
  control sequences
- log-file: honor readSync's byte count and decode with a streaming
  TextDecoder so multi-byte UTF-8 survives chunk boundaries
- proxy-events: sanitize event text at the single event() choke point
  (one event = one line), normalize the response media type before the
  application/json check (charset params), and bound proxy.log with a
  single .1 rollover at 10 MB
- log-subscription: retry only during the initial connect window and
  track/unref/clear the retry timer so close() is fully synchronous
- cli: throw commander's InvalidArgumentError from the --tail parser
  and align the --verbose help text with the actual quiet defaults
- tests: add follow-mode script/grep, truncation/rotation, UTF-8 split,
  event sanitization, rollover, charset content-type, and subscription
  retry cases; move temp-dir handling to afterEach cleanup

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fix(config): unfreeze bundled ckb-tui version for upgraded installs
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: add legacy-data upgrade-path integration test to CI

Simulate a user upgrading offckb: an old release (0.3.4, CKB 0.113.1)
creates a devnet in a sandboxed HOME/XDG, then the current build must
operate on that legacy data without breaking it:

- the chain continues (same genesis hash, tip grows past the old tip),
  so a silent chain reset cannot pass as 'RPC responds'
- the legacy ckb.toml is migrated (Terminal RPC module +
  tcp_listen_address) — the class of bug that previously reached users
  before we noticed
- the bundled chain spec stays byte-identical
- a fresh transfer succeeds and is committed

Runs per-PR on ubuntu-latest only, after create-test; the npm cache is
cached to keep the extra legacy CLI install cheap. Verified end-to-end
locally twice (offckb 0.3.4 → 0.4.10).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: run legacy-data test before create-test

create-test.sh's cleanup kills only the pnpm wrapper, leaving the CKB
processes orphaned and holding ports 8114/28114 (the runner's orphan
cleanup only fires at job end), which tripped the legacy test's
precondition check. Run the legacy test first instead — its own
teardown is complete, so create-test still starts on free ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
chore: version packages for 0.4.11 release
@github-actions

Copy link
Copy Markdown

Missing Changeset

Please add a changeset describing your changes:

pnpm changeset

If your changes do not need a version bump (docs, CI, refactoring),
add the skip-changeset label to this PR.

For dependency updates, use the dependencies label.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added offckb logs for viewing and following node, script, miner, and RPC logs with filtering and tail options.
    • Added --verbose output for foreground node and miner processes.
    • Improved CKB compatibility, including version-aware Terminal RPC configuration.
  • Improvements
    • Updated bundled CKB to 0.208.0 and ckb-tui to v0.1.4.
    • Added safer ckb-tui installation verification and log file handling.
  • Bug Fixes
    • Improved legacy devnet data upgrades and configuration migration.
  • Documentation
    • Updated usage guidance for logs, verbose output, and CKB version requirements.

Walkthrough

The CLI release adds unified devnet logging, quiet and verbose node output, version-aware Terminal RPC configuration, legacy-data upgrade testing, ckb-tui v0.1.4 verification, persisted settings migration, and the 0.4.11 release documentation.

Changes

Unified devnet logging

Layer / File(s) Summary
Log storage and subscriptions
src/devnet/log-file.ts, src/devnet/log-subscription.ts, src/cmd/status.ts, tests/logs.test.ts, tests/log-subscription.test.ts
Adds shared log parsing, filtering, tailing, following, path resolution, and TCP subscription handling.
CLI log commands and node output
src/cli.ts, src/cmd/logs.ts, src/cmd/node.ts, README.md, tests/logs-command.test.ts, tests/node-quiet-mode.test.ts
Adds offckb logs, --verbose, quiet foreground output, script-log streaming, sanitization, and shutdown cleanup.
RPC proxy event logging
src/tools/proxy-events.ts, src/tools/rpc-proxy.ts, tests/proxy-events.test.ts
Adds sanitized proxy event logs, transaction persistence, JSON-RPC response handling, and log rollover.

CKB compatibility and legacy upgrades

Layer / File(s) Summary
Version-aware chain configuration
src/node/init-chain.ts, src/cmd/node.ts, src/cmd/status.ts, tests/init-chain.test.ts, tests/node-terminal-rpc.test.ts, tests/node-supervisor.test.ts, README.md
Propagates CKB versions and adapts Terminal RPC configuration for older binaries.
Legacy devnet upgrade validation
scripts/legacy-data-test.sh, .github/workflows/test.yml, package.json
Creates legacy data with an older CLI, upgrades it with the current CLI, validates migration, and confirms a committed transaction.

Settings and ckb-tui release

Layer / File(s) Summary
Bundled version settings
src/cfg/setting.ts, tests/setting.test.ts
Updates bundled versions and migrates persisted ckb-tui versions while omitting the default version from saved settings.
Verified ckb-tui installation
src/tools/ckb-tui.ts, tests/ckb-tui-checksum.test.ts, tests/ckb-tui-install.test.ts
Adds pinned digest checks and atomic binary replacement with rollback handling.
Release metadata and documentation
package.json, CHANGELOG.md, README.md
Updates the package to 0.4.11 and documents logging and Terminal RPC compatibility.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

Suggested reviewers: humble-little-bear

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided, so the changeset lacks a written summary from the author. Add a concise description that summarizes the v0.4.11 release changes, including logging, version upgrades, legacy-data migration, and compatibility updates.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the v0.4.11 release merge into the master branch, which matches the pull request objectives and changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (7)
tests/ckb-tui-install.test.ts (2)

199-237: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the EXDEV staging branch.

The three tests cover the direct-rename path and the directory backup and restore paths. No test reaches the EXDEV fallback in renameIntoPlace, which stages a copy next to the target and then renames it. That branch also runs fs.rmSync(stagingPath, { force: true }) in a finally block after a successful rename. Force the branch by making the first fs.renameSync call throw an EXDEV error.

♻️ Proposed additional test
   it('restores the original directory when publishing fails', () => {
it('stages the copy next to the target when the rename crosses filesystems', () => {
  const extracted = path.join(binDir, 'extracted-ckb-tui');
  fs.writeFileSync(extracted, REPLACED_BINARY);

  const real = fs.renameSync;
  let firstCall = true;
  jest.spyOn(fs, 'renameSync').mockImplementation((from, to) => {
    if (firstCall) {
      firstCall = false;
      const error = new Error('EXDEV: cross-device link not permitted') as NodeJS.ErrnoException;
      error.code = 'EXDEV';
      throw error;
    }
    return real(from, to);
  });

  internals.publishExtractedBinary(extracted, binaryPath);

  expect(fs.readFileSync(binaryPath, 'utf8')).toBe(REPLACED_BINARY);
  // No staging file is left behind.
  expect(fs.readdirSync(binDir).filter((name) => name.includes('.staging-'))).toHaveLength(0);
  jest.restoreAllMocks();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ckb-tui-install.test.ts` around lines 199 - 237, Add a test covering
the EXDEV fallback in renameIntoPlace by mocking the first fs.renameSync call to
throw an error with code EXDEV, then delegating subsequent calls to the real
implementation. Use internals.publishExtractedBinary with a valid extracted
binary, assert the target contains the replacement content, and verify no
.staging- file remains; ensure the fs.renameSync mock is restored.

88-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider importing the pinned digest table instead of duplicating it.

pinnedDigest() copies the three v0.1.4 entries from KNOWN_BINARY_SHA256 in src/tools/ckb-tui.ts. The comment states that the copy is kept in sync by hand. Export the table from the module and read it here, so a digest change cannot leave the test asserting a stale value.

The mocked crypto.createHash at lines 101-103 returns the pinned digest for any input. The test therefore verifies the comparison and the keep decision, but not that installedBinaryMatches hashes the file bytes. Consider computing the digest of a fixture file and injecting that value into the mocked settings-driven lookup instead, so the test exercises the real hash path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ckb-tui-install.test.ts` around lines 88 - 107, Update the test’s
pinnedDigest setup to import and read the exported KNOWN_BINARY_SHA256 table
from ckb-tui.ts instead of duplicating its entries. Also revise the
existing-binary test around CKBTui.ensureInstalled and crypto.createHash so the
mock returns the digest computed from the fixture file bytes through the
settings-driven lookup, preserving verification of the real
installedBinaryMatches hashing path while still asserting installation is
skipped.
tests/setting.test.ts (1)

79-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting that sibling tools keys survive the delete.

writeSettings deletes toWrite.tools.ckbTui. No test asserts that other tools entries, such as rootFolder, remain in the written file. A future change from delete tools.ckbTui to replacing tools would pass the current suite.

♻️ Proposed additional assertion
     it('omits the bundled ckb-tui version when it equals the shipped default', () => {
       const settings = readSettings();
       writeSettings(settings);
 
       const written = JSON.parse(fs.readFileSync(configPath, 'utf8'));
       expect(written.tools.ckbTui).toBeUndefined();
+      // Sibling tools entries must survive the delete.
+      expect(written.tools.rootFolder).toBe(defaultSettings.tools.rootFolder);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/setting.test.ts` around lines 79 - 95, Add an assertion in the
writeSettings tests that the sibling tools.rootFolder value remains present and
unchanged after writeSettings deletes or omits tools.ckbTui, covering both the
default-version and differing-version cases as appropriate.
tests/init-chain.test.ts (1)

210-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for devnetConfigHasTerminalRpc.

This suite covers supportsTerminalRpcModule and the migration, but no test exercises devnetConfigHasTerminalRpc with real files. That function gates the fail-fast path at src/cmd/node.ts:105, and tests/node-terminal-rpc.test.ts mocks it. Add cases for a Terminal-enabled config, a config without Terminal, a missing file, and an unparseable file.

Do you want me to generate those tests?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/init-chain.test.ts` around lines 210 - 227, Add tests in the “Terminal
RPC module version gating” suite that exercise devnetConfigHasTerminalRpc using
real temporary config files: verify Terminal-enabled, Terminal-absent,
missing-file, and unparseable-file cases. Reuse the existing root and
mockConfigPath setup, and assert the function’s expected boolean result for each
scenario.
src/cmd/logs.ts (1)

38-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Return the stop function from showLogs.

showLogs discards the value returned by followLogFile. The process stays alive because fs.watchFile holds the event loop, and Ctrl-C terminates it. But no caller and no test can stop the follow. Return the stop function so callers control the subscription.

♻️ Proposed refactor
-export function showLogs(target: LogTarget, options: LogsOptions, settings: Settings, logger: UnifiedLogger): void {
+export function showLogs(
+  target: LogTarget,
+  options: LogsOptions,
+  settings: Settings,
+  logger: UnifiedLogger,
+): (() => void) | undefined {
@@
-  if (!options.follow) return;
+  if (!options.follow) return undefined;
 
   let inScriptEntry = false;
-  followLogFile(filePath, (line) => {
+  return followLogFile(filePath, (line) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/logs.ts` around lines 38 - 51, Update showLogs to return the stop
function produced by followLogFile instead of discarding it, while preserving
the existing early return when following is disabled and the log filtering
behavior.
src/devnet/log-file.ts (2)

88-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a network parameter for the rpc target.

resolveLogPath hardcodes Network.devinet… correction: it hardcodes Network.devnet for rpc. createRPCProxy in src/tools/rpc-proxy.ts writes proxy events for testnet and mainnet too, using proxyLogPathForNetwork(network, settings). Those files are then unreachable from offckb logs rpc. Add an optional network argument when the CLI gains a --network flag for logs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-file.ts` around lines 88 - 91, The resolveLogPath function
hardcodes Network.devnet for rpc logs, preventing testnet and mainnet proxy logs
from being resolved. Add an optional network parameter to resolveLogPath and use
it with proxyLogPathForNetwork for the rpc target, then propagate the CLI’s
--network value through the logs command while preserving devnet as the default.

93-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Read the tail without loading the whole log file.

readLogTail reads the complete file into a string. run.log grows without bound during a long devnet session, so a offckb logs call allocates the entire file to print 100 lines. For very large files Node also throws a string-length error instead of the helpful message.

Read backwards in fixed-size chunks until count newlines are found.

♻️ Proposed bounded tail read
 export function readLogTail(filePath: string, count: number): string[] {
-  let content: string;
+  const CHUNK = 64 * 1024;
+  let fd: number;
   try {
-    content = fs.readFileSync(filePath, 'utf8');
+    fd = fs.openSync(filePath, 'r');
   } catch (error) {
     const err = error as NodeJS.ErrnoException;
     if (err.code === 'ENOENT') {
       throw new Error(
         `Log file not found at ${filePath}. Start the devnet first (offckb node) and make sure ` +
           'ckb.toml keeps log_to_file = true.',
       );
     }
     throw error;
   }
-  return tailLines(content, count);
+  try {
+    let position = fs.fstatSync(fd).size;
+    const chunks: Buffer[] = [];
+    let newlines = 0;
+    while (position > 0 && newlines <= count) {
+      const length = Math.min(CHUNK, position);
+      position -= length;
+      const buffer = Buffer.alloc(length);
+      fs.readSync(fd, buffer, 0, length, position);
+      chunks.unshift(buffer);
+      for (const byte of buffer) if (byte === 0x0a) newlines++;
+    }
+    return tailLines(Buffer.concat(chunks).toString('utf8'), count);
+  } finally {
+    fs.closeSync(fd);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-file.ts` around lines 93 - 108, Update readLogTail to avoid
fs.readFileSync and read the log backwards in fixed-size chunks, accumulating
only enough data to identify the final count lines; preserve the existing ENOENT
message and tailLines behavior, including sensible handling when the requested
count exceeds available lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cmd/logs.ts`:
- Around line 54-56: Update logsCommand to reject unrecognized target values
instead of defaulting them to 'node'. Validate the provided target against
LOG_TARGETS, report an error that includes the accepted values, and only call
showLogs for valid targets or when the target is omitted and should use the
default.
- Around line 32-36: Adjust the log retrieval flow around readLogTail and
filterLinesByTarget so target-filtered requests read a sufficiently large window
before filtering, then limit the filtered results to the requested tail count.
Preserve the existing grep behavior and ensure unfiltered logs continue using
the current tail semantics.

In `@src/devnet/log-file.ts`:
- Around line 124-131: Update followLogFile’s onChange reset condition in
src/devnet/log-file.ts:124-131 to also reset when curr.ino differs from
prev.ino, alongside the existing size checks. Update tests/logs.test.ts:197-201
to simulate rotation by renaming run.log, writing a larger replacement at the
original path, and asserting the replacement content is emitted from offset 0.

In `@src/node/init-chain.ts`:
- Around line 74-77: Update supportsTerminalRpcModule to use semver.satisfies
with the range >=0.205.0-0, while preserving the existing null and
invalid-version behavior. Update the related test expectation so 0.205.0
prerelease versions, including rc builds, are accepted for Terminal RPC support.

In `@src/tools/ckb-tui.ts`:
- Around line 109-138: Update installedBinaryMatches to validate execute
permission in addition to readability on POSIX systems, using fs.accessSync with
X_OK so binaries without the execute bit return false and enter the reinstall
path. Preserve the existing regular-file, digest, and fallback behavior, while
handling platforms where POSIX execute checks are not applicable.

In `@src/tools/proxy-events.ts`:
- Around line 63-76: Separate rollover operations from event appending in the
logging flow around the existing fs.renameSync and fs.appendFileSync calls. If
rollover deletion or renaming fails, retain the current log file and continue to
append the event instead of entering the catch path that drops it or leaves size
invalid; update size consistently after the append so subsequent events can
retry rollover when appropriate.
- Around line 86-111: Update handleProxyRequestBody to normalize the parsed
JSON-RPC payload into a single-item or batch array, then process each request
entry so batched send_transaction calls are recorded and persisted. Before
accessing the first parameter, validate that params is an array; skip
transaction handling for missing or non-array params instead of allowing a
TypeError to be reported as a JSON parsing failure.

In `@src/tools/rpc-proxy.ts`:
- Around line 15-23: Annotate the ctx object in the RPC proxy setup with the
ProxyEventContext type, then connect the --verbose option to the
UnifiedLogger/Winston log-level configuration so ctx.sink.debug emits proxy
request logs during verbose runs; otherwise explicitly document that --verbose
does not enable proxy debug logging.

---

Nitpick comments:
In `@src/cmd/logs.ts`:
- Around line 38-51: Update showLogs to return the stop function produced by
followLogFile instead of discarding it, while preserving the existing early
return when following is disabled and the log filtering behavior.

In `@src/devnet/log-file.ts`:
- Around line 88-91: The resolveLogPath function hardcodes Network.devnet for
rpc logs, preventing testnet and mainnet proxy logs from being resolved. Add an
optional network parameter to resolveLogPath and use it with
proxyLogPathForNetwork for the rpc target, then propagate the CLI’s --network
value through the logs command while preserving devnet as the default.
- Around line 93-108: Update readLogTail to avoid fs.readFileSync and read the
log backwards in fixed-size chunks, accumulating only enough data to identify
the final count lines; preserve the existing ENOENT message and tailLines
behavior, including sensible handling when the requested count exceeds available
lines.

In `@tests/ckb-tui-install.test.ts`:
- Around line 199-237: Add a test covering the EXDEV fallback in renameIntoPlace
by mocking the first fs.renameSync call to throw an error with code EXDEV, then
delegating subsequent calls to the real implementation. Use
internals.publishExtractedBinary with a valid extracted binary, assert the
target contains the replacement content, and verify no .staging- file remains;
ensure the fs.renameSync mock is restored.
- Around line 88-107: Update the test’s pinnedDigest setup to import and read
the exported KNOWN_BINARY_SHA256 table from ckb-tui.ts instead of duplicating
its entries. Also revise the existing-binary test around CKBTui.ensureInstalled
and crypto.createHash so the mock returns the digest computed from the fixture
file bytes through the settings-driven lookup, preserving verification of the
real installedBinaryMatches hashing path while still asserting installation is
skipped.

In `@tests/init-chain.test.ts`:
- Around line 210-227: Add tests in the “Terminal RPC module version gating”
suite that exercise devnetConfigHasTerminalRpc using real temporary config
files: verify Terminal-enabled, Terminal-absent, missing-file, and
unparseable-file cases. Reuse the existing root and mockConfigPath setup, and
assert the function’s expected boolean result for each scenario.

In `@tests/setting.test.ts`:
- Around line 79-95: Add an assertion in the writeSettings tests that the
sibling tools.rootFolder value remains present and unchanged after writeSettings
deletes or omits tools.ckbTui, covering both the default-version and
differing-version cases as appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f28a43f8-70d2-4d52-bb5f-19a1e03db4f9

📥 Commits

Reviewing files that changed from the base of the PR and between c4a05f8 and 51f437e.

📒 Files selected for processing (27)
  • .github/workflows/test.yml
  • CHANGELOG.md
  • README.md
  • package.json
  • scripts/legacy-data-test.sh
  • src/cfg/setting.ts
  • src/cli.ts
  • src/cmd/logs.ts
  • src/cmd/node.ts
  • src/cmd/status.ts
  • src/devnet/log-file.ts
  • src/devnet/log-subscription.ts
  • src/node/init-chain.ts
  • src/tools/ckb-tui.ts
  • src/tools/proxy-events.ts
  • src/tools/rpc-proxy.ts
  • tests/ckb-tui-checksum.test.ts
  • tests/ckb-tui-install.test.ts
  • tests/init-chain.test.ts
  • tests/log-subscription.test.ts
  • tests/logs-command.test.ts
  • tests/logs.test.ts
  • tests/node-quiet-mode.test.ts
  • tests/node-supervisor.test.ts
  • tests/node-terminal-rpc.test.ts
  • tests/proxy-events.test.ts
  • tests/setting.test.ts

Comment thread src/cmd/logs.ts
Comment on lines +32 to +36
let lines = readLogTail(filePath, tail);
const scriptOnly = target === 'script';
if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET);
if (options.grep) lines = grepLines(lines, options.grep);
for (const line of lines) logger.info(line);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

offckb logs script can print nothing because the filter runs after the tail.

readLogTail returns the last tail lines of run.log, then filterLinesByTarget keeps only ckb-script entries. A devnet node writes many non-script lines per second. So the last 100 lines usually contain no script entry, and the command prints an empty result even though script output exists earlier in the file.

Read a larger window when a target filter is active, then trim to tail after filtering.

🐛 Proposed fix
 export function showLogs(target: LogTarget, options: LogsOptions, settings: Settings, logger: UnifiedLogger): void {
   const filePath = resolveLogPath(target, settings);
   const tail = options.tail ?? DEFAULT_TAIL;
-
-  let lines = readLogTail(filePath, tail);
   const scriptOnly = target === 'script';
-  if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET);
+  // Script entries are sparse in run.log, so scan a wider window and trim
+  // back to `tail` after filtering.
+  const scanWindow = scriptOnly ? tail * 100 : tail;
+
+  let lines = readLogTail(filePath, scanWindow);
+  if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET).slice(-tail);
   if (options.grep) lines = grepLines(lines, options.grep);
   for (const line of lines) logger.info(line);
📝 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
let lines = readLogTail(filePath, tail);
const scriptOnly = target === 'script';
if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET);
if (options.grep) lines = grepLines(lines, options.grep);
for (const line of lines) logger.info(line);
const scriptOnly = target === 'script';
// Script entries are sparse in run.log, so scan a wider window and trim
// back to `tail` after filtering.
const scanWindow = scriptOnly ? tail * 100 : tail;
let lines = readLogTail(filePath, scanWindow);
if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET).slice(-tail);
if (options.grep) lines = grepLines(lines, options.grep);
for (const line of lines) logger.info(line);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/logs.ts` around lines 32 - 36, Adjust the log retrieval flow around
readLogTail and filterLinesByTarget so target-filtered requests read a
sufficiently large window before filtering, then limit the filtered results to
the requested tail count. Preserve the existing grep behavior and ensure
unfiltered logs continue using the current tail semantics.

Comment thread src/cmd/logs.ts
Comment on lines +54 to +56
export function logsCommand(target: string | undefined, options: LogsOptions): void {
const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node';
showLogs(resolved, options, readSettings(), defaultLogger);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an unknown log target instead of falling back to node.

logsCommand maps any unrecognized target to 'node'. A typo such as offckb logs scrpit then prints the node log with no warning, and the user reads the wrong output. Validate the argument and report the accepted values.

🐛 Proposed fix
 export function logsCommand(target: string | undefined, options: LogsOptions): void {
-  const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node';
+  if (target != null && !LOG_TARGETS.includes(target as LogTarget)) {
+    throw new Error(`Unknown log target "${target}". Use one of: ${LOG_TARGETS.join(', ')}.`);
+  }
+  const resolved: LogTarget = (target as LogTarget) ?? 'node';
   showLogs(resolved, options, readSettings(), defaultLogger);
 }
📝 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
export function logsCommand(target: string | undefined, options: LogsOptions): void {
const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node';
showLogs(resolved, options, readSettings(), defaultLogger);
export function logsCommand(target: string | undefined, options: LogsOptions): void {
if (target != null && !LOG_TARGETS.includes(target as LogTarget)) {
throw new Error(`Unknown log target "${target}". Use one of: ${LOG_TARGETS.join(', ')}.`);
}
const resolved: LogTarget = (target as LogTarget) ?? 'node';
showLogs(resolved, options, readSettings(), defaultLogger);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/logs.ts` around lines 54 - 56, Update logsCommand to reject
unrecognized target values instead of defaulting them to 'node'. Validate the
provided target against LOG_TARGETS, report an error that includes the accepted
values, and only call showLogs for valid targets or when the target is omitted
and should use the default.

Comment thread src/devnet/log-file.ts
Comment on lines +124 to +131
const onChange = (curr: fs.Stats, prev: fs.Stats) => {
if (curr.size < prev.size || curr.size < offset) {
// Truncated or rotated: restart from the beginning.
offset = 0;
partial = '';
decoder = new TextDecoder('utf-8');
}
if (curr.size === offset) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rotation is modeled as a size decrease in followLogFile and in its test. fs.watchFile polls a path, so a rotated log is a new inode at the same path. The shared root cause is that both the implementation and the test treat rotation as a shrinking file, which misses a replacement file that is already larger than the previous one.

  • src/devnet/log-file.ts#L124-L131: add curr.ino !== prev.ino to the reset condition, next to the existing size checks.
  • tests/logs.test.ts#L197-L201: replace the in-place fs.writeFileSync truncation with a fs.renameSync of run.log followed by writing a larger replacement file, then assert that the new content is emitted from offset 0.
📍 Affects 2 files
  • src/devnet/log-file.ts#L124-L131 (this comment)
  • tests/logs.test.ts#L197-L201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devnet/log-file.ts` around lines 124 - 131, Update followLogFile’s
onChange reset condition in src/devnet/log-file.ts:124-131 to also reset when
curr.ino differs from prev.ino, alongside the existing size checks. Update
tests/logs.test.ts:197-201 to simulate rotation by renaming run.log, writing a
larger replacement at the original path, and asserting the replacement content
is emitted from offset 0.

Comment thread src/node/init-chain.ts
Comment on lines +74 to +77
export function supportsTerminalRpcModule(ckbVersion: string | null | undefined): boolean {
if (ckbVersion == null || !semver.valid(ckbVersion)) return true;
return semver.gte(ckbVersion, TERMINAL_RPC_MIN_CKB_VERSION);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

semver satisfies ">=1.2.3-0" prerelease range behavior

💡 Result:

In the npm semver implementation, the range >=1.2.3-0 is designed to explicitly include prerelease versions [1][2]. By default, the semver library excludes prerelease versions from range matching unless the range itself contains a prerelease tag [3][4]. This design ensures that users who have not explicitly opted into prerelease versions are not accidentally upgraded to unstable builds [3][5]. When you use the comparator >=1.2.3-0: 1. Opt-in Behavior: Because the range includes a prerelease identifier (-0), the semver matcher enables prerelease matching for the associated [major, minor, patch] tuple (in this case, 1.2.3) [3][4]. 2. The -0 suffix: The identifier -0 is used as a convention because it is the lowest possible prerelease version [1]. By setting the bound to 1.2.3-0, you instruct the system to include 1.2.3 and any of its subsequent prerelease versions (e.g., 1.2.3-alpha.1, 1.2.3-beta.2) [1]. 3. Range Matching: This comparator will match 1.2.3, all prerelease versions of 1.2.3 that are greater than or equal to 1.2.3-0, and all normal (non-prerelease) versions greater than 1.2.3 [6][1][7]. If you need to include all prerelease versions regardless of the default safety behavior, you can also use the includePrerelease flag (e.g., setting { includePrerelease: true } in the options object) when calling semver functions, which suppresses the requirement for an explicit prerelease tag in the range [3][4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)init-chain\.(ts|test\.ts)$|package(-lock|)\.json$|tsconfig' || true

echo
echo "== init-chain relevant =="
if [ -f src/node/init-chain.ts ]; then
  nl -ba src/node/init-chain.ts | sed -n '1,130p'
fi

echo
echo "== test references =="
if [ -f tests/init-chain.test.ts ]; then
  rg -n "supportsTerminalRpcModule|TERMINAL_RPC_MIN_CKB_VERSION|0\.205\.0|226" tests/init-chain.test.ts -C 3 || true
fi

echo
echo "== package semver version =="
node - <<'JS'
const fs = require('fs');
for (const name of ['package.json','package-lock.json','pnpm-lock.yaml','yarn.lock']) {
  if (fs.existsSync(name)) console.log(name, JSON.parse(fs.readFileSync(name,'utf8')).dependencies?.semver || JSON.parse(fs.readFileSync(name,'utf8')).devDependencies?.semver || 'missing top-level');
}
try {
  const p = require('/tmp/semver-behavior');
} catch (e) {
  console.log('node_modules semver availability:', e.code || e.message);
}
try {
  const semver = require('semver');
  console.log('semver loaded', semver.clean(process.versions.node)+"?");
  const pairs = [
    ['0.205.0-rc1','0.205.0'],
    ['0.205.0-0','0.205.0'],
    ['0.205.0','0.205.0'],
    ['0.204.9','0.205.0'],
    ['0.205.1','0.205.0']
  ];
  for (const [a,b] of pairs) {
    console.log(a, 'gte', b, '=>', semver.gte(a,b), 'satisfies >=', b+'-0', '=>', semver.satisfies(a, '>='+b+'-0'));
  }
} catch (e) {
  console.log('require semver failed', e.message);
}
JS

Repository: ckb-devrel/offckb

Length of output: 415


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== initial chain references =="
awk '{printf "%5d: %s\n", NR, $0}' src/node/init-chain.ts | sed -n '1,130p'

echo
echo "== test references =="
if [ -f tests/init-chain.test.ts ]; then
  awk '{printf "%5d: %s\n", NR, $0}' tests/init-chain.test.ts | sed -n '210,235p'
  rg -n "supportsTerminalRpcModule|TERMINAL_RPC_MIN_CKB_VERSION|0\.205\.0|226" tests/init-chain.test.ts -C 3 || true
fi

echo
echo "== package semver and runtime behavior =="
if [ -f package.json ]; then
  node - <<'JS'
  const fs = require('fs');
  const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
  console.log(JSON.stringify({
    dep: pkg.dependencies?.semver,
    devDep: pkg.devDependencies?.semver
  }));
  JS
fi

if command -v node >/dev/null 2>&1; then
  node - <<'JS'
  try {
    const semver = require('semver');
    console.log('semver version', semver.SEMVER_SPEC_VERSION || 'unknown');
    const pairs = [
      ['0.205.0-rc1','0.205.0'],
      ['0.205.0-0','0.205.0'],
      ['0.205.0','0.205.0'],
      ['0.204.9','0.205.0'],
      ['0.205.1','0.205.0'],
      ['0.205.0-alpha','0.205.0'],
      ['0.205.1-alpha','0.205.0']
    ];
    for (const [a,b] of pairs) {
      console.log(`${a} gte ${b} => ${semver.gte(a,b)} saturifes >=${b}-0 => ${semver.satisfies(a, '>=' + b + '-0')}`);
    }
  } catch (e) {
    console.log('require semver failed:', e.message);
  }
  JS
fi

Repository: ckb-devrel/offckb

Length of output: 10227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package semver versions =="
node - <<'JS'
const fs = require('fs');
if (fs.existsSync('package.json')) {
  const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
  console.log(JSON.stringify({
    dependencies_semver: pkg.dependencies && pkg.dependencies.semver,
    devDependencies_semver: pkg.devDependencies &&(pkg.devDependencies.semver),
  }));
}
const files = Array.from(fs.readdirSync('.'));
for (const name of ['package-lock.json','pnpm-lock.yaml','yarn.lock','npm-shrinkwrap.json']) {
  if (files.includes(name)) console.log(name, 'exists');
}
JS

node - <<'JS'
try {
  const semver = require('semver');
  console.log('available', true, semver.version || '');
  const map = {
    cgtEq: String(semver.gte('0.205.0-rc1','0.205.0')),
    satisfiesZero: String(semver.satisfies('0.205.0-rc1','>=0.205.0-0')),
    satisfiesEq: String(semver.satisfies('0.205.0-rc1','>=0.205.0')),
    satisfy02049: String(semver.satisfies('0.204.9','>=0.205.0-0')),
    satisfy02050: String(semver.satisfies('0.205.0','>=0.205.0-0')),
    satisfy02070: String(semver.satisfies('0.207.0','>=0.205.0-0')),
    satisfiesWithPrerelease: String(semver.satisfies('0.205.0-beta.1', '>=0.205.0-beta.0')),
    satisfy02050Beta: String(semver.satisfies('0.205.0-beta.1','>=0.205.0-0'))
  };
  console.log(JSON.stringify(map));
} catch (e) {
  console.log('semver unavailable', e.code || e.message);
}
JS

Repository: ckb-devrel/offckb

Length of output: 445


Allow CKB 0.205.0 prereleases in Terminal RPC gating.

semver.gte('0.205.0-rc1', '0.205.0') is false, so 0.205.0-rc* binaries are treated as pre-Terminal and have rpc.modules stripped. Use semver.satisfies(ckbVersion, '>=0.205.0-0') and update the test expectation accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/node/init-chain.ts` around lines 74 - 77, Update
supportsTerminalRpcModule to use semver.satisfies with the range >=0.205.0-0,
while preserving the existing null and invalid-version behavior. Update the
related test expectation so 0.205.0 prerelease versions, including rc builds,
are accepted for Terminal RPC support.

Comment thread src/tools/ckb-tui.ts
Comment on lines +109 to +138
/**
* Verifies an existing on-disk binary against the configured version's
* pinned binary digest. Returns true when the binary may be kept: either it
* matches the digest, or the configured version has no pinned binary digest
* (presence-only fallback; install-time archive verification still applies).
* Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count
* as a mismatch so the reinstall flow runs instead of crashing with a raw fs
* error, blocking forever on a special file, or failing later at spawn time.
*/
private static installedBinaryMatches(binaryPath: string): boolean {
const settings = readSettings();
const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()];
try {
// Require a regular file first: opening a FIFO or device for reading
// would block indefinitely, before any digest comparison could run.
if (!fs.statSync(binaryPath).isFile()) {
return false;
}
// Metadata alone does not prove readability; an unreadable binary must
// flow into the reinstall path (which republishes with correct modes).
fs.accessSync(binaryPath, fs.constants.R_OK);
if (!expected) {
return true;
}
const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex');
return actual === expected;
} catch {
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check X_OK, not only R_OK, or correct the doc claim.

Line 116 states that the check prevents "failing later at spawn time". accessSync(binaryPath, fs.constants.R_OK) proves readability only. A binary whose mode lost the execute bit passes both the readability check and the digest check, so ensureInstalled returns it and spawnSync then fails with EACCES. Add an execute check on POSIX so such a binary flows into the reinstall path, which republishes with mode 0o755.

🛡️ Proposed fix
       // Metadata alone does not prove readability; an unreadable binary must
       // flow into the reinstall path (which republishes with correct modes).
       fs.accessSync(binaryPath, fs.constants.R_OK);
+      // The binary is spawned directly, so a lost execute bit must also count
+      // as a mismatch. Windows reports X_OK for any readable file.
+      if (process.platform !== 'win32') {
+        fs.accessSync(binaryPath, fs.constants.X_OK);
+      }
       if (!expected) {
         return true;
       }
📝 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
/**
* Verifies an existing on-disk binary against the configured version's
* pinned binary digest. Returns true when the binary may be kept: either it
* matches the digest, or the configured version has no pinned binary digest
* (presence-only fallback; install-time archive verification still applies).
* Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count
* as a mismatch so the reinstall flow runs instead of crashing with a raw fs
* error, blocking forever on a special file, or failing later at spawn time.
*/
private static installedBinaryMatches(binaryPath: string): boolean {
const settings = readSettings();
const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()];
try {
// Require a regular file first: opening a FIFO or device for reading
// would block indefinitely, before any digest comparison could run.
if (!fs.statSync(binaryPath).isFile()) {
return false;
}
// Metadata alone does not prove readability; an unreadable binary must
// flow into the reinstall path (which republishes with correct modes).
fs.accessSync(binaryPath, fs.constants.R_OK);
if (!expected) {
return true;
}
const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex');
return actual === expected;
} catch {
return false;
}
}
/**
* Verifies an existing on-disk binary against the configured version's
* pinned binary digest. Returns true when the binary may be kept: either it
* matches the digest, or the configured version has no pinned binary digest
* (presence-only fallback; install-time archive verification still applies).
* Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count
* as a mismatch so the reinstall flow runs instead of crashing with a raw fs
* error, blocking forever on a special file, or failing later at spawn time.
*/
private static installedBinaryMatches(binaryPath: string): boolean {
const settings = readSettings();
const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()];
try {
// Require a regular file first: opening a FIFO or device for reading
// would block indefinitely, before any digest comparison could run.
if (!fs.statSync(binaryPath).isFile()) {
return false;
}
// Metadata alone does not prove readability; an unreadable binary must
// flow into the reinstall path (which republishes with correct modes).
fs.accessSync(binaryPath, fs.constants.R_OK);
// The binary is spawned directly, so a lost execute bit must also count
// as a mismatch. Windows reports X_OK for any readable file.
if (process.platform !== 'win32') {
fs.accessSync(binaryPath, fs.constants.X_OK);
}
if (!expected) {
return true;
}
const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex');
return actual === expected;
} catch {
return false;
}
}
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] 132-132: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(binaryPath)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/ckb-tui.ts` around lines 109 - 138, Update installedBinaryMatches
to validate execute permission in addition to readability on POSIX systems,
using fs.accessSync with X_OK so binaries without the execute bit return false
and enter the reinstall path. Preserve the existing regular-file, digest, and
fallback behavior, while handling platforms where POSIX execute checks are not
applicable.

Comment thread src/tools/proxy-events.ts
Comment on lines +63 to +76
const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`;
if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
if (size > 0 && size + Buffer.byteLength(line) > maxBytes) {
fs.rmSync(`${filePath}.1`, { force: true });
fs.renameSync(filePath, `${filePath}.1`);
size = 0;
}
fs.appendFileSync(filePath, line);
size += Buffer.byteLength(line);
} catch {
// Logging must never break request forwarding.
dirReady = false;
size = -1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A failed rollover stops all further event logging.

If fs.renameSync fails, the catch block drops the event and sets size = -1. The next event re-stats the file, finds it still over maxBytes, and attempts the same rename again. On Windows a rename fails while another process holds the file open, so every later event is dropped and proxy.log stops growing. The user then sees an empty offckb logs rpc with no error.

Separate the rollover from the append. If the rollover fails, keep appending.

🐛 Proposed fix
         const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`;
         if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
         if (size > 0 && size + Buffer.byteLength(line) > maxBytes) {
-          fs.rmSync(`${filePath}.1`, { force: true });
-          fs.renameSync(filePath, `${filePath}.1`);
-          size = 0;
+          try {
+            fs.rmSync(`${filePath}.1`, { force: true });
+            fs.renameSync(filePath, `${filePath}.1`);
+            size = 0;
+          } catch {
+            // Rollover failed (for example the file is locked). Keep
+            // appending so events are not lost, and retry later.
+            size = maxBytes;
+          }
         }
         fs.appendFileSync(filePath, line);
         size += Buffer.byteLength(line);
📝 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
const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`;
if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
if (size > 0 && size + Buffer.byteLength(line) > maxBytes) {
fs.rmSync(`${filePath}.1`, { force: true });
fs.renameSync(filePath, `${filePath}.1`);
size = 0;
}
fs.appendFileSync(filePath, line);
size += Buffer.byteLength(line);
} catch {
// Logging must never break request forwarding.
dirReady = false;
size = -1;
}
const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`;
if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
if (size > 0 && size + Buffer.byteLength(line) > maxBytes) {
try {
fs.rmSync(`${filePath}.1`, { force: true });
fs.renameSync(filePath, `${filePath}.1`);
size = 0;
} catch {
// Rollover failed (for example the file is locked). Keep
// appending so events are not lost, and retry later.
size = maxBytes;
}
}
fs.appendFileSync(filePath, line);
size += Buffer.byteLength(line);
} catch {
// Logging must never break request forwarding.
dirReady = false;
size = -1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 63 - 76, Separate rollover operations
from event appending in the logging flow around the existing fs.renameSync and
fs.appendFileSync calls. If rollover deletion or renaming fails, retain the
current log file and continue to append the event instead of entering the catch
path that drops it or leaves size invalid; update size consistently after the
append so subsequent events can retry rollover when appropriate.

Comment thread src/tools/proxy-events.ts
Comment on lines +86 to +111
export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void {
if (reqData.length === 0) return;

try {
const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload;
const method = jsonRpcContent.method;
const params = jsonRpcContent.params;
ctx.sink.debug('RPC Req: ', method);
if (typeof method === 'string') {
ctx.events.event(`request ${method}`);
}

if (method === 'send_transaction') {
const tx = (params as unknown[])[0];
const txHash = ctx.hashTransaction(tx);
if (!fs.existsSync(ctx.transactionsPath)) {
fs.mkdirSync(ctx.transactionsPath, { recursive: true });
}
const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`);
fs.writeFileSync(txFile, JSON.stringify(tx, null, 2));
// The hash line mirrors the RPC method name on purpose: at request time
// the proxy does not yet know whether the node will accept the tx, and
// any rejection surfaces separately as an RPC error line.
ctx.sink.info(`send_transaction: ${txHash}`);
ctx.events.event(`send_transaction ${txHash}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle batch request payloads and non-array params.

Two gaps in this handler:

  1. handleProxyResponseBody accepts an array payload (line 131), but this function does not. A JSON-RPC batch request has no top-level method, so a batched send_transaction is never recorded and its transaction file is never written.
  2. (params as unknown[])[0] throws a TypeError when params is absent or not an array. The catch then reports "Error parsing JSON-RPC req content", which points at the wrong cause.

Normalize the payload to an array and guard params.

🐛 Proposed fix
 export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void {
   if (reqData.length === 0) return;
 
   try {
-    const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload;
+    const parsed = JSON.parse(reqData) as JsonRpcRequestPayload | JsonRpcRequestPayload[];
+    for (const jsonRpcContent of Array.isArray(parsed) ? parsed : [parsed]) {
+      handleOneRequest(jsonRpcContent, ctx);
+    }
+  } catch (err) {
+    ctx.sink.error('Error parsing JSON-RPC req content:', (err as Error).message);
+  }
+}
+
+function handleOneRequest(jsonRpcContent: JsonRpcRequestPayload, ctx: ProxyEventContext): void {
     const method = jsonRpcContent.method;
     const params = jsonRpcContent.params;
     ctx.sink.debug('RPC Req: ', method);
     if (typeof method === 'string') {
       ctx.events.event(`request ${method}`);
     }
 
     if (method === 'send_transaction') {
-      const tx = (params as unknown[])[0];
+      if (!Array.isArray(params) || params.length === 0) {
+        ctx.sink.warn('send_transaction request has no params; skipping tx dump');
+        return;
+      }
+      const tx = params[0];
       const txHash = ctx.hashTransaction(tx);
📝 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
export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void {
if (reqData.length === 0) return;
try {
const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload;
const method = jsonRpcContent.method;
const params = jsonRpcContent.params;
ctx.sink.debug('RPC Req: ', method);
if (typeof method === 'string') {
ctx.events.event(`request ${method}`);
}
if (method === 'send_transaction') {
const tx = (params as unknown[])[0];
const txHash = ctx.hashTransaction(tx);
if (!fs.existsSync(ctx.transactionsPath)) {
fs.mkdirSync(ctx.transactionsPath, { recursive: true });
}
const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`);
fs.writeFileSync(txFile, JSON.stringify(tx, null, 2));
// The hash line mirrors the RPC method name on purpose: at request time
// the proxy does not yet know whether the node will accept the tx, and
// any rejection surfaces separately as an RPC error line.
ctx.sink.info(`send_transaction: ${txHash}`);
ctx.events.event(`send_transaction ${txHash}`);
}
export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void {
if (reqData.length === 0) return;
try {
const parsed = JSON.parse(reqData) as JsonRpcRequestPayload | JsonRpcRequestPayload[];
for (const jsonRpcContent of Array.isArray(parsed) ? parsed : [parsed]) {
handleOneRequest(jsonRpcContent, ctx);
}
} catch (err) {
ctx.sink.error('Error parsing JSON-RPC req content:', (err as Error).message);
}
}
function handleOneRequest(jsonRpcContent: JsonRpcRequestPayload, ctx: ProxyEventContext): void {
const method = jsonRpcContent.method;
const params = jsonRpcContent.params;
ctx.sink.debug('RPC Req: ', method);
if (typeof method === 'string') {
ctx.events.event(`request ${method}`);
}
if (method === 'send_transaction') {
if (!Array.isArray(params) || params.length === 0) {
ctx.sink.warn('send_transaction request has no params; skipping tx dump');
return;
}
const tx = params[0];
const txHash = ctx.hashTransaction(tx);
if (!fs.existsSync(ctx.transactionsPath)) {
fs.mkdirSync(ctx.transactionsPath, { recursive: true });
}
const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`);
fs.writeFileSync(txFile, JSON.stringify(tx, null, 2));
// The hash line mirrors the RPC method name on purpose: at request time
// the proxy does not yet know whether the node will accept the tx, and
// any rejection surfaces separately as an RPC error line.
ctx.sink.info(`send_transaction: ${txHash}`);
ctx.events.event(`send_transaction ${txHash}`);
}
}
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 104-104: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(txFile, JSON.stringify(tx, null, 2))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/proxy-events.ts` around lines 86 - 111, Update
handleProxyRequestBody to normalize the parsed JSON-RPC payload into a
single-item or batch array, then process each request entry so batched
send_transaction calls are recorded and persisted. Before accessing the first
parameter, validate that params is an array; skip transaction handling for
missing or non-array params instead of allowing a TypeError to be reported as a
JSON parsing failure.

Comment thread src/tools/rpc-proxy.ts
Comment on lines +15 to +23
const ctx = {
sink: logger,
events,
transactionsPath: settings[network].transactionsPath,
hashTransaction: (tx: unknown) => {
const cccTx = cccA.JsonRpcTransformers.transactionTo(tx);
return cccTx.hash() as string;
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify UnifiedLogger exposes debug/warn and how levels are configured.
ast-grep outline src/util/logger.ts --items all

rg -nP --type=ts -C3 '\b(debug|warn)\s*\(' src/util/logger.ts

# Check how the log level is selected, including any verbose flag wiring.
rg -nP --type=ts -C4 "level\s*[:=]|'debug'|\"debug\"|verbose" src/util/logger.ts src/cmd/node.ts src/cli.ts

Repository: ckb-devrel/offckb

Length of output: 11531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant logger initialization, ProxyEventContext, and rpc-proxy context construction.
sed -n '24,46p' src/util/logger.ts
printf '\n--- logger.ts debug/warn around processParams/output handling ---\n'
sed -n '112,155p' src/util/logger.ts
sed -n '230,264p' src/util/logger.ts
printf '\n--- logger.ts create() default config and usages ---\n'
sed -n '100,108p' src/util/logger.ts
rg -n --type=ts '\bUnifiedLogger\.create|process\.argv|LOG_LEVEL|verbose' src -C 2

printf '\n--- proxy-events context and calls ---\n'
sed -n '1,220p' src/tools/proxy-events.ts

printf '\n--- rpc-proxy relevant lines ---\n'
sed -n '1,90p' src/tools/rpc-proxy.ts

Repository: ckb-devrel/offckb

Length of output: 17331


Add ProxyEventContext annotation and connect --verbose to proxy debug logging.

UnifiedLogger has debug, warn, info, and error, so it satisfies ProxyLogSink. The global logger instance still uses Winston’s default info level; proxy request logging is at ctx.sink.debug(...), so per-request “RPC Req” lines are emitted only when LOG_LEVEL=debug. Wire --verbose into UnifiedLogger/Winston for RPC proxy startup or document that --verbose affects node/miner stdout only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/rpc-proxy.ts` around lines 15 - 23, Annotate the ctx object in the
RPC proxy setup with the ProxyEventContext type, then connect the --verbose
option to the UnifiedLogger/Winston log-level configuration so ctx.sink.debug
emits proxy request logs during verbose runs; otherwise explicitly document that
--verbose does not enable proxy debug logging.

@RetricSu
RetricSu merged commit 2bf0db7 into master Jul 31, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants