cloudsmith-cli 1.20.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. cloudsmith_cli/__init__.py +10 -0
  2. cloudsmith_cli/__main__.py +8 -0
  3. cloudsmith_cli/cli/__init__.py +1 -0
  4. cloudsmith_cli/cli/command.py +160 -0
  5. cloudsmith_cli/cli/commands/__init__.py +33 -0
  6. cloudsmith_cli/cli/commands/auth.py +173 -0
  7. cloudsmith_cli/cli/commands/check.py +129 -0
  8. cloudsmith_cli/cli/commands/copy.py +98 -0
  9. cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
  10. cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
  11. cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
  12. cloudsmith_cli/cli/commands/delete.py +67 -0
  13. cloudsmith_cli/cli/commands/dependencies.py +108 -0
  14. cloudsmith_cli/cli/commands/docs.py +16 -0
  15. cloudsmith_cli/cli/commands/download.py +620 -0
  16. cloudsmith_cli/cli/commands/entitlements.py +819 -0
  17. cloudsmith_cli/cli/commands/help_.py +12 -0
  18. cloudsmith_cli/cli/commands/list_.py +317 -0
  19. cloudsmith_cli/cli/commands/login.py +99 -0
  20. cloudsmith_cli/cli/commands/logout.py +151 -0
  21. cloudsmith_cli/cli/commands/main.py +73 -0
  22. cloudsmith_cli/cli/commands/mcp.py +523 -0
  23. cloudsmith_cli/cli/commands/metadata.py +503 -0
  24. cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
  25. cloudsmith_cli/cli/commands/metrics/command.py +20 -0
  26. cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
  27. cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
  28. cloudsmith_cli/cli/commands/move.py +111 -0
  29. cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
  30. cloudsmith_cli/cli/commands/policy/command.py +20 -0
  31. cloudsmith_cli/cli/commands/policy/deny.py +248 -0
  32. cloudsmith_cli/cli/commands/policy/license.py +335 -0
  33. cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
  34. cloudsmith_cli/cli/commands/push.py +1323 -0
  35. cloudsmith_cli/cli/commands/quarantine.py +148 -0
  36. cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
  37. cloudsmith_cli/cli/commands/quota/command.py +20 -0
  38. cloudsmith_cli/cli/commands/quota/history.py +122 -0
  39. cloudsmith_cli/cli/commands/quota/quota.py +107 -0
  40. cloudsmith_cli/cli/commands/repos.py +330 -0
  41. cloudsmith_cli/cli/commands/resync.py +90 -0
  42. cloudsmith_cli/cli/commands/status.py +92 -0
  43. cloudsmith_cli/cli/commands/tags.py +375 -0
  44. cloudsmith_cli/cli/commands/tokens.py +318 -0
  45. cloudsmith_cli/cli/commands/upstream.py +479 -0
  46. cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
  47. cloudsmith_cli/cli/commands/whoami.py +188 -0
  48. cloudsmith_cli/cli/config.py +635 -0
  49. cloudsmith_cli/cli/decorators.py +624 -0
  50. cloudsmith_cli/cli/exceptions.py +215 -0
  51. cloudsmith_cli/cli/metadata_common.py +146 -0
  52. cloudsmith_cli/cli/saml.py +109 -0
  53. cloudsmith_cli/cli/table.py +59 -0
  54. cloudsmith_cli/cli/types.py +14 -0
  55. cloudsmith_cli/cli/utils.py +267 -0
  56. cloudsmith_cli/cli/validators.py +378 -0
  57. cloudsmith_cli/cli/webserver.py +263 -0
  58. cloudsmith_cli/core/__init__.py +1 -0
  59. cloudsmith_cli/core/api/__init__.py +1 -0
  60. cloudsmith_cli/core/api/distros.py +31 -0
  61. cloudsmith_cli/core/api/entitlements.py +130 -0
  62. cloudsmith_cli/core/api/exceptions.py +57 -0
  63. cloudsmith_cli/core/api/files.py +131 -0
  64. cloudsmith_cli/core/api/init.py +109 -0
  65. cloudsmith_cli/core/api/metadata.py +217 -0
  66. cloudsmith_cli/core/api/metrics.py +78 -0
  67. cloudsmith_cli/core/api/orgs.py +201 -0
  68. cloudsmith_cli/core/api/packages.py +309 -0
  69. cloudsmith_cli/core/api/quota.py +64 -0
  70. cloudsmith_cli/core/api/rates.py +28 -0
  71. cloudsmith_cli/core/api/repos.py +81 -0
  72. cloudsmith_cli/core/api/status.py +27 -0
  73. cloudsmith_cli/core/api/upstreams.py +72 -0
  74. cloudsmith_cli/core/api/user.py +109 -0
  75. cloudsmith_cli/core/api/version.py +15 -0
  76. cloudsmith_cli/core/api/vulnerabilities.py +230 -0
  77. cloudsmith_cli/core/cache_utils.py +160 -0
  78. cloudsmith_cli/core/config.py +140 -0
  79. cloudsmith_cli/core/credentials/__init__.py +0 -0
  80. cloudsmith_cli/core/credentials/chain.py +69 -0
  81. cloudsmith_cli/core/credentials/models.py +44 -0
  82. cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
  83. cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
  84. cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
  85. cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
  86. cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
  87. cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
  88. cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
  89. cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
  90. cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
  91. cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
  92. cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
  93. cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
  94. cloudsmith_cli/core/credentials/provider.py +17 -0
  95. cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
  96. cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
  97. cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
  98. cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
  99. cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
  100. cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
  101. cloudsmith_cli/core/download.py +594 -0
  102. cloudsmith_cli/core/keyring.py +171 -0
  103. cloudsmith_cli/core/mcp/__init__.py +0 -0
  104. cloudsmith_cli/core/mcp/data.py +17 -0
  105. cloudsmith_cli/core/mcp/server.py +786 -0
  106. cloudsmith_cli/core/pagination.py +131 -0
  107. cloudsmith_cli/core/ratelimits.py +88 -0
  108. cloudsmith_cli/core/rest.py +255 -0
  109. cloudsmith_cli/core/utils.py +95 -0
  110. cloudsmith_cli/core/version.py +20 -0
  111. cloudsmith_cli/credential_helpers/__init__.py +7 -0
  112. cloudsmith_cli/credential_helpers/backends.py +41 -0
  113. cloudsmith_cli/credential_helpers/common.py +111 -0
  114. cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
  115. cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
  116. cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
  117. cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
  118. cloudsmith_cli/credential_helpers/launchers.py +175 -0
  119. cloudsmith_cli/data/VERSION +1 -0
  120. cloudsmith_cli/data/config.ini +23 -0
  121. cloudsmith_cli/data/credentials.ini +14 -0
  122. cloudsmith_cli/templates/__init__.py +3 -0
  123. cloudsmith_cli/templates/auth_error.html +45 -0
  124. cloudsmith_cli/templates/auth_success.html +37 -0
  125. cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
  126. cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
  127. cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
  128. cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
  129. cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
  130. cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,12 @@
