From 002e6271c3f1d713730ee5470aca22e46f429397 Mon Sep 17 00:00:00 2001 From: Jun Zhou Date: Thu, 30 Jul 2026 12:16:41 -0700 Subject: [PATCH] feat(cli): add `ccc version` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now the only way to learn the version was `ccc daemon status` or `ccc doctor`, both of which require reaching the daemon — so a user whose daemon won't start had no way to report what client they're on. `ccc version` prints the client version offline: no project discovery, no daemon, no settings. It reuses the same `_version.__version__` the daemon handshake compares against, so the CLI and the protocol check can't disagree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016YGCBE6RbF1uvvPYjfCUoW --- CLAUDE.md | 2 +- README.md | 1 + src/cocoindex_code/cli.py | 7 +++++++ tests/test_cli_helpers.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index be1572f..e130f7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ The daemon idle-exits after `daemon.idle_timeout_minutes` (default 180, 0 = neve **`server.py`** — FastMCP wrapper that delegates `search` calls to the daemon client. Also contains the legacy `main()` entry point (`cocoindex-code` command) for backward-compatible env-var-based configuration. -**`cli.py`** — Typer app with `ccc` subcommands (`init`, `index`, `search`, `grep`, `status`, `reset`, `doctor`, `mcp`, `daemon status/restart/stop`). CLI logic only — delegates to `client.py` or `grep.py`. +**`cli.py`** — Typer app with `ccc` subcommands (`init`, `index`, `search`, `grep`, `status`, `reset`, `doctor`, `mcp`, `version`, `daemon status/restart/stop`). CLI logic only — delegates to `client.py` or `grep.py`. **`grep.py`** — Structural code search (`ccc grep`) using CocoIndex's `code_match`. Runs entirely locally with no daemon or index. Matches in parallel, streams results per file. diff --git a/README.md b/README.md index f3a6b4d..6594c82 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ The background daemon starts automatically on first use. | `ccc mcp` | Run as MCP server in stdio mode | | `ccc doctor` | Run diagnostics — checks settings, daemon, model, file matching, and index health | | `ccc reset` | Delete index databases. `--all` also removes settings. `-f` skips confirmation. | +| `ccc version` | Print the CLI version | | `ccc daemon status` | Show daemon version, uptime, and loaded projects | | `ccc daemon restart` | Restart the background daemon | | `ccc daemon stop` | Stop the daemon | diff --git a/src/cocoindex_code/cli.py b/src/cocoindex_code/cli.py index a58a8c4..dfba828 100644 --- a/src/cocoindex_code/cli.py +++ b/src/cocoindex_code/cli.py @@ -22,6 +22,7 @@ SearchResponse, ) +from ._version import __version__ from .settings import ( DEFAULT_ST_MODEL, EmbeddingSettings, @@ -1120,6 +1121,12 @@ def daemon_stop() -> None: _typer.echo("Daemon stopped.") +@app.command() +def version() -> None: + """Print the CLI version.""" + _typer.echo(__version__) + + @app.command("run-daemon", hidden=True) def run_daemon_cmd() -> None: """Internal: run the daemon process.""" diff --git a/tests/test_cli_helpers.py b/tests/test_cli_helpers.py index 22467e6..07b62a5 100644 --- a/tests/test_cli_helpers.py +++ b/tests/test_cli_helpers.py @@ -288,6 +288,42 @@ def test_apply_host_cwd_noop_when_unset( assert capsys.readouterr().err == "" +# --------------------------------------------------------------------------- +# ccc version +# --------------------------------------------------------------------------- + + +def test_version_prints_client_version() -> None: + """`ccc version` reports the client version and exits 0.""" + from typer.testing import CliRunner + + from cocoindex_code._version import __version__ + from cocoindex_code.cli import app + + result = CliRunner().invoke(app, ["version"], catch_exceptions=False) + + assert result.exit_code == 0 + assert result.stdout.strip() == __version__ + + +def test_version_works_outside_a_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """No project discovery, no daemon, no settings needed.""" + from typer.testing import CliRunner + + from cocoindex_code._version import __version__ + from cocoindex_code.cli import app + + standalone = tmp_path / "not-a-project" + standalone.mkdir() + monkeypatch.chdir(standalone) + monkeypatch.setenv("COCOINDEX_CODE_DIR", str(tmp_path / "no-such-home")) + + result = CliRunner().invoke(app, ["version"], catch_exceptions=False) + + assert result.exit_code == 0 + assert result.stdout.strip() == __version__ + + # --------------------------------------------------------------------------- # ccc init — auto-populate indexing_params / query_params from curated table # ---------------------------------------------------------------------------