From a162019852d3fb7f78fb35ddb65a9d785ccef948 Mon Sep 17 00:00:00 2001 From: juanpabloguerra16 Date: Thu, 30 Apr 2026 19:58:20 -0700 Subject: [PATCH 1/4] Fix CLI search quoting for multi-word text --- src/ctxd/cli.py | 29 ++++++++++++++++++++++++++++- tests/test_cli.py | 24 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/ctxd/cli.py b/src/ctxd/cli.py index 43ab49e..43b52a2 100644 --- a/src/ctxd/cli.py +++ b/src/ctxd/cli.py @@ -4,6 +4,7 @@ import getpass import json import os +import re import sys import webbrowser from typing import Sequence @@ -30,7 +31,7 @@ def main(argv: Sequence[str] | None = None) -> int: client = Client() if args.command == "search": - result = client.search(" ".join(args.query)) + result = client.search(_normalize_search_query(args.query)) return _emit_result(result.model_dump(), as_json=True) if args.command == "fetch": result = client.fetch_document(args.document_uid) @@ -153,6 +154,32 @@ def _build_parser() -> argparse.ArgumentParser: return parser +def _normalize_search_query(query_tokens: Sequence[str]) -> str: + normalized_tokens = [ + _quote_shell_stripped_text_token(token) for token in query_tokens + ] + return " ".join(normalized_tokens) + + +def _quote_shell_stripped_text_token(token: str) -> str: + if not token.lower().startswith("text:"): + return token + + value = token[5:] + if not value or not re.search(r"\s", value): + return token + + stripped_value = value.strip() + if ( + stripped_value.startswith(("\"", "'", "(")) + or stripped_value.endswith(("\"", "'", ")")) + ): + return token + + escaped_value = stripped_value.replace("\\", "\\\\").replace('"', '\\"') + return f'text:"{escaped_value}"' + + def _handle_login(args: argparse.Namespace) -> int: del args api_key, should_save = _resolve_login_api_key() diff --git a/tests/test_cli.py b/tests/test_cli.py index 17bdf7c..4476784 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -431,6 +431,30 @@ def test_cli_search_accepts_unquoted_query_tokens() -> None: assert '"results": []' in stdout.getvalue() +def test_cli_search_restores_shell_stripped_text_quotes() -> None: + stdout = StringIO() + + with patch( + "ctxd.cli.Client.search", + return_value=type( + "SearchResultLike", + (), + { + "model_dump": lambda self: { + "results": [], + "error": None, + "dsl_parse_error": None, + } + }, + )(), + ) as search, redirect_stdout(stdout): + exit_code = main(["search", "text:deployment process", "application:slack"]) + + assert exit_code == 0 + search.assert_called_once_with('text:"deployment process" application:slack') + assert '"results": []' in stdout.getvalue() + + def test_cli_search_outputs_json_for_empty_success() -> None: stdout = StringIO() From 506aa9a16902339047797686318ae09728ae10f0 Mon Sep 17 00:00:00 2001 From: juanpabloguerra16 Date: Thu, 30 Apr 2026 20:07:33 -0700 Subject: [PATCH 2/4] Publish PyPI only from main --- .github/workflows/ci.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e6bd8d..6219990 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,6 @@ on: pull_request: push: branches: [main] - tags: ["v*"] workflow_dispatch: jobs: @@ -34,7 +33,7 @@ jobs: run: uv build publish: - if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} name: Publish to PyPI needs: test runs-on: ubuntu-latest @@ -62,6 +61,18 @@ jobs: - name: Build Distributions run: uv build + - name: Check PyPI Version + id: pypi_version + run: | + VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + if curl -fs "https://pypi.org/pypi/ctxd/$VERSION/json" > /dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Publish Distributions to PyPI + if: ${{ steps.pypi_version.outputs.exists == 'false' }} uses: pypa/gh-action-pypi-publish@release/v1 - From d5de5241e47ef22f66fb8161830bacaa4671fdde Mon Sep 17 00:00:00 2001 From: juanpabloguerra16 Date: Thu, 30 Apr 2026 20:11:27 -0700 Subject: [PATCH 3/4] Document package version bump process --- CONTRIBUTING.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8238d8c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing + +## Bumping the Package Version + +The package version is defined in `pyproject.toml` in two places: + +- `project.version` +- `tool.bumpversion.current_version` + +PyPI does not allow re-publishing the same version. Any PR that should publish a +new package must bump both values to the next version before merging. + +### Manual Version Bump + +For a patch release, update `pyproject.toml`: + +```toml +[project] +version = "0.1.14" + +[tool.bumpversion] +current_version = "0.1.14" +``` + +Then run the test suite: + +```bash +uv run pytest tests -v +``` + +Commit the version bump with the code change: + +```bash +git add pyproject.toml +git commit -m "Bump version to 0.1.14" +``` + +### Using bump-my-version + +This repository includes `tool.bumpversion` configuration. If +`bump-my-version` is installed, you can bump the patch version with: + +```bash +uv tool run bump-my-version bump patch +``` + +Review the resulting `pyproject.toml` change before committing it. + +### Publishing + +Publishing is handled by GitHub Actions. Pull requests run tests and build the +package, but do not publish to PyPI. + +After a PR is merged, a push to `main` runs the full test job. If the tests pass, +the publish job checks whether the current version already exists on PyPI: + +- If the version does not exist, the package is published. +- If the version already exists, publishing is skipped. + +Do not push release tags to publish. Releases are published from `main` after +the merge commit passes CI. From 5a80a935c873bcb59422a98042ab32ae286b5e28 Mon Sep 17 00:00:00 2001 From: juanpabloguerra16 Date: Thu, 30 Apr 2026 20:12:26 -0700 Subject: [PATCH 4/4] Reference contributing guide in README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d51ffa2..a54bdce 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,8 @@ from ctxd import AsyncClient async with AsyncClient(api_key="") as client: results = await client.search("text:deployment") ``` + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the package version bump and release +process.