feat: add cg watch for real-time WebSocket price streaming#15
feat: add cg watch for real-time WebSocket price streaming#15
Conversation
Add a new `cg watch` command that streams live coin prices via CoinGecko's WebSocket API (paid plans only). Supports both an interactive table mode with price flash coloring and an NDJSON output mode for piping into other tools. Key features: - Real-time price updates (~10s) via ActionCable WebSocket protocol - Table mode with screen-clearing redraw, animated status dots, and 500ms green/red price flash on change - NDJSON mode (`-o json`) with clean broken-pipe handling - Symbol resolution via `--symbols` flag - Automatic reconnect with exponential backoff (1s→30s + jitter) - Graceful shutdown on SIGINT with unsubscribe - `--dry-run` support showing WebSocket request info - Liveness timeout (60s) to detect half-open connections New files: cmd/watch.go, cmd/watch_test.go, internal/ws/client.go, internal/ws/client_test.go. Modified: client_factory (Streamer interface), commands catalog, dryrun, display (FprintBanner, StdoutColorEnabled, VisibleWidth rune fix), docs.
- Suppress errcheck warnings for Close() calls in watch command and WS tests - Update plan-restricted error to: "this command requires a paid plan (analyst or above)" for clearer guidance
Align command help text, README section header, and command reference with the actual error message wording.
There was a problem hiding this comment.
Pull request overview
This PR adds a cg watch command for real-time WebSocket price streaming via CoinGecko's paid WebSocket API. It includes a full WebSocket client with reconnect logic, both table and NDJSON output modes, dry-run support, and display improvements for per-writer color detection and Unicode-aware width measurement.
Changes:
- New
internal/wspackage with WebSocket client implementing ActionCable protocol, automatic reconnect with exponential backoff + jitter, and graceful shutdown with unsubscribe - New
cmd/watch.gocommand supporting table mode (with animated status dots and price flash) and NDJSON mode (-o json) with broken pipe handling; plusStreamerinterface inclient_factory.gofor test injection - Display improvements:
FprintBanner(io.Writer)with per-writer color detection,StdoutColorEnabled(), andVisibleWidthfix usingutf8.RuneCountInStringfor correct Unicode measurement
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
internal/ws/client.go |
WebSocket client: connect, subscribe, readLoop with reconnect, graceful close |
internal/ws/client_test.go |
Comprehensive tests for WS client: happy path, reconnect, goroutine leak, close |
cmd/watch.go |
cg watch command with table/JSON output modes, signal handling, price flash |
cmd/watch_test.go |
Command-level tests: missing flags, demo rejection, dry-run, JSON output |
cmd/client_factory.go |
Streamer interface and newStreamer factory for test injection |
cmd/dryrun.go |
dryRunWSOutput struct and printDryRunWS for --dry-run mode |
cmd/commands.go |
watch command metadata with Transport: "websocket" |
internal/display/color.go |
StdoutIsTerminal(), StdoutColorEnabled(), VisibleWidth Unicode fix |
internal/display/banner.go |
FprintBanner(io.Writer) with per-writer color detection; PrintBanner delegates |
go.mod / go.sum |
Add gorilla/websocket v1.5.3 dependency |
README.md |
Documentation for cg watch command |
CLAUDE.md |
Project structure and conventions update for WebSocket support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
… write race gorilla/websocket doesn't support concurrent writers. Close() now holds mu while sending unsubscribe/close messages, preventing races with reconnect's subscribe/setTokens writes.
Use ErrPlanRestricted directly instead of wrapping it with a message that repeats the same information, which produced a doubled error.
- Validate --ids against /simple/price, skip unknown IDs with warning - Resolve --symbols via /search (picks highest market_cap_rank match) instead of /simple/price which returns symbol keys, not coin IDs - Dry-run now shows preflight API requests - Improved error messages with helpful hints - Added tests for invalid IDs, unresolved symbols, rank disambiguation
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
…watch table - subscribe() and setTokens() now hold mu during WriteMessage calls, matching the pattern in Close() — prevents concurrent write races - Watch table 24h% column now uses stdout-based color detection instead of stderr-based ColorPercent(), so colors are consistent when stderr is redirected
| if id := matchSymbol(res.Coins, sym); id != "" { | ||
| coinIDs = append(coinIDs, id) | ||
| } else { | ||
| warnf("could not resolve symbol %q to a coin, skipping\n", sym) |
There was a problem hiding this comment.
Consider adding syscall.SIGTERM here so cg watch shuts down cleanly when killed by a container runtime or process manager (e.g. docker stop, systemctl stop):
| warnf("could not resolve symbol %q to a coin, skipping\n", sym) | |
| signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) |
|
|
||
| case <-dotTicker.C: | ||
| dotFrame = (dotFrame + 1) % len(statusDots) | ||
| if len(state) > 0 && display.StdoutIsTerminal() { |
There was a problem hiding this comment.
\033[4;1H hardcodes the status line at row 4. If the banner format ever changes, this will silently overwrite the wrong line. Worth tracking the row dynamically or using relative cursor movement instead of an absolute row number.
| "github.com/coingecko/coingecko-cli/internal/ws" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) |
There was a problem hiding this comment.
Other paid-only commands use requirePaid() at the top of the command handler for a consistent, user-friendly error message. Here the check happens deeper in ws.Client.Connect() via cfg.IsPaid(), which gives a different (less helpful) error path for demo-plan users. Adding requirePaid() here would keep the UX consistent.
…coded row - Handle SIGTERM alongside SIGINT for clean container/process manager shutdown - Check cfg.IsPaid() early in runWatch for consistent error UX with other paid commands - Add comment noting row 4 dependency on FprintBanner layout
3ce1352 to
3202b48
Compare
- Use CoinGecko brand green (#4BCC00) for price flash instead of softer green - Create API client once before ID/symbol resolution instead of twice - Rename colorBrandGreen to colorFlashGreen for clarity
Summary
cg watchcommand that streams live coin prices via CoinGecko's WebSocket API (analyst plan or above, USD prices)-o json) for piping intojq,head, or other tools — exits cleanly on broken pipeinternal/ws) with automatic reconnect (exponential backoff 1s→30s + jitter), liveness timeout (60s), and graceful shutdown with unsubscribe--idsvalidated against/simple/pricebefore connecting — unknown IDs are skipped with a warning--symbolsresolved to coin IDs via/search(picks exact case-insensitive match with highest market cap rank)--dry-runshows preflight API requests and WebSocket payloads without connectingStreamerinterface incmd/client_factory.gofor test injectionFprintBanner(io.Writer)with per-writer color detection,StdoutColorEnabled(),VisibleWidthrune-based fixClose()WebSocket writes to prevent concurrent write race with reconnectTest plan
go test -race ./...passesmake lintcleancg watch --ids bitcoin— live table updates with price flashcg watch --ids bitcoin -o json— streams NDJSONcg watch --ids bitcoin -o json | head -5— clean exit on broken pipecg watch --ids bitcoin --dry-run— shows preflight requests + WS payloadscg watch --ids bitcoinwith demo key — shows plan restricted errorcg watch --symbols btc,eth— resolves symbols via /search and streamscg watch --ids fakecoin— warns and exits with helpful error messagecg watch --ids bitcoin,fakecoin— warns about invalid ID, streams valid onescg watch --symbols zzzzz— warns and exits with helpful error message