This file provides guidance to coding agents when working with code in this repository.
The repo uses direnv (.envrc) to bootstrap a .venv via uv, install the project editable, install dev deps from requirements.in, and pre-commit install. If you don't use direnv, the equivalent is:
python -m venv .venv && source .venv/bin/activate
pip install -e .
pip install -r requirements.in # dev deps (matches what .envrc installs)
pre-commit install
requirements.txt is the locked/pinned form of requirements.in and is what CI uses; requirements.in is the source list. Install whichever matches your needs (.envrc uses requirements.in).
Python >=3.10 is required (CI tests 3.10–3.14).
- Run the CLI locally:
cloudsmith ...(console_script) orpython -m cloudsmith_cli .... - Run tests:
pytest(configured viasetup.cfg— adds--cov=cloudsmith_cli). - Run a single test:
pytest cloudsmith_cli/cli/tests/test_push.py::TestClass::test_nameor by node id /-k <expr>. - Lint/format (all run via pre-commit):
pre-commit run --all-files. Individual tools:black .,isort .,flake8 --config=.flake8,pylint --rcfile=.pylintrc <path>,pyupgrade --py310-plus <files>. - Release:
bumpversion <major|minor|revision>thengit push origin <tag>. TheVERSIONsymlink in repo root points atcloudsmith_cli/data/VERSION.
The CLI is a Click app that wraps the auto-generated cloudsmith-api Python SDK plus some non-SDK HTTP/SAML/keyring logic.
-
cloudsmith_cli/cli/— Click commands, decorators, output formatting, validators, config-file parsing, SAML browser-callback webserver. API call sites belong incore/api/*; commands invoke those wrappers rather than calling the SDK directly. (A few commands —login,logout,check— do importcloudsmith_apiforConfigurationdefaults or exception classes; that's fine, but new request code should go throughcore/api/*.)cli/commands/main.pydefines the top-levelmainClick group (cls=AliasGroup). Every other command module importsmainand registers itself with@main.command(...)or@main.group(...).cli/commands/__init__.pyimports every module so registration happens on import.cli/command.pyprovidesAliasGroup(DYM + alias support) and JSON-aware Click exception formatting — Click errors are serialized to JSON when-F json|pretty_jsonis in effect (checked from both Click context andsys.argv).cli/decorators.pyis the seam between CLI and API:@common_cli_config_options,@common_cli_output_options,@common_api_auth_options, and@initialise_api(which callscore.api.init.initialise_apito configure thecloudsmith_apiSDK with key/host/proxy/SSL/retry/SAML token).@initialise_mcpwires up the MCP server.cli/config.pyreadsconfig.iniandcredentials.iniviaclick-configfile. Search path: cwd,click.get_app_dir("cloudsmith"),~/.cloudsmith. Profiles use[profile:NAME]sections.cli/metadata_common.pyholds the shared metadata-payload helpers used by bothmetadatasubcommands and the--metadata-*flags on everypush <format>subcommand. Don't duplicate metadata-resolution logic in callers.
-
cloudsmith_cli/core/— non-CLI logic: API initialization, REST helpers, pagination, rate-limit handling, keyring access-token storage, file download streaming, and the MCP server.core/api/*.py— one module per resource (packages, repos, entitlements, metadata, ...). These call the generatedcloudsmith_apiSDK and translate its exceptions intocore.api.exceptions.ApiExceptionfor the CLI layer to render.core/mcp/— dynamically builds an MCP (Model Context Protocol) server from the Cloudsmith OpenAPI specs at runtime.server.pydiscovers tools fromswagger/?format=openapi(v1) andopenapi/?format=json(v2).mcp_allowed_tools/mcp_allowed_tool_groupsconfig keys gate exposure.cli/commands/mcp.pyexposesmcp start,mcp list_tools,mcp list_groups, andmcp configure(which writes server configs into Claude Desktop / Cursor / VS Code / Gemini-CLI config files).
-
cloudsmith_cli/data/— packaged data files includingVERSION, defaultconfig.ini, defaultcredentials.ini.
cli/commands/push.py is the most complex command. Every push <format> subcommand accepts --metadata-* flags resolved via metadata_common. Push validates metadata both locally and against the API before any file upload so malformed SBOM/BuildInfo payloads cannot leave orphan packages behind. Failure behavior is configurable with precedence: --on-metadata-failure flag > $CLOUDSMITH_METADATA_FAILURE_MODE env > metadata_failure_mode config key > error default. The kwarg names listed in METADATA_KWARG_NAMES and METADATA_FAILURE_MODE_KWARG must be popped off the kwargs before they reach the API client, which will reject unknown keys.
Most commands support -F/--output-format {pretty,json,pretty_json}. The pattern is: do work (often inside utils.maybe_spinner(opts)), then if utils.maybe_print_as_json(opts, data): return before falling through to the human-readable rendering. Use utils.should_use_stderr(opts) to silence "OK"/progress text when JSON mode is on.
Three auth paths feed core.api.init.initialise_api:
-k/--api-keyflag,$CLOUDSMITH_API_KEY, orcredentials.ini.- SAML SSO via
cloudsmith auth— opens IdP URL, runs a localhost callback webserver (cli/webserver.py,cli/saml.py), stores access/refresh tokens in the OS keyring (core/keyring.py). logoutclears keyring entries and the credentials file but only warns about$CLOUDSMITH_API_KEY(env vars can't be unset from a child process).
Tests live alongside code: cloudsmith_cli/cli/tests/ and cloudsmith_cli/core/tests/. The CLI tests use Click's CliRunner; API tests stub HTTP with httpretty and freeze time with freezegun. bin/ and .venv/ are excluded from pytest discovery (norecursedirs in setup.cfg).
flake8ignoresE203,E501,D107,D102,W503and usesmax-line-length=100(butblackenforces 88).isortprofile lists known third-party packages explicitly — when adding a new third-party import, add it to.isort.cfg'sknown_third_partyif isort puts it in the wrong group.pyupgrade --py310-plusruns on every commit, so use modern syntax (X | Yunions,dict[str, ...]generics, etc.).- Adding a new subcommand: create a module under
cli/commands/, register it incli/commands/__init__.py, and attach it tomain(or a subgroup) with theAliasGroupfor alias support. - Adding a new API call: put SDK wrapping in
core/api/<resource>.py, translate exceptions toApiException, and call it from the command layer. Avoid making freshcloudsmith_apirequest calls fromcli/commands/*; importingcloudsmith_apiforConfigurationdefaults or exception types is OK and matches whatlogin/logout/checkalready do.