1
+ """Main command/entrypoint."""
2
+
3
+ import click
4
+
5
+ from .main import main
6
+
7
+
8
+ @main.command(name="help")
9
+ @click.pass_context
10
+ def help_(ctx):
11
+ """Show this delightful help message and exit."""
12
+ click.echo(ctx.parent.get_help())
@@ -0,0 +1,317 @@
1
+ """CLI/Commands - List objects."""
2
+
3
+ import functools
4
+ from operator import itemgetter
5
+
6
+ import click
7
+
8
+ from ...core.api.distros import list_distros
9
+ from ...core.api.packages import get_package_format_names_with_distros, list_packages
10
+ from ...core.pagination import paginate_results
11
+ from .. import command, decorators, utils, validators
12
+ from ..exceptions import handle_api_exceptions
13
+ from ..utils import maybe_spinner
14
+ from . import dependencies, entitlements
15
+ from .main import main
16
+ from .repos import get as get_repos
17
+
18
+
19
+ @main.group(cls=command.AliasGroup, name="list", aliases=["ls"])
20
+ @decorators.common_cli_config_options
21
+ @decorators.common_cli_output_options
22
+ @decorators.common_api_auth_options
23
+ @decorators.initialise_api
24
+ @click.pass_context
25
+ def list_(ctx, opts): # pylint: disable=unused-argument
26
+ """
27
+ List distros, packages, repos and entitlements.
28
+
29
+ See the help for subcommands for more information on each.
30
+ """
31
+
32
+
33
+ @list_.command(name="dependencies", aliases=["deps"])
34
+ @dependencies.list_dependencies_options
35
+ @functools.wraps(entitlements.list_entitlements)
36
+ @click.pass_context
37
+ def dependencies_(*args, **kwargs): # pylint: disable=missing-docstring
38
+ return dependencies.list_dependencies(*args, **kwargs)
39
+
40
+
41
+ @list_.command()
42
+ @decorators.common_cli_config_options
43
+ @decorators.common_cli_output_options
44
+ @decorators.common_api_auth_options
45
+ @decorators.initialise_api
46
+ @click.argument(
47
+ "package-format",
48
+ default=None,
49
+ required=False,
50
+ type=click.Choice(get_package_format_names_with_distros()),
51
+ )
52
+ @click.pass_context
53
+ def distros(ctx, opts, package_format):
54
+ """List available distributions."""
55
+ # Use stderr for messages if the output is something else (e.g. # JSON)
56
+ use_stderr = utils.should_use_stderr(opts)
57
+
58
+ if not use_stderr:
59
+ click.echo("Getting list of distributions ... ", nl=False, err=use_stderr)
60
+
61
+ context_msg = "Failed to get list of distributions!"
62
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
63
+ with maybe_spinner(opts):
64
+ distros_ = list_distros(package_format=package_format)
65
+
66
+ if not use_stderr:
67
+ click.secho("OK", fg="green", err=use_stderr)
68
+
69
+ if utils.maybe_print_as_json(opts, distros_):
70
+ return
71
+
72
+ headers = ["Distro", "Release", "Format", "Distro / Release (Identifier)"]
73
+ if package_format:
74
+ headers.remove("Format")
75
+
76
+ rows = []
77
+ for distro in sorted(distros_, key=itemgetter("slug")):
78
+ if not distro["versions"]:
79
+ continue
80
+
81
+ for release in sorted(distro["versions"], key=itemgetter("slug")):
82
+ row = [
83
+ click.style(distro["name"], fg="cyan"),
84
+ click.style(release["name"], fg="yellow"),
85
+ click.style(distro["format"], fg="blue"),
86
+ "%(distro)s/%(release)s"
87
+ % {
88
+ "distro": click.style(distro["slug"], fg="magenta"),
89
+ "release": click.style(release["slug"], fg="green"),
90
+ },
91
+ ]
92
+
93
+ if package_format:
94
+ row.pop(2) # Remove format column
95
+
96
+ rows.append(row)
97
+
98
+ if distros_:
99
+ click.echo()
100
+ utils.pretty_print_table(headers, rows)
101
+
102
+ click.echo()
103
+
104
+ num_results = sum(
105
+ 1 for distro in distros_ for release in distro["versions"] if release
106
+ )
107
+ list_suffix = "distribution release%s" % ("s" if num_results != 1 else "")
108
+ utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
109
+
110
+
111
+ @list_.command(name="entitlements", aliases=["ents"])
112
+ @entitlements.list_entitlements_options
113
+ @functools.wraps(entitlements.list_entitlements)
114
+ @click.pass_context
115
+ def entitlements_(*args, **kwargs): # pylint: disable=missing-docstring
116
+ return entitlements.list_entitlements(*args, **kwargs)
117
+
118
+
119
+ @list_.command(aliases=["pkgs"])
120
+ @decorators.common_cli_config_options
121
+ @decorators.common_cli_output_options
122
+ @decorators.common_cli_list_options
123
+ @decorators.common_api_auth_options
124
+ @decorators.initialise_api
125
+ @click.argument(
126
+ "owner_repo", metavar="OWNER/REPO", callback=validators.validate_owner_repo
127
+ )
128
+ @click.option(
129
+ "-q",
130
+ "--query",
131
+ help=("A boolean-like search term for querying package attributes."),
132
+ )
133
+ @click.option(
134
+ "--sort",
135
+ type=click.Choice(
136
+ [
137
+ "date",
138
+ "-date",
139
+ "downloads",
140
+ "-downloads",
141
+ "format",
142
+ "-format",
143
+ "name",
144
+ "-name",
145
+ "scan",
146
+ "-scan",
147
+ "scan_date",
148
+ "-scan_date",
149
+ "size",
150
+ "-size",
151
+ "status",
152
+ "-status",
153
+ "version",
154
+ "-version",
155
+ ]
156
+ ),
157
+ help=("Sort packages by field. Prefix with '-' for descending order."),
158
+ )
159
+ @click.pass_context
160
+ def packages(ctx, opts, owner_repo, page, page_size, query, sort, page_all):
161
+ """
162
+ List packages for a repository.
163
+
164
+ OWNER/REPO: Specify the OWNER namespace (i.e. user or org), and the
165
+ REPO name to list packages for that namespace and repository. All separated
166
+ by a slash.
167
+
168
+ You can use the search query (-q|--query) to filter packages:
169
+
170
+ - By name: 'my-package' (implicit) or 'name:my-package'
171
+
172
+ - By filename: 'pkg.ext' (implicit) or 'filename:pkg.ext' (explicit)
173
+
174
+ - By version: '1.0.0' (implicit) or 'version:1.0.0' (explicit)
175
+
176
+ - By arch: 'x86_64' (implicit) or 'architecture:x86_64' (explicit)
177
+
178
+ - By distro: 'el' (implicit) or 'distribution:el' (explicit)
179
+
180
+ You can also modify the search terms:
181
+
182
+ - '^foo' to anchor to start of term
183
+
184
+ - 'foo$' to anchor to end of term
185
+
186
+ - 'foo*bar' for fuzzy matching
187
+
188
+ - '~foo' for negation of the term (explicit only, e.g. name:~foo)
189
+
190
+ Multiple search terms are conjunctive (AND).
191
+
192
+ Examples, to find packages named exactly foo, with a zip filename, that are
193
+ NOT the x86 architecture, use something like this:
194
+
195
+ --query 'name:^foo$ filename:.zip$ architecture:~x86'
196
+
197
+ You can sort the results using --sort with these fields:
198
+ - date/-date: Sort by creation date
199
+ - downloads/-downloads: Sort by download count
200
+ - format/-format: Sort by package format
201
+ - name/-name: Sort by package name
202
+ - scan/-scan: Sort by scan status
203
+ - scan_date/-scan_date: Sort by last scan date
204
+ - size/-size: Sort by package size
205
+ - status/-status: Sort by package status
206
+ - version/-version: Sort by version
207
+
208
+ Prefix any field with '-' for descending order (e.g. -date for newest first).
209
+ """
210
+ owner, repo = owner_repo
211
+
212
+ # Use stderr for messages if the output is something else (e.g. # JSON)
213
+ use_stderr = utils.should_use_stderr(opts)
214
+
215
+ if not use_stderr:
216
+ click.echo("Getting list of packages ... ", nl=False, err=use_stderr)
217
+
218
+ context_msg = "Failed to get list of packages!"
219
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
220
+ with maybe_spinner(opts):
221
+ packages_, page_info = paginate_results(
222
+ list_packages,
223
+ page_all=page_all,
224
+ page=page,
225
+ page_size=page_size,
226
+ owner=owner,
227
+ repo=repo,
228
+ query=query,
229
+ sort=sort,
230
+ )
231
+
232
+ if not use_stderr:
233
+ click.secho("OK", fg="green", err=use_stderr)
234
+
235
+ if utils.maybe_print_as_json(opts, packages_, page_info):
236
+ return
237
+
238
+ headers = ["Name", "Version", "Status", "Owner / Repository (Identifier)"]
239
+ rows = []
240
+ for package in sorted(packages_, key=itemgetter("namespace", "slug")):
241
+ rows.append(
242
+ [
243
+ click.style(_get_package_name(package), fg="cyan"),
244
+ click.style(_get_package_version(package), fg="yellow"),
245
+ click.style(_get_package_status(package), fg="blue"),
246
+ "%(owner_slug)s/%(repo_slug)s/%(slug)s"
247
+ % {
248
+ "owner_slug": click.style(package["namespace"], fg="magenta"),
249
+ "repo_slug": click.style(package["repository"], fg="magenta"),
250
+ "slug": click.style(package["slug"], fg="green"),
251
+ },
252
+ ]
253
+ )
254
+
255
+ if packages_:
256
+ click.echo()
257
+ utils.pretty_print_table(headers, rows)
258
+
259
+ click.echo()
260
+
261
+ num_results = len(packages_)
262
+ list_suffix = "package%s" % ("s" if num_results != 1 else "")
263
+ utils.pretty_print_list_info(
264
+ num_results=num_results,
265
+ page_info=None if page_all else page_info,
266
+ suffix=f"{list_suffix} retrieved" if page_all else f"{list_suffix} visible",
267
+ page_all=page_all,
268
+ )
269
+
270
+
271
+ @list_.command()
272
+ @decorators.common_cli_config_options
273
+ @decorators.common_cli_output_options
274
+ @decorators.common_cli_list_options
275
+ @decorators.common_api_auth_options
276
+ @decorators.initialise_api
277
+ @click.argument(
278
+ "owner_repo",
279
+ metavar="OWNER/REPO",
280
+ callback=validators.validate_optional_owner_repo,
281
+ default="",
282
+ required=False,
283
+ )
284
+ @click.pass_context
285
+ def repos(ctx, opts, owner_repo, page, page_size, page_all):
286
+ """
287
+ List repositories for a namespace (owner).
288
+
289
+ OWNER/REPO: Specify the OWNER namespace (i.e user or org) to list the
290
+ repositories for that namespace.
291
+
292
+ If REPO isn't specified, all repositories will be retrieved from the
293
+ OWNER namespace.
294
+
295
+ If OWNER isn't specified it'll default to the currently authenticated user
296
+ (if any). If you're unauthenticated, no results will be returned.
297
+ """
298
+ ctx.forward(get_repos)
299
+
300
+
301
+ def _get_package_name(package):
302
+ """Get the name (or filename) for a package."""
303
+ return package["name"] or package["filename"]
304
+
305
+
306
+ def _get_package_status(package):
307
+ """Get the status for a package."""
308
+ status = package["status_str"] or "Unknown"
309
+ stage = package["stage_str"] or "Unknown"
310
+ if stage == "Fully Synchronised":
311
+ return status
312
+ return f"{status} / {stage}"
313
+
314
+
315
+ def _get_package_version(package):
316
+ """Get the version for a package (if any)."""
317
+ return package["version"] or "None"
@@ -0,0 +1,99 @@
1
+ """CLI/Commands - Get an API token."""
2
+
3
+ import click
4
+ import cloudsmith_api
5
+
6
+ from ...core.api.exceptions import TwoFactorRequiredException
7
+ from ...core.api.user import get_user_token
8
+ from ...core.config import create_config_files, new_config_messaging
9
+ from .. import decorators, utils
10
+ from ..exceptions import handle_api_exceptions
11
+ from ..utils import maybe_spinner
12
+ from .main import main
13
+
14
+
15
+ def validate_login(ctx, param, value):
16
+ """Ensure that login is not blank."""
17
+ # pylint: disable=unused-argument
18
+ value = value.strip()
19
+ if not value:
20
+ raise click.BadParameter("The value cannot be blank.", param=param)
21
+ return value
22
+
23
+
24
+ @main.command(aliases=["token"])
25
+ @click.option(
26
+ "-l",
27
+ "--login",
28
+ required=True,
29
+ callback=validate_login,
30
+ prompt=True,
31
+ help="Your Cloudsmith login account (email address).",
32
+ )
33
+ @click.password_option("-p", "--password", help="Your Cloudsmith login password.")
34
+ @decorators.common_cli_config_options
35
+ @decorators.common_cli_output_options
36
+ @decorators.initialise_api
37
+ @click.pass_context
38
+ def login(ctx, opts, login, password): # pylint: disable=redefined-outer-name
39
+ """Retrieve your API authentication token/key via login."""
40
+ use_stderr = utils.should_use_stderr(opts)
41
+ click.echo(
42
+ "Retrieving API token for %(login)s ... "
43
+ % {"login": click.style(login, bold=True)},
44
+ nl=False,
45
+ err=use_stderr,
46
+ )
47
+
48
+ context_msg = "Failed to retrieve the API token!"
49
+ try:
50
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
51
+ with maybe_spinner(opts):
52
+ api_key = get_user_token(login=login, password=password)
53
+ except TwoFactorRequiredException as e:
54
+ click.echo("\r\033[K", nl=False, err=use_stderr)
55
+ click.echo("Two-factor authentication is required.", err=use_stderr)
56
+
57
+ totp_token = click.prompt(
58
+ "Enter your two-factor authentication code", type=str, err=use_stderr
59
+ )
60
+ click.echo(
61
+ "Verifying two-factor code for %(login)s ... "
62
+ % {"login": click.style(login, bold=True)},
63
+ nl=False,
64
+ err=use_stderr,
65
+ )
66
+
67
+ try:
68
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
69
+ with maybe_spinner(opts):
70
+ api_key = get_user_token(
71
+ login=login,
72
+ password=password,
73
+ totp_token=totp_token,
74
+ two_factor_token=e.two_factor_token,
75
+ )
76
+ except cloudsmith_api.rest.ApiException:
77
+ click.echo("\r\033[K", nl=False, err=use_stderr)
78
+ click.secho(
79
+ "Authentication failed: The entered TOTP token is not valid.",
80
+ fg="red",
81
+ err=use_stderr,
82
+ )
83
+ ctx.exit(1)
84
+
85
+ except cloudsmith_api.rest.ApiException as e:
86
+ click.echo("\r\033[K", nl=False, err=use_stderr)
87
+ click.secho(f"Authentication failed: {str(e)}", fg="red", err=use_stderr)
88
+ ctx.exit(1)
89
+
90
+ click.secho("OK", fg="green", err=use_stderr)
91
+
92
+ if not utils.maybe_print_as_json(opts, {"token": api_key, "login": login}):
93
+ click.echo(
94
+ "Your API key/token is: %(token)s"
95
+ % {"token": click.style(api_key, fg="magenta")}
96
+ )
97
+
98
+ create, has_errors = create_config_files(ctx, opts, api_key=api_key)
99
+ new_config_messaging(has_errors, opts, create, api_key=api_key)
@@ -0,0 +1,151 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """CLI/Commands - Log out and clear authentication state."""
3
+
4
+ import os
5
+
6
+ import click
7
+ import cloudsmith_api
8
+
9
+ from ...core import keyring
10
+ from .. import decorators, utils
11
+ from ..config import CredentialsReader
12
+ from .main import main
13
+
14
+
15
+ def _clear_credentials(dry_run, use_stderr):
16
+ """Clear credential files. Returns result dict."""
17
+ creds_files = CredentialsReader.find_existing_files()
18
+ if not creds_files:
19
+ click.echo("No credentials file found.", err=use_stderr)
20
+ return {"action": "not_found", "files": []}
21
+
22
+ if not dry_run:
23
+ for path in creds_files:
24
+ CredentialsReader.clear_api_key(path)
25
+
26
+ verb = "Would remove" if dry_run else "Removed"
27
+ for path in creds_files:
28
+ click.echo(
29
+ f"{verb} credentials from: " + click.style(path, bold=True),
30
+ err=use_stderr,
31
+ )
32
+ action = "would_remove" if dry_run else "removed"
33
+ return {"action": action, "files": list(creds_files)}
34
+
35
+
36
+ def _clear_keyring(api_host, dry_run, use_stderr):
37
+ """Clear SSO tokens from keyring. Returns result dict."""
38
+ if not keyring.should_use_keyring():
39
+ click.secho(
40
+ "Keyring is disabled (CLOUDSMITH_NO_KEYRING is set).",
41
+ fg="yellow",
42
+ err=use_stderr,
43
+ )
44
+ return {"action": "disabled"}
45
+
46
+ if not keyring.has_sso_tokens(api_host):
47
+ click.echo("No SSO tokens found in system keyring.", err=use_stderr)
48
+ return {"action": "not_found"}
49
+
50
+ if dry_run:
51
+ click.echo("Would remove SSO tokens from system keyring.", err=use_stderr)
52
+ return {"action": "would_remove"}
53
+
54
+ deleted = keyring.delete_sso_tokens(api_host)
55
+ action = "removed" if deleted else "failed"
56
+ msg = f"{'Removed' if deleted else 'Failed to remove'} SSO tokens from system keyring."
57
+ click.secho(msg, fg=None if deleted else "red", err=use_stderr)
58
+ return {"action": action} if deleted else {"action": action, "message": msg}
59
+
60
+
61
+ def _env_api_key_status():
62
+ """Return structured status for the CLOUDSMITH_API_KEY env var."""
63
+ is_set = bool(os.environ.get("CLOUDSMITH_API_KEY"))
64
+ return {
65
+ "is_set": is_set,
66
+ "action": "unset CLOUDSMITH_API_KEY" if is_set else "none",
67
+ }
68
+
69
+
70
+ def _collect_warnings(keyring_only, config_only):
71
+ """Collect advisory warnings based on flags and environment."""
72
+ warnings = []
73
+ if config_only:
74
+ warnings.append("SSO tokens were not modified (--config-only).")
75
+ if keyring_only:
76
+ warnings.append("credentials.ini was not modified (--keyring-only).")
77
+ if os.environ.get("CLOUDSMITH_API_KEY"):
78
+ warnings.append(
79
+ "CLOUDSMITH_API_KEY is set in your environment. "
80
+ "Run: unset CLOUDSMITH_API_KEY"
81
+ )
82
+ return warnings
83
+
84
+
85
+ @main.command()
86
+ @click.option(
87
+ "--api-host",
88
+ envvar="CLOUDSMITH_API_HOST",
89
+ default=None,
90
+ help="The API host to clear keyring tokens for.",
91
+ )
92
+ @click.option(
93
+ "--keyring-only",
94
+ is_flag=True,
95
+ default=False,
96
+ help="Only clear SSO tokens from the system keyring.",
97
+ )
98
+ @click.option(
99
+ "--config-only",
100
+ is_flag=True,
101
+ default=False,
102
+ help="Only clear credentials from credentials.ini.",
103
+ )
104
+ @click.option(
105
+ "--dry-run",
106
+ is_flag=True,
107
+ default=False,
108
+ help="Show what would be removed without removing anything.",
109
+ )
110
+ @decorators.common_cli_config_options
111
+ @decorators.common_cli_output_options
112
+ @click.pass_context
113
+ def logout(ctx, opts, api_host, keyring_only, config_only, dry_run):
114
+ """Clear stored authentication credentials and SSO tokens."""
115
+ if keyring_only and config_only:
116
+ raise click.UsageError(
117
+ "--keyring-only and --config-only are mutually exclusive."
118
+ )
119
+
120
+ if api_host is None:
121
+ api_host = opts.api_host or cloudsmith_api.Configuration().host
122
+
123
+ use_stderr = utils.should_use_stderr(opts)
124
+
125
+ credential_file = (
126
+ _clear_credentials(dry_run, use_stderr)
127
+ if not keyring_only
128
+ else {"action": "skipped", "files": []}
129
+ )
130
+ keyring_result = (
131
+ _clear_keyring(api_host, dry_run, use_stderr)
132
+ if not config_only
133
+ else {"action": "skipped"}
134
+ )
135
+ warnings = _collect_warnings(keyring_only, config_only)
136
+
137
+ for warning in warnings:
138
+ click.secho(f"Note: {warning}", fg="yellow", err=use_stderr)
139
+
140
+ utils.maybe_print_as_json(
141
+ opts,
142
+ {
143
+ "api_host": api_host,
144
+ "dry_run": dry_run,
145
+ "sources": {
146
+ "credential_file": credential_file,
147
+ "keyring": keyring_result,
148
+ "environment_api_key": _env_api_key_status(),
149
+ },
150
+ },
151
+ )
@@ -0,0 +1,73 @@
1
+ """Main command/entrypoint."""
2
+
3
+ import click
4
+
5
+ from ...core.api.version import get_version as get_api_version
6
+ from ...core.utils import get_github_website, get_help_website
7
+ from ...core.version import get_version as get_cli_version
8
+ from .. import command, decorators, utils
9
+
10
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
11
+
12
+
13
+ def print_version(opts):
14
+ """Print the environment versions."""
15
+ cli_version = get_cli_version()
16
+ api_version = get_api_version()
17
+
18
+ data = {
19
+ "cli_version": cli_version,
20
+ "api_version": api_version,
21
+ }
22
+
23
+ if not utils.maybe_print_as_json(opts, data):
24
+ click.echo("Versions:")
25
+ click.secho(
26
+ "CLI Package Version: %(version)s"
27
+ % {"version": click.style(cli_version, bold=True)}
28
+ )
29
+ click.secho(
30
+ "API Package Version: %(version)s"
31
+ % {"version": click.style(api_version, bold=True)}
32
+ )
33
+
34
+
35
+ @click.group(
36
+ cls=command.AliasGroup,
37
+ context_settings=CONTEXT_SETTINGS,
38
+ invoke_without_command=True,
39
+ help="""\b
40
+ ________ __ _ __ __ ________ ____
41
+ / ____/ /___ __ ______/ /________ ___ (_) /_/ /_ / ____/ / / _/
42
+ / / / / __ \\/ / / / __ / ___/ __ `__ \\/ / __/ __ \\ / / / / / /
43
+ / /___/ / /_/ / /_/ / /_/ (__ ) / / / / / / /_/ / / / / /___/ /____/ /
44
+ \\____/_/\\____/\\__,_/\\__,_/____/_/ /_/ /_/_/\\__/_/ /_/ \\____/_____/___/
45
+
46
+
47
+ The Cloudsmith Command-Line Interface - Be Awesome. Automate Everything.
48
+ """,
49
+ epilog="""
50
+ For more help, see the docs: %(help_website)s
51
+
52
+ For issues/contributing: %(github_website)s
53
+ """
54
+ % {"help_website": get_help_website(), "github_website": get_github_website()},
55
+ )
56
+ @click.option(
57
+ "-V",
58
+ "--version",
59
+ help="Show the version numbers for the API and CLI.",
60
+ is_flag=True,
61
+ is_eager=True,
62
+ )
63
+ @decorators.common_cli_config_options
64
+ @decorators.common_cli_output_options
65
+ @click.pass_context
66
+ def main(ctx, opts, version):
67
+ """Handle entrypoint to CLI."""
68
+ # pylint: disable=unused-argument
69
+
70
+ if version:
71
+ print_version(opts)
72
+ elif ctx.invoked_subcommand is None:
73
+ click.echo(ctx.get_help())