Skip to content
Draft
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
1 change: 1 addition & 0 deletions cloudsmith_cli/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
docs,
download,
entitlements,
exec_,
help_,
list_,
login,
Expand Down
2 changes: 2 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ..main import main
from .docker import docker as docker_cmd
from .manage import install_cmd, list_cmd, uninstall_cmd
from .shell import shell_init


@click.group()
Expand All @@ -32,6 +33,7 @@ def credential_helper():


credential_helper.add_command(docker_cmd, name="docker")
credential_helper.add_command(shell_init, name="shell-init")
credential_helper.add_command(install_cmd, name="install")
credential_helper.add_command(uninstall_cmd, name="uninstall")
credential_helper.add_command(list_cmd, name="list")
Expand Down
45 changes: 39 additions & 6 deletions cloudsmith_cli/cli/commands/credential_helper/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

from __future__ import annotations

import os
import sys

import click

from ....credential_helpers.docker.installer import DockerInstaller
from ....credential_helpers.maven.installer import MavenInstaller
from ....credential_helpers.shellplugin.config import DEFAULT_REGISTRY_ID
from ... import utils
from ...decorators import (
common_api_auth_options,
Expand All @@ -28,6 +29,7 @@

_INSTALLERS: dict[str, type] = {
"docker": DockerInstaller,
"maven": MavenInstaller,
}


Expand Down Expand Up @@ -86,7 +88,7 @@ def _get_installer(name: str):
"--no-discover",
is_flag=True,
default=False,
help="Disable automatic discovery of custom Docker domains.",
help="Disable automatic discovery of custom domains.",
)
@click.option(
"--refresh",
Expand All @@ -97,7 +99,21 @@ def _get_installer(name: str):
@click.option(
"--org",
default=None,
help="Cloudsmith organisation slug for custom-domain discovery.",
envvar="CLOUDSMITH_ORG",
help="Cloudsmith organisation slug.",
)
@click.option(
"--repo",
default=None,
envvar="CLOUDSMITH_REPO",
help="Cloudsmith repository slug.",
)
@click.option(
"--registry-id",
default=DEFAULT_REGISTRY_ID,
show_default=True,
help="Id the credentials are registered under in your project config "
"(e.g. the Maven settings.xml/pom.xml <server> id).",
)
@common_cli_config_options
@common_cli_output_options
Expand All @@ -114,6 +130,8 @@ def install_cmd(
no_discover: bool,
refresh: bool,
org: str | None,
repo: str | None,
registry_id: str,
) -> None:
"""Install a credential helper launcher and configure the package manager.

Expand All @@ -138,16 +156,29 @@ def install_cmd(
$ cloudsmith credential-helper install docker --no-discover
"""
installer = _get_installer(helper)
org = org or os.environ.get("CLOUDSMITH_ORG", "").strip() or None
api_key = opts.credential.api_key if opts.credential else None
auth_type = (
getattr(opts.credential, "auth_type", "api_key")
if opts.credential
else "api_key"
)

per_repo = getattr(installer, "requires_repo", False)
if per_repo and not repo:
click.echo(
f"Error: helper {helper!r} requires --repo (and --org).",
err=True,
)
sys.exit(1)

# Shell plugins (per-repo) keep their shims in a fixed dir; --bin-dir only
# applies to launcher-based helpers like Docker.
if per_repo:
extra: dict = {"repo": repo, "registry_id": registry_id}
else:
extra = {"bin_dir": bin_dir}
try:
actions = installer.install(
bin_dir=bin_dir,
domains=domains,
dry_run=dry_run,
discover=not no_discover,
Expand All @@ -156,6 +187,7 @@ def install_cmd(
api_key=api_key,
auth_type=auth_type,
api_host=opts.api_host,
**extra,
)
except OSError as exc:
raise click.ClickException(
Expand Down Expand Up @@ -217,8 +249,9 @@ def uninstall_cmd(ctx, opts, helper: str, bin_dir: str | None, dry_run: bool) ->
$ cloudsmith credential-helper uninstall docker --dry-run
"""
installer = _get_installer(helper)
extra = {} if getattr(installer, "requires_repo", False) else {"bin_dir": bin_dir}
try:
actions = installer.uninstall(bin_dir=bin_dir, dry_run=dry_run)
actions = installer.uninstall(dry_run=dry_run, **extra)
except OSError as exc:
raise click.ClickException(
f"Failed to uninstall {helper!r} credential helper: {exc}"
Expand Down
36 changes: 36 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2026 Cloudsmith Ltd
"""``cloudsmith credential-helper shell-init`` — print shell init for shims.

Add ``eval "$(cloudsmith credential-helper shell-init)"`` to your shell rc file
to put the Cloudsmith shims directory ahead of the real package-manager
binaries on ``$PATH``.
"""

import click

from ....credential_helpers.shellplugin.shellinit import detect_shell, generate_init


@click.command(name="shell-init")
@click.option(
"--shell",
"shell_name",
type=click.Choice(["bash", "zsh", "fish"]),
default=None,
help="Target shell. Auto-detected from $SHELL when omitted.",
)
def shell_init(shell_name):
"""Print shell init that puts the Cloudsmith shims dir first on PATH.

Examples:

\b
# bash / zsh
$ eval "$(cloudsmith credential-helper shell-init)"

\b
# fish
$ cloudsmith credential-helper shell-init --shell fish | source
"""
shell = shell_name or detect_shell()
click.echo(generate_init(shell), nl=False)
41 changes: 41 additions & 0 deletions cloudsmith_cli/cli/commands/exec_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2026 Cloudsmith Ltd
"""CLI/Commands - Run a command with Cloudsmith credentials provisioned."""

import sys

import click

from ...credential_helpers.shellplugin import runner
from ..decorators import common_api_auth_options, resolve_credentials
from .main import main


@main.command(name="exec", context_settings={"ignore_unknown_options": True})
@click.option(
"--org", default=None, envvar="CLOUDSMITH_ORG", help="Cloudsmith organisation slug."
)
@click.option(
"--repo", default=None, envvar="CLOUDSMITH_REPO", help="Cloudsmith repository slug."
)
@click.argument("command", nargs=-1, type=click.UNPROCESSED, required=True)
@common_api_auth_options
@resolve_credentials
def exec_(opts, org, repo, command):
"""Run a package-manager command authenticated against Cloudsmith.

Wraps the command so it resolves dependencies from (and publishes to) your
Cloudsmith repository, with credentials injected for that single run and
cleaned up afterwards. The package manager is detected automatically from
the command, so just put it after ``--``:

\b
$ cloudsmith exec -- mvn clean deploy
"""
exit_code = runner.run(
list(command),
credential=opts.credential,
owner=org,
repo=repo,
api_host=opts.api_host,
)
sys.exit(exit_code)
Loading
Loading