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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.10.1
current_version = 1.10.2
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<revision>\d+)
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## [1.10.2] - 2026-01-08

### Deprecation Notice

- The `--json` flag used in `cloudsmith auth` command will be removed in upcoming releases. Please migrate to `--output-format json` instead.

### Fixed

- Fixed JSON output for all commands
- Informational messages, warnings, and interactive prompts are now routed to stderr when `--output-format json` is active.
- Error messages are now formatted as structured JSON on stdout when JSON output is requested.
- [Issue #250](https://github.com/cloudsmith-io/cloudsmith-cli/issues/250) - Updated `requests_toolbelt` dependency to `>=1.0.0` to ensure compatibility with `urllib3>=2.5` and avoid `urllib3.contrib.appengine` import errors.

## [1.10.1] - 2025-12-16
Expand Down
66 changes: 66 additions & 0 deletions cloudsmith_cli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@
from click_didyoumean import DYMGroup


def _is_json_output_requested(exception):
"""Determine if JSON output was requested, checking context and argv."""
# Check context if available
ctx = getattr(exception, "ctx", None)
if ctx and ctx.params:
fmt = ctx.params.get("output")
if fmt in ("json", "pretty_json"):
return True

# Fallback: check sys.argv for output format flags
import sys

argv = sys.argv

if "--output-format=json" in argv or "--output-format=pretty_json" in argv:
return True

for idx, arg in enumerate(argv):
if arg in ("-F", "--output-format") and idx + 1 < len(argv):
if argv[idx + 1] in ("json", "pretty_json"):
return True

return False


def _format_click_exception_as_json(exception):
"""Format a ClickException as a JSON error dict."""
return {
"detail": exception.format_message(),
"meta": {
"code": exception.exit_code,
"description": "Usage Error",
},
"help": {
"context": "Invalid usage",
"hint": "Check your command arguments/flags.",
},
}


class AliasGroup(DYMGroup):
"""A command group with DYM and alias support."""

Expand Down Expand Up @@ -92,3 +132,29 @@ def decorator(f):
def format_commands(self, ctx, formatter):
ctx.showing_help = True
return super().format_commands(ctx, formatter)

def main(self, *args, **kwargs):
"""Override main to intercept exceptions and format as JSON if requested."""
import sys

original_standalone_mode = kwargs.get("standalone_mode", True)
kwargs["standalone_mode"] = False

try:
return super().main(*args, **kwargs)
except click.exceptions.Abort:
if not original_standalone_mode:
raise
click.echo("Aborted!", err=True)
sys.exit(1)
except click.exceptions.ClickException as e:
if _is_json_output_requested(e):
import json

click.echo(json.dumps(_format_click_exception_as_json(e), indent=4))
sys.exit(e.exit_code)

if not original_standalone_mode:
raise
e.show()
sys.exit(e.exit_code)
37 changes: 25 additions & 12 deletions cloudsmith_cli/cli/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import click

from .. import decorators, validators
from .. import decorators, utils, validators
from ..exceptions import handle_api_exceptions
from ..saml import create_configured_session, get_idp_url
from ..webserver import AuthenticationWebRequestHandler, AuthenticationWebServer
Expand All @@ -22,14 +22,15 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
api_host = opts.api_config.host

idp_url = get_idp_url(api_host, owner, session=session)
if not json:
click.echo(
f"Opening your organization's SAML IDP URL in your browser: {click.style(idp_url, bold=True)}"
)
click.echo()

click.echo(
f"Opening your organization's SAML IDP URL in your browser: {click.style(idp_url, bold=True)}",
err=json,
)
click.echo(err=json)
webbrowser.open(idp_url)
if not json:
click.echo("Starting webserver to begin authentication ... ")

click.echo("Starting webserver to begin authentication ... ", err=json)

auth_server = AuthenticationWebServer(
(AUTH_SERVER_HOST, AUTH_SERVER_PORT),
Expand Down Expand Up @@ -86,13 +87,25 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
@click.pass_context
def authenticate(ctx, opts, owner, token, force, save_config, json):
"""Authenticate to Cloudsmith using the org's SAML setup."""
owner = owner[0].strip("'[]'")
json = json or utils.should_use_stderr(opts)
# If using json output, we redirect info messages to stderr
use_stderr = json

if not json:
click.echo(
f"Beginning authentication for the {click.style(owner, bold=True)} org ... "
if json and not utils.should_use_stderr(opts):
click.secho(
"DEPRECATION WARNING: The `--json` flag is deprecated and will be removed in a future release. "
"Please use `--output-format json` instead.",
fg="yellow",
err=True,
)

owner = owner[0].strip("'[]'")

click.echo(
f"Beginning authentication for the {click.style(owner, bold=True)} org ... ",
err=use_stderr,
)

context_message = "Failed to authenticate via SSO!"
with handle_api_exceptions(ctx, opts=opts, context_msg=context_message):
_perform_saml_authentication(
Expand Down
22 changes: 18 additions & 4 deletions cloudsmith_cli/cli/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ def check(ctx, opts): # pylint: disable=unused-argument
@click.pass_context
def rates(ctx, opts):
"""Check current API rate limits."""
click.echo("Retrieving rate limits ... ", nl=False)
use_stderr = utils.should_use_stderr(opts)
click.echo("Retrieving rate limits ... ", nl=False, err=use_stderr)

context_msg = "Failed to retrieve status!"
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
with maybe_spinner(opts):
resources_limits = get_rate_limits()

click.secho("OK", fg="green")
click.secho("OK", fg="green", err=use_stderr)

if utils.maybe_print_as_json(opts, resources_limits):
return

headers = ["Resource", "Throttled", "Remaining", "Interval (Seconds)", "Reset"]

Expand Down Expand Up @@ -77,17 +81,27 @@ def rates(ctx, opts):
@click.pass_context
def service(ctx, opts):
"""Check the status of the Cloudsmith service."""
click.echo("Retrieving service status ... ", nl=False)
use_stderr = utils.should_use_stderr(opts)
click.echo("Retrieving service status ... ", nl=False, err=use_stderr)

context_msg = "Failed to retrieve status!"
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
with maybe_spinner(opts):
status, version = get_status(with_version=True)

click.secho("OK", fg="green")
click.secho("OK", fg="green", err=use_stderr)

config = cloudsmith_api.Configuration()

data = {
"endpoint": config.host,
"status": status,
"version": version,
}

if utils.maybe_print_as_json(opts, data):
return

click.echo()
click.echo(f"The service endpoint is: {click.style(config.host, bold=True)}")
click.echo(f"The service status is: {click.style(status, bold=True)}")
Expand Down
10 changes: 8 additions & 2 deletions cloudsmith_cli/cli/commands/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import click

from ...core.api.packages import copy_package
from .. import decorators, validators
from .. import decorators, utils, validators
from ..exceptions import handle_api_exceptions
from ..utils import maybe_spinner
from .main import main
Expand Down Expand Up @@ -56,6 +56,8 @@ def copy(
"""
owner, source, slug = owner_repo_package

use_stderr = utils.should_use_stderr(opts)

click.echo(
"Copying %(slug)s package from %(source)s to %(dest)s ... "
% {
Expand All @@ -64,6 +66,7 @@ def copy(
"dest": click.style(destination, bold=True),
},
nl=False,
err=use_stderr,
)

context_msg = "Failed to copy package!"
Expand All @@ -75,9 +78,10 @@ def copy(
owner=owner, repo=source, identifier=slug, destination=destination
)

click.secho("OK", fg="green")
click.secho("OK", fg="green", err=use_stderr)

if no_wait_for_sync:
utils.maybe_print_status_json(opts, {"slug": new_slug, "status": "OK"})
return

wait_for_package_sync(
Expand All @@ -90,3 +94,5 @@ def copy(
skip_errors=skip_errors,
attempts=sync_attempts,
)

utils.maybe_print_status_json(opts, {"slug": new_slug, "status": "OK"})
12 changes: 9 additions & 3 deletions cloudsmith_cli/cli/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,23 @@ def delete(ctx, opts, owner_repo_package, yes):
"package": click.style(slug, bold=True),
}

use_stderr = utils.should_use_stderr(opts)

prompt = "delete the %(package)s from %(owner)s/%(repo)s" % delete_args
if not utils.confirm_operation(prompt, assume_yes=yes):
if not utils.confirm_operation(prompt, assume_yes=yes, err=use_stderr):
return

click.echo(
"Deleting %(package)s from %(owner)s/%(repo)s ... " % delete_args, nl=False
"Deleting %(package)s from %(owner)s/%(repo)s ... " % delete_args,
nl=False,
err=use_stderr,
)

context_msg = "Failed to delete the package!"
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
with maybe_spinner(opts):
delete_package(owner=owner, repo=repo, identifier=slug)

click.secho("OK", fg="green")
click.secho("OK", fg="green", err=use_stderr)

utils.maybe_print_status_json(opts, {"slug": slug, "status": "OK"})
2 changes: 1 addition & 1 deletion cloudsmith_cli/cli/commands/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def list_dependencies(ctx, opts, owner_repo_package):
owner, repo, identifier = owner_repo_package

# Use stderr for messages if the output is something else (e.g. # JSON)
use_stderr = opts.output != "pretty"
use_stderr = utils.should_use_stderr(opts)

click.echo(
"Getting direct (non-transitive) dependencies of %(package)s in "
Expand Down
Loading