Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,27 @@ $ posit connect api v1/users -X GET -f page_size=5 # or force GET
$ posit connect api v1/content -f name=my-app # POST body (creates content)
```

## `posit connect version`

Print the Connect server version using your saved credentials:

```console
$ posit connect version
$ posit connect version --name production
$ posit connect version --server https://connect.example.com --api-key "$CONNECT_API_KEY"
```

The command supports `-n`/`--name`, `-s`/`--server`, `-k`/`--api-key`,
`--no-tls-verify`, and `-c`/`--cacert`, with the same connection environment
variables as `posit connect api`. It prints an empty line if the server hides
its version. Rsconnect's `CONNECT_SERVER_VERSION` environment variable overrides
the reported version.

## `posit connect *`

`posit` wraps [`rsconnect-python`](https://github.com/posit-dev/rsconnect-python):
it re-exposes the full `rsconnect` command set under `posit connect`, in addition to the `api` utility.
The `version` command is overridden to report the Connect server version.

## Using `posit` with Claude Code

Expand Down
3 changes: 3 additions & 0 deletions src/posit_cli/connect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rsconnect.main import cli as rsconnect_cli

from .api import api as api_cmd
from .version import version as version_cmd


_epilog = (
Expand All @@ -29,3 +30,5 @@ def connect() -> None:
connect.add_command(_cmd, name=_name)

connect.add_command(api_cmd, name="api")
# The mounted command reports the rsconnect package version.
connect.add_command(version_cmd, name="version")
68 changes: 68 additions & 0 deletions src/posit_cli/connect/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Print the selected Connect server's version."""

from typing import Optional

import click
from rsconnect.api import RSConnectException, RSConnectExecutor


@click.command(
context_settings={"help_option_names": ["-h", "--help"]},
short_help="Show the Connect server version.",
)
@click.option("--name", "-n", default=None, help="Nickname of a saved server.")
@click.option(
"--server",
"-s",
default=None,
envvar="CONNECT_SERVER",
help="Connect server URL [env: CONNECT_SERVER].",
)
@click.option(
"--api-key",
"-k",
default=None,
envvar="CONNECT_API_KEY",
help="Connect API key [env: CONNECT_API_KEY].",
)
@click.option(
"--no-tls-verify",
"insecure",
is_flag=True,
default=False,
envvar="CONNECT_INSECURE",
help="Skip TLS certificate verification (still uses TLS) [env: CONNECT_INSECURE].",
)
@click.option(
"--cacert",
"-c",
type=click.Path(exists=True, dir_okay=False),
default=None,
envvar="CONNECT_CA_CERTIFICATE",
help="Path to trusted TLS CA certificate.",
)
def version(
name: Optional[str],
server: Optional[str],
api_key: Optional[str],
insecure: bool,
cacert: Optional[str],
) -> None:
"""Print the Connect server version using saved credentials by default.

CONNECT_SERVER_VERSION overrides the reported version. If the server hides
its version, print an empty line.
"""
try:
executor = RSConnectExecutor(
ctx=None,
name=name,
url=server,
api_key=api_key,
insecure=insecure,
cacert=cacert,
)
server_version = executor.client.server_version()
except RSConnectException as exc:
raise click.ClickException(str(exc)) from exc
click.echo(server_version)
1 change: 1 addition & 0 deletions tests/test_rsconnect_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def test_executor_surface():
assert expected in params, f"RSConnectExecutor lost __init__ param {expected!r}"

assert hasattr(RSConnectClient, "request")
assert callable(getattr(RSConnectClient, "server_version", None))
req_params = inspect.signature(RSConnectClient.request).parameters
for expected in ("method", "path", "query_params", "body", "headers"):
assert expected in req_params, f"RSConnectClient.request lost {expected!r}"
Expand Down
97 changes: 97 additions & 0 deletions tests/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Tests for Connect server version output and connection selection."""

from unittest.mock import patch

import pytest
from click.testing import CliRunner
from rsconnect.api import RSConnectClient, RSConnectException, RSConnectServer

from posit_cli.__main__ import cli


@pytest.mark.parametrize(
"settings, output", [({"version": "2026.10.0"}, "2026.10.0\n"), ({}, "\n")]
)
def test_server_version(settings, output, monkeypatch):
monkeypatch.delenv("CONNECT_SERVER_VERSION", raising=False)
client = RSConnectClient(RSConnectServer("https://connect.example.com", api_key="key"))
with patch("posit_cli.connect.version.RSConnectExecutor") as executor:
with patch.object(client, "get", return_value=settings) as get:
executor.return_value.client = client
result = CliRunner().invoke(cli, ["connect", "version"])
assert result.exit_code == 0, result.output
assert result.output == output
get.assert_called_once_with("server_settings")
executor.assert_called_once_with(
ctx=None, name=None, url=None, api_key=None, insecure=False, cacert=None
)


def test_server_version_override(monkeypatch):
monkeypatch.setenv("CONNECT_SERVER_VERSION", "2026.09.0")
client = RSConnectClient(RSConnectServer("https://connect.example.com", api_key="key"))
with patch("posit_cli.connect.version.RSConnectExecutor") as executor:
with patch.object(client, "get") as get:
executor.return_value.client = client
result = CliRunner().invoke(cli, ["connect", "version"])
assert result.exit_code == 0, result.output
assert result.output == "2026.09.0\n"
get.assert_not_called()


@pytest.mark.parametrize("use_env", [False, True])
def test_connection_options(tmp_path, use_env):
cacert = tmp_path / "ca.pem"
cacert.write_text("certificate")
args = ["connect", "version", "--name", "production"]
env = {}
if use_env:
env = {
"CONNECT_SERVER": "https://connect.example.com",
"CONNECT_API_KEY": "key",
"CONNECT_INSECURE": "true",
"CONNECT_CA_CERTIFICATE": str(cacert),
}
else:
args += [
"-s",
"https://connect.example.com",
"-k",
"key",
"--no-tls-verify",
"-c",
str(cacert),
]
with patch("posit_cli.connect.version.RSConnectExecutor") as executor:
executor.return_value.client.server_version.return_value = "2026.10.0"
result = CliRunner().invoke(cli, args, env=env)
assert result.exit_code == 0, result.output
executor.assert_called_once_with(
ctx=None,
name="production",
url="https://connect.example.com",
api_key="key",
insecure=True,
cacert=str(cacert),
)


@pytest.mark.parametrize("stage", ["selection", "request"])
def test_errors(stage):
with patch("posit_cli.connect.version.RSConnectExecutor") as executor:
error = RSConnectException("unable to retrieve server version")
if stage == "selection":
executor.side_effect = error
else:
executor.return_value.client.server_version.side_effect = error
result = CliRunner().invoke(cli, ["connect", "version"])
assert result.exit_code == 1
assert result.output == "Error: unable to retrieve server version\n"
assert isinstance(result.exception, SystemExit)


def test_help():
result = CliRunner().invoke(cli, ["connect", "version", "-h"])
assert result.exit_code == 0
assert "Connect server version" in result.output
assert "--server" in result.output
Loading