Skip to content

Commit 6f54296

Browse files
feat: add daemon mode and --json output (#448)
* feat: add daemon mode and --json output for agent-friendly usage - Add --daemon flag to offckb node to run the devnet in the background - Add global --json flag for structured JSON log output - Write daemon PID and logs to devnet data folder - Add tests for logger JSON mode and node daemon spawning - Update README with daemon and --json usage Fixes #446 Co-Authored-By: Claude <noreply@anthropic.com> * feat: add node stop command Add offckb node stop to terminate the devnet daemon started by offckb node --daemon. It reads the PID file, sends SIGTERM, waits for graceful shutdown, falls back to SIGKILL if needed, and removes the PID file. Also add tests and update README to mention node stop instead of manual kill. Co-Authored-By: Claude <noreply@anthropic.com> * ci: fix formatting, windows test paths, and add changeset - Run prettier so lint job's git diff --exit-code passes - Use path.join in node-command tests so Windows path assertions pass - Add changeset for daemon mode, --json output, and node stop command Co-Authored-By: Claude <noreply@anthropic.com> * chore: change changeset level from minor to patch Co-Authored-By: Claude <noreply@anthropic.com> * fix(node): harden daemon lifecycle per test team review - Reject duplicate daemon starts when PID file points to a live process. - Verify target process identity before stopNode sends signals. - Harden CLI entry resolution with OFFCKB_CLI_PATH fallback and file validation. - Fix stopNode race condition between existsSync/readFileSync by reading once. - Always clean up PID file even when signal delivery fails. - Handle spawn sync exceptions, missing child.pid, and log dir creation failures. - Use taskkill on Windows to terminate the daemon process tree. - Distinguish EPERM vs ESRCH and surface clear error messages. Co-Authored-By: Claude <noreply@anthropic.com> * test(node): make daemon lifecycle tests platform-aware for Windows CI - Assert the resolved script path so Windows backslash normalization passes. - Mock WMIC output with the CommandLine= prefix required by the parser. - Normalize process.platform to linux in stop tests for deterministic POSIX signal assertions; the Windows taskkill path is covered by the daemon spawn tests and integration tests. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b5b41e7 commit 6f54296

7 files changed

Lines changed: 883 additions & 10 deletions

File tree

.changeset/brave-falcons-dance.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@offckb/cli": patch
3+
---
4+
5+
Add daemon mode and structured JSON output for agent-friendly usage, plus a `node stop` command to terminate the daemon.
6+
7+
- `offckb node --daemon` starts the CKB devnet as a detached background process and writes the PID and logs to the devnet data folder.
8+
- `offckb --json <command>` emits structured JSON log output for programmatic consumption.
9+
- `offckb node stop` reads the daemon PID file and gracefully shuts down the daemon, falling back to force-kill if necessary. It now verifies the target process identity, handles stale PID files, and cleans up on error paths.
10+
- Hardened daemon lifecycle: duplicate daemon starts are rejected, CLI entry resolution supports packaged/npx environments via `OFFCKB_CLI_PATH`, and log/PID directory creation failures are handled gracefully.

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Options:
7070

7171
Commands:
7272
node [CKB-Version] Use the CKB to start devnet
73+
node stop Stop the running CKB devnet daemon
7374
create [options] [project-name] Create a new CKB Smart Contract project in JavaScript.
7475
deploy [options] Deploy contracts to different networks, only supports devnet and testnet
7576
debug [options] Quickly debug transaction with tx-hash
@@ -118,6 +119,40 @@ offckb node --binary-path /path/to/your/ckb/binary
118119

119120
When using `--binary-path`, it will ignore the specified version and network, and only work for devnet.
120121

122+
**Run in Daemon Mode**
123+
124+
Start the devnet in the background so your terminal stays free:
125+
126+
```sh
127+
offckb node --daemon
128+
```
129+
130+
The daemon writes its logs and PID to the devnet data folder, for example:
131+
132+
- Logs: `~/Library/Application Support/offckb-nodejs/devnet/data/logs/daemon.log`
133+
- PID file: `~/Library/Application Support/offckb-nodejs/devnet/data/logs/daemon.pid`
134+
135+
Stop the daemon later with:
136+
137+
```sh
138+
offckb node stop
139+
```
140+
141+
**Agent-Friendly JSON Output**
142+
143+
For programmatic consumption or agent integration, add `--json` to any command to emit structured JSON logs:
144+
145+
```sh
146+
offckb node --json
147+
offckb node --daemon --json
148+
```
149+
150+
Each log line is a single JSON object:
151+
152+
```json
153+
{"level":"info","message":"Launching CKB devnet Node...","timestamp":"2026-07-07T07:10:00.000Z"}
154+
```
155+
121156
**RPC & Proxy RPC**
122157

123158
When the Devnet starts:

src/cli.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import { Command } from 'commander';
3-
import { startNode } from './cmd/node';
3+
import { startNode, stopNode } from './cmd/node';
44
import { accounts } from './cmd/accounts';
55
import { clean } from './cmd/clean';
66
import { setUTF8EncodingForWindows } from './util/encoding';
@@ -28,18 +28,32 @@ setUTF8EncodingForWindows();
2828
const program = new Command();
2929
program.name('offckb').description(description).version(version).enablePositionalOptions();
3030

31-
program
31+
program.option('--json', 'Output logs in JSON format for agent/programmatic consumption');
32+
program.hook('preAction', (thisCommand) => {
33+
const opts = thisCommand.opts();
34+
if (opts.json) {
35+
logger.setJsonMode(true);
36+
}
37+
});
38+
39+
const nodeCommand = program
3240
.command('node [CKB-Version]')
3341
.description('Use the CKB to start devnet')
3442
.option('--network <network>', 'Specify the network to deploy to', 'devnet')
3543
.option(
3644
'-b, --binary-path <binaryPath>',
3745
'Specify the CKB binary path to use, only for devnet, when set, will ignore version and network',
3846
)
39-
.action(async (version: string, options: { network: Network; binaryPath?: string }) => {
40-
return startNode({ version, network: options.network, binaryPath: options.binaryPath });
47+
.option('--daemon', 'Run the node in the background as a daemon (devnet only)')
48+
.action(async (version: string, options: { network: Network; binaryPath?: string; daemon?: boolean }) => {
49+
return startNode({ version, network: options.network, binaryPath: options.binaryPath, daemon: options.daemon });
4150
});
4251

52+
nodeCommand
53+
.command('stop')
54+
.description('Stop the running CKB devnet daemon')
55+
.action(async () => stopNode());
56+
4357
program
4458
.command('create [project-name]')
4559
.description('Create a new CKB Smart Contract project in JavaScript.')

0 commit comments

Comments
 (0)