Commit 1d306cd
feat(cli): chfx — decode / query / capture ClickHouse wire formats to JSON (#43)
* feat(cli): add chfx CLI with decode command (items 1 + 4)
A publish-ready npm bin (`chfx`) that decodes ClickHouse wire-format dumps
to structured JSON for humans and agents. Reuses the src/core decoders
(DOM-free) and bundles to a single ESM file via esbuild.
- `chfx decode [file]`: decode a .chproto capture, raw Native body, or raw
RowBinaryWithNamesAndTypes body. Autodetects .chproto by magic and raw
bodies by trial decode; `--format` forces it. Reads stdin when no path
(or `-`) is given. `--protocol-version` sets the Native client version.
- Output: the web ParsedData/AstNode tree as JSON, a top-level `bytesHex`
(whole decoded buffer once), and per-node inline `bytes` by default so a
consumer can read a value's bytes without slicing by range
(`--no-node-bytes` to omit). bigints → decimal strings, byte blobs → hex.
- Agent-friendly: deterministic JSON on stdout, JSON error envelope on
stderr, exit codes (0 ok / 2 usage / 1 io|decode), `--help`/`--version`,
non-interactive.
Packaging: `bin`/`files`/`prepublishOnly` wired; `npm run cli` (tsx, dev)
and `npm run cli:build` (esbuild → dist/cli/index.js). Adds esbuild, tsx,
@types/node devDeps.
Tests: src/cli/cli.test.ts — arg parsing, decode of every protocol fixture
(+ bigint-safe serialization), hand-built Native/RowBinary bodies, autodetect
+ override, per-node bytes match their range, and tsx end-to-end (stdin,
exit codes). README + AGENTS.md + docs/cli-spec.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): add query + capture commands; UX one-shot and simpler surface
Collapse the capture→file→decode dance into single commands under chfx,
and make the dump file optional.
- `chfx query --query "<sql>"`: run AND decode in one step (no temp file).
- `--protocol tcp` (default): drive clickhouse-client through the capturing
proxy and decode the native packet stream; `--save <f>` keeps the .chproto.
- `--protocol http`: POST to ClickHouse HTTP requesting `--format`
(native | RowBinaryWithNamesAndTypes, default native) and decode the body;
`--protocol-version` sets the Native client version. Port defaults 8123;
auth via X-ClickHouse-User/-Key headers.
- `chfx capture --query "<sql>"`: capture to a .chproto dump only; `--out <f>`
writes a file (+ JSON summary), otherwise streams raw bytes to stdout so
`chfx capture … | chfx decode` works. `npm run capture` is now an alias to it;
the standalone scripts/capture-native.mjs is folded in and removed.
- Shared connection flags with env fallbacks (CH_NATIVE_HOST, CH_NATIVE_PORT /
CH_HTTP_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CLICKHOUSE_CLIENT) and
experimental type settings on by default (--no-experimental-settings,
repeatable --setting k=v).
Refactor: extract decodeCaptureStreams + buildDecodeEnvelope (shared by decode
and query); commands return a JSON|raw CommandOutput union the entry point
renders. Arg parser gains repeatable multiFlags (--setting). The TS CLI imports
the JS proxy via a new scripts/native-proxy.d.mts declaration; query/capture
reuse the same captureQuery the web/Electron paths use.
Docs: README quick start now leads with `chfx query` and `npm link`; full
transport/connection option tables. AGENTS.md + docs/cli-spec.md updated.
Tests: query (tcp via injected capture; http via injected fetch for Native +
RowBinary + error + flag-validation), capture (raw stdout + file summary),
repeatable-flag parsing, and connection/env resolution. 44 tests pass; verified
end-to-end against a live server on both transports.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): address review findings (OOM on tiny input, EPIPE, stale schema help)
- Blocker: a 0-column RowBinary header made decodeRows loop forever
(offset never advances while remaining > 0), exhausting memory — trivially
reachable since `decode` autodetect trials RowBinary first. `printf '\x00\x02'
| chfx decode -` OOM'd the process. Guard the row loop to break on a
non-advancing iteration (rowbinary-decoder.ts). Now terminates with a clean
usage error.
- High: piping decode output into a consumer that closes early (`… | head`)
threw an unhandled EPIPE and dumped a Node stack trace to stderr, violating
the clean-exit contract. Handle EPIPE on stdout/stderr and exit 0.
- Stale `schema` references: general --help advertised a `chfx schema` command
that was dropped; removed it and the registry comment.
- Classify --save / --out write failures as io errors (not decode); wrap both
writeFile calls.
- README: build before `npm link` (link points at the not-yet-built binary).
- Tidy an orphaned doc comment in connection.ts.
Tests: regression for degenerate/tiny inputs terminating (no OOM). 45 CLI tests
+ 86 core unit tests pass; lint + tsc clean; both blockers verified fixed
end-to-end (2-byte input → exit 2 usage error; `| head` → no stack trace).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): cover option combos, failure modes, env fallbacks, http request shape
Fill the gaps from the coverage review (45 → 67 tests):
- decode edge/error: empty input, missing file, invalid --protocol-version,
ambiguous raw-body autodetect.
- query http request construction (spied fetch): default_format, --setting and
--database query params, X-ClickHouse-User/-Key headers, body, port 8123, and
--protocol-version → client_protocol_version; RowBinaryWithNamesAndTypes format.
- query/capture failure modes: tcp capture throw → io, --save write failure → io,
empty http body → decode, http transport throw → io, unknown --protocol,
unknown http --format, capture-command throw → io.
- capture: -o alias, --out - raw stdout.
- connection: CH_NATIVE_HOST/PORT + CH_HTTP_PORT env fallbacks and flag
precedence, resolveHttpConnection defaults, --setting overriding an experimental
default (added a withEnv save/restore helper and a shared fakeCaptureOf).
- e2e via tsx: --version, unknown-command exit 2, and a clean-exit-on-EPIPE
check (decode | head closes early → no stack trace).
eslint + tsc clean; 67 CLI tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(capture): don't hang when clickhouse-client exits before connecting
Fuzz testing found that a TCP `chfx query`/`capture` hangs forever on any flag
the client rejects at startup (e.g. --setting totally_fake_setting_xyz=1 or a
bad setting value): clickhouse-client exits before opening the proxied TCP
connection, so the proxy's `done` promise (which only resolves once both ends
of a connection close) never settles and `await done` blocks indefinitely.
Fix in scripts/native-proxy.mjs (shared by the web/Electron/CLI capture paths):
- Settle `done` exactly once via finishOk/finishErr, and make the proxy's
close() resolve `done` with whatever was captured so far.
- In captureQuery, on a non-zero client exit, race `done` against a 100ms grace
window then force-close — so a pre-connect failure yields a clean io error
("clickhouse-client exited 40: …") instead of a hang. The happy path and the
connected-but-failed path (server Exception captured) are unchanged.
Regression test uses `false` as the client (exits before connecting) so it
needs no server. 68 CLI tests pass; lint + tsc clean; verified against the live
server that the original repros now return a clean error and normal queries
still work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): reject unknown options and unexpected positionals (review)
Address Copilot review on PR #43: the permissive parser meant commands
silently ignored unknown flags (a typo like `--protcol http` ran the default
tcp path) and extra positionals. Add rejectUnknownArgs(allowed, maxPositionals)
and call it in decode/query/capture so unrecognized options or surplus
arguments fail fast as `usage` errors (exit 2), matching the documented
contract. Also correct the stringOption doc comment (it errors on a repeated
*multi* flag, not any repeat).
Tests: unknown-flag + extra-positional rejection for decode/query/capture.
73 CLI tests pass; lint + tsc clean; verified `--protcol` now errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): reject --save '-' and out-of-range ports (review round 2)
Address the second Copilot review on PR #43:
- query --save '-' previously wrote a file literally named "-"; since stdout
carries the decoded JSON, reject "-" as a usage error with a clear message.
- --port accepted values > 65535 (only >0 was checked); require 1..65535 so
invalid ports fail fast with a clear message instead of at connect time.
Tests added for both. 75 CLI tests pass; lint + tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent a31a6d9 commit 1d306cd
21 files changed
Lines changed: 2518 additions & 217 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
39 | 45 | | |
40 | 46 | | |
41 | 47 | | |
| |||
78 | 84 | | |
79 | 85 | | |
80 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
81 | 99 | | |
82 | 100 | | |
83 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
44 | 139 | | |
45 | 140 | | |
46 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
53 | 66 | | |
54 | 67 | | |
55 | 68 | | |
| |||
66 | 79 | | |
67 | 80 | | |
68 | 81 | | |
69 | | - | |
70 | | - | |
71 | | - | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
72 | 89 | | |
73 | 90 | | |
74 | 91 | | |
75 | 92 | | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
80 | 102 | | |
81 | 103 | | |
82 | 104 | | |
| |||
0 commit comments