cloudsmith-cli 1.24.0__tar.gz → 1.25.0__tar.gz

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 (149) hide show
  1. {cloudsmith_cli-1.24.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.25.0}/PKG-INFO +2 -2
  2. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/__init__.py +8 -0
  3. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/manage.py +24 -9
  4. cloudsmith_cli-1.25.0/cloudsmith_cli/cli/commands/credential_helper/pnpm.py +51 -0
  5. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/entitlements.py +17 -5
  6. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/logout.py +6 -3
  7. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/mcp.py +7 -2
  8. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/push.py +30 -1
  9. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/upstream.py +1 -0
  10. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/config.py +1 -1
  11. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/decorators.py +5 -2
  12. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/saml.py +25 -24
  13. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/validators.py +23 -0
  14. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/webserver.py +62 -8
  15. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/keyring.py +38 -1
  16. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/generic.py +16 -0
  17. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/launchers.py +19 -9
  18. cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/__init__.py +4 -0
  19. cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/installer.py +264 -0
  20. cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/rc.py +185 -0
  21. cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/runtime.py +68 -0
  22. cloudsmith_cli-1.25.0/cloudsmith_cli/data/VERSION +1 -0
  23. cloudsmith_cli-1.25.0/cloudsmith_cli/templates/auth_two_factor.html +38 -0
  24. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0/cloudsmith_cli.egg-info}/PKG-INFO +2 -2
  25. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/SOURCES.txt +7 -1
  26. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/requires.txt +1 -1
  27. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/pyproject.toml +3 -2
  28. cloudsmith_cli-1.24.0/cloudsmith_cli/data/VERSION +0 -1
  29. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/LICENSE +0 -0
  30. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/MANIFEST.in +0 -0
  31. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/README.md +0 -0
  32. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/__init__.py +0 -0
  33. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/__main__.py +0 -0
  34. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/__init__.py +0 -0
  35. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/command.py +0 -0
  36. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
  37. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/auth.py +0 -0
  38. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/check.py +0 -0
  39. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
  40. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/docker.py +0 -0
  41. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/generic.py +0 -0
  42. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
  43. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
  44. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
  45. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/domains.py +0 -0
  46. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/download.py +0 -0
  47. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
  48. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
  49. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/login.py +0 -0
  50. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/main.py +0 -0
  51. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metadata.py +0 -0
  52. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
  53. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
  54. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
  55. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
  56. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/move.py +0 -0
  57. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
  58. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
  59. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
  60. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
  61. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
  62. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
  63. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
  64. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
  65. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
  66. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
  67. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
  68. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
  69. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/status.py +0 -0
  70. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
  71. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
  72. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
  73. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
  74. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/dsc_parser.py +0 -0
  75. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/exceptions.py +0 -0
  76. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/metadata_common.py +0 -0
  77. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/table.py +0 -0
  78. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/types.py +0 -0
  79. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/utils.py +0 -0
  80. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/conftest.py +0 -0
  81. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/__init__.py +0 -0
  82. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/__init__.py +0 -0
  83. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/distros.py +0 -0
  84. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
  85. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
  86. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/files.py +0 -0
  87. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/init.py +0 -0
  88. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/metadata.py +0 -0
  89. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/metrics.py +0 -0
  90. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/orgs.py +0 -0
  91. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/packages.py +0 -0
  92. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/quota.py +0 -0
  93. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/rates.py +0 -0
  94. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/repos.py +0 -0
  95. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/status.py +0 -0
  96. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
  97. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/user.py +0 -0
  98. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/version.py +0 -0
  99. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
  100. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/cache_utils.py +0 -0
  101. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/config.py +0 -0
  102. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/__init__.py +0 -0
  103. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/chain.py +0 -0
  104. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/models.py +0 -0
  105. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/__init__.py +0 -0
  106. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/cache.py +0 -0
  107. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +0 -0
  108. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/aws.py +0 -0
  109. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +0 -0
  110. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/base.py +0 -0
  111. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +0 -0
  112. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +0 -0
  113. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/generic.py +0 -0
  114. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +0 -0
  115. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +0 -0
  116. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/exchange.py +0 -0
  117. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/provider.py +0 -0
  118. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/__init__.py +0 -0
  119. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/cli_flag.py +0 -0
  120. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/credentials_file.py +0 -0
  121. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/env_var.py +0 -0
  122. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/keyring_provider.py +0 -0
  123. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/oidc_provider.py +0 -0
  124. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/download.py +0 -0
  125. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
  126. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/data.py +0 -0
  127. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/server.py +0 -0
  128. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/pagination.py +0 -0
  129. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/ratelimits.py +0 -0
  130. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/rest.py +0 -0
  131. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/utils.py +0 -0
  132. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/version.py +0 -0
  133. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/__init__.py +0 -0
  134. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/backends.py +0 -0
  135. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/common.py +0 -0
  136. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/custom_domains.py +0 -0
  137. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/default_domains.py +0 -0
  138. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/__init__.py +0 -0
  139. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/installer.py +0 -0
  140. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/runtime.py +0 -0
  141. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/data/config.ini +0 -0
  142. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/data/credentials.ini +0 -0
  143. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/__init__.py +0 -0
  144. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/auth_error.html +0 -0
  145. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/auth_success.html +0 -0
  146. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
  147. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
  148. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
  149. {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudsmith-cli
3
- Version: 1.24.0
3
+ Version: 1.25.0
4
4
  Summary: Cloudsmith Command-Line Interface (CLI)
5
5
  Author-email: Cloudsmith Ltd <support@cloudsmith.io>
6
6
  License-Expression: Apache-2.0
@@ -28,7 +28,7 @@ Requires-Dist: click-configfile>=0.2.3
28
28
  Requires-Dist: click-didyoumean>=0.0.3
29
29
  Requires-Dist: click-spinner>=0.1.7
30
30
  Requires-Dist: json5>=0.9.0
31
- Requires-Dist: cloudsmith-api<3.0,>=2.0.29
31
+ Requires-Dist: cloudsmith-api<3.0,>=2.0.31
32
32
  Requires-Dist: keyring>=25.4.1
33
33
  Requires-Dist: keyrings-alt>=5.0.2
34
34
  Requires-Dist: keyrings-cryptfile>=1.3.9
@@ -12,6 +12,7 @@ from ..main import main
12
12
  from .docker import docker as docker_cmd
13
13
  from .generic import generic as generic_cmd
14
14
  from .manage import install_cmd, list_cmd, uninstall_cmd
15
+ from .pnpm import pnpm as pnpm_cmd
15
16
 
16
17
 
17
18
  @click.group()
@@ -27,11 +28,18 @@ def credential_helper():
27
28
  # Install Docker credential helper
28
29
  $ cloudsmith credential-helper install docker
29
30
 
31
+ # Install pnpm credential helper
32
+ $ cloudsmith credential-helper install pnpm
33
+
30
34
  # Test Docker credential helper directly
31
35
  $ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker
36
+
37
+ # Test pnpm credential helper directly
38
+ $ cloudsmith credential-helper pnpm npm.cloudsmith.io
32
39
  """
33
40
 
34
41
 
42
+ credential_helper.add_command(pnpm_cmd, name="pnpm")
35
43
  credential_helper.add_command(docker_cmd, name="docker")
36
44
  credential_helper.add_command(generic_cmd, name="generic")
37
45
  credential_helper.add_command(install_cmd, name="install")
@@ -12,6 +12,9 @@ import sys
12
12
 
13
13
  import click
14
14
 
15
+ from cloudsmith_cli.credential_helpers.generic import PartialInstallError
16
+ from cloudsmith_cli.credential_helpers.pnpm.installer import PNPMInstaller
17
+
15
18
  from ....credential_helpers.docker.installer import DockerInstaller
16
19
  from ... import utils
17
20
  from ...decorators import (
@@ -27,6 +30,7 @@ from ...decorators import (
27
30
 
28
31
  _INSTALLERS: dict[str, type] = {
29
32
  "docker": DockerInstaller,
33
+ "pnpm": PNPMInstaller,
30
34
  }
31
35
 
32
36
 
@@ -40,7 +44,7 @@ def _get_installer(name: str):
40
44
 
41
45
  Returns
42
46
  -------
43
- DockerInstaller
47
+ BaseInstaller
44
48
  An instance of the appropriate installer class.
45
49
 
46
50
  Raises
@@ -85,7 +89,7 @@ def _get_installer(name: str):
85
89
  "--no-discover",
86
90
  is_flag=True,
87
91
  default=False,
88
- help="Disable automatic discovery of custom Docker domains.",
92
+ help="Disable automatic discovery of custom domains.",
89
93
  )
90
94
  @click.option(
91
95
  "--refresh",
@@ -110,25 +114,30 @@ def install_cmd(
110
114
  ) -> None:
111
115
  """Install a credential helper launcher and configure the package manager.
112
116
 
113
- HELPER is the name of the credential helper to install (e.g. ``docker``).
117
+ HELPER is the name of the credential helper to install (e.g. ``docker``, ``pnpm``).
118
+
119
+ Important for pnpm: The tokenHelper directive is only honored in the
120
+ user-level ~/.npmrc file, not in a project-level .npmrc. This is a pnpm
121
+ security restriction. The absolute path to the launcher is automatically
122
+ calculated and configured.
114
123
 
115
124
  Examples:
116
125
 
117
126
  \b
118
- # Install Docker credential helper
119
- $ cloudsmith credential-helper install docker
127
+ # Install credential helper
128
+ $ cloudsmith credential-helper install HELPER
120
129
 
121
130
  \b
122
131
  # Install with a custom domain
123
- $ cloudsmith credential-helper install docker --domain my.registry.example.com
132
+ $ cloudsmith credential-helper install HELPER --domain my.registry.example.com
124
133
 
125
134
  \b
126
135
  # Preview without making changes
127
- $ cloudsmith credential-helper install docker --dry-run
136
+ $ cloudsmith credential-helper install HELPER --dry-run
128
137
 
129
138
  \b
130
139
  # Disable automatic custom-domain discovery
131
- $ cloudsmith credential-helper install docker --no-discover
140
+ $ cloudsmith credential-helper install HELPER --no-discover
132
141
  """
133
142
  installer = _get_installer(helper)
134
143
  try:
@@ -146,6 +155,11 @@ def install_cmd(
146
155
  raise click.ClickException(
147
156
  f"Failed to install {helper!r} credential helper: {exc}"
148
157
  )
158
+ except PartialInstallError as exc:
159
+ actions = exc.actions
160
+ ec = exc.exit_code
161
+ else:
162
+ ec = 0
149
163
 
150
164
  use_stderr = utils.should_use_stderr(opts)
151
165
  warnings = [a for a in actions if a.startswith("WARNING")]
@@ -157,7 +171,7 @@ def install_cmd(
157
171
  "warnings": warnings,
158
172
  }
159
173
  if utils.maybe_print_as_json(opts, data):
160
- return
174
+ sys.exit(ec)
161
175
 
162
176
  if dry_run:
163
177
  click.echo("Dry run — no changes will be made:", err=use_stderr)
@@ -165,6 +179,7 @@ def install_cmd(
165
179
  click.echo(f" {action}" if dry_run else action, err=use_stderr)
166
180
  for warning in warnings:
167
181
  click.secho(f" {warning}" if dry_run else warning, err=True, fg="yellow")
182
+ sys.exit(ec)
168
183
 
169
184
 
170
185
  # ---------------------------------------------------------------------------
@@ -0,0 +1,51 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """
3
+ pnpm credential helper command.
4
+
5
+ Implements the pnpm credential helper protocol for Cloudsmith registries.
6
+ """
7
+
8
+ import sys
9
+
10
+ import click
11
+
12
+ from ....credential_helpers.pnpm import execute
13
+ from ...decorators import common_api_auth_options, resolve_credentials
14
+
15
+
16
+ @click.command()
17
+ @click.argument("repo", required=False, default="npm.cloudsmith.io")
18
+ @common_api_auth_options
19
+ @resolve_credentials
20
+ def pnpm(opts, repo):
21
+ """
22
+ Input (arg, optional):
23
+ Server URL as plain text (e.g. "npm.cloudsmith.io")
24
+
25
+ Output (stdout):
26
+ Text: <cloudsmith-token>
27
+
28
+ \b
29
+ Exit codes:
30
+ 0: Success
31
+ 1: Error (no credentials available, not a Cloudsmith registry, etc.)
32
+
33
+ \b
34
+ Environment variables:
35
+ CLOUDSMITH_API_KEY: API key for authentication (optional)
36
+ CLOUDSMITH_ORG: Organisation slug (required for custom domain support)
37
+ """
38
+
39
+ exit_code, stdout, stderr = execute(
40
+ repo,
41
+ credential=opts.credential,
42
+ api_host=opts.api_host,
43
+ org=opts.org,
44
+ )
45
+
46
+ if stdout is not None:
47
+ click.echo(stdout, nl=False)
48
+ if stderr is not None:
49
+ click.echo(stderr, err=True)
50
+
51
+ sys.exit(exit_code)
@@ -115,7 +115,9 @@ def list_entitlements(ctx, opts, owner_repo, page, page_size, show_tokens, page_
115
115
 
116
116
  click.secho("OK", fg="green", err=use_stderr)
117
117
 
118
- print_entitlements(opts=opts, data=entitlements_, page_info=page_info)
118
+ print_entitlements(
119
+ opts=opts, data=entitlements_, page_info=page_info, page_all=page_all
120
+ )
119
121
 
120
122
 
121
123
  @entitlements.command(name="list", aliases=["ls"])
@@ -126,7 +128,7 @@ def list_(*args, **kwargs): # pylint: disable=missing-docstring
126
128
  return list_entitlements(*args, **kwargs)
127
129
 
128
130
 
129
- def print_entitlements(opts, data, page_info=None, show_list_info=True):
131
+ def print_entitlements(opts, data, page_info=None, show_list_info=True, page_all=False):
130
132
  """Print entitlements as a table or output in another format."""
131
133
  if utils.maybe_print_as_json(opts, data, page_info):
132
134
  return
@@ -163,11 +165,16 @@ def print_entitlements(opts, data, page_info=None, show_list_info=True):
163
165
 
164
166
  num_results = len(data)
165
167
  list_suffix = "entitlement%s" % ("s" if num_results != 1 else "")
166
- utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
168
+ utils.pretty_print_list_info(
169
+ num_results=num_results,
170
+ page_info=None if page_all else page_info,
171
+ suffix=f"{list_suffix} retrieved" if page_all else f"{list_suffix} visible",
172
+ page_all=page_all,
173
+ )
167
174
 
168
175
 
169
176
  def print_entitlements_with_restrictions(
170
- opts, data, page_info=None, show_list_info=True
177
+ opts, data, page_info=None, show_list_info=True, page_all=False
171
178
  ):
172
179
  # pylint: disable=too-many-locals
173
180
  """Print entitlements (with restrictions) as a table or output in another format."""
@@ -276,7 +283,12 @@ def print_entitlements_with_restrictions(
276
283
 
277
284
  num_results = len(data)
278
285
  list_suffix = "entitlement%s" % ("s" if num_results != 1 else "")
279
- utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
286
+ utils.pretty_print_list_info(
287
+ num_results=num_results,
288
+ page_info=None if page_all else page_info,
289
+ suffix=f"{list_suffix} retrieved" if page_all else f"{list_suffix} visible",
290
+ page_all=page_all,
291
+ )
280
292
 
281
293
 
282
294
  @entitlements.command(aliases=["new"])
@@ -7,7 +7,7 @@ import click
7
7
  import cloudsmith_api
8
8
 
9
9
  from ...core import keyring
10
- from .. import decorators, utils
10
+ from .. import decorators, utils, validators
11
11
  from ..config import CredentialsReader
12
12
  from .main import main
13
13
 
@@ -117,8 +117,11 @@ def logout(ctx, opts, api_host, keyring_only, config_only, dry_run):
117
117
  "--keyring-only and --config-only are mutually exclusive."
118
118
  )
119
119
 
120
- if api_host is None:
121
- api_host = opts.api_host or cloudsmith_api.Configuration().host
120
+ api_host = (
121
+ validators.normalize_api_host(api_host)
122
+ or opts.api_host
123
+ or cloudsmith_api.Configuration().host
124
+ )
122
125
 
123
126
  use_stderr = utils.should_use_stderr(opts)
124
127
 
@@ -1,17 +1,22 @@
1
1
  """Main command/entrypoint."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import json
4
6
  import os
5
7
  import shutil
6
8
  import sys
7
9
  import tempfile
8
10
  from pathlib import Path
11
+ from typing import TYPE_CHECKING
9
12
 
10
13
  import click
11
14
  import json5
12
15
 
13
- from ...core.mcp import server
14
- from ...core.mcp.data import OpenAPITool
16
+ if TYPE_CHECKING:
17
+ from ...core.mcp import server
18
+ from ...core.mcp.data import OpenAPITool
19
+
15
20
  from .. import command, decorators, utils
16
21
  from .main import main
17
22
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  # pylint: disable=too-many-lines
4
4
 
5
+ import json
5
6
  import math
6
7
  import os
7
8
  import shlex
@@ -694,7 +695,7 @@ def wait_for_package_sync(
694
695
  # When using stderr for logs, avoid an interactive progress bar and just poll for status.
695
696
  while True:
696
697
  res = get_package_status(owner, repo, slug)
697
- ok, failed, _, _, _, _ = res
698
+ ok, failed, _, status_str, stage_str, reason = res
698
699
  if ok or failed:
699
700
  break
700
701
 
@@ -811,6 +812,34 @@ def wait_for_package_sync(
811
812
  attempts=attempts,
812
813
  )
813
814
  else:
815
+ if use_stderr:
816
+ # In JSON output mode the human-readable text above went to
817
+ # stderr, so stdout still needs a machine-readable envelope.
818
+ # Mirror the shape produced by ``handle_api_exceptions`` so a
819
+ # sync failure is parseable the same way as an API failure.
820
+ error_data = {
821
+ "detail": reason or "Package failed to synchronise.",
822
+ "help": {
823
+ "context": context_msg,
824
+ "hint": None,
825
+ },
826
+ "meta": {
827
+ "status": status_str,
828
+ "stage": stage_str or "Unknown",
829
+ "seconds": seconds,
830
+ },
831
+ }
832
+
833
+ metadata_context = getattr(opts, "push_metadata_info", None)
834
+ if metadata_context is not None:
835
+ error_data["metadata_attachment"] = metadata_context
836
+
837
+ click.echo(
838
+ json.dumps(
839
+ error_data, indent=4 if opts.output == "pretty_json" else None
840
+ )
841
+ )
842
+
814
843
  ctx.exit(1)
815
844
 
816
845
 
@@ -31,6 +31,7 @@ UPSTREAM_FORMATS = [
31
31
  "hex",
32
32
  "huggingface",
33
33
  "maven",
34
+ "nix",
34
35
  "npm",
35
36
  "nuget",
36
37
  "python",
@@ -341,7 +341,7 @@ class Options: # pylint: disable=too-many-public-methods
341
341
  @api_host.setter
342
342
  def api_host(self, value):
343
343
  """Set value for API host."""
344
- self._set_option("api_host", value)
344
+ self._set_option("api_host", validators.normalize_api_host(value))
345
345
 
346
346
  @property
347
347
  def api_key(self):
@@ -16,7 +16,6 @@ from ..core.credentials.oidc.detectors import (
16
16
  disabled_detectors_from_env,
17
17
  registered_detectors,
18
18
  )
19
- from ..core.mcp import server
20
19
  from ..core.rest import create_requests_session as _create_session
21
20
  from . import config, utils
22
21
 
@@ -312,7 +311,9 @@ def initialise_session(f):
312
311
  """Create a shared HTTP session with proxy/SSL/user-agent settings."""
313
312
 
314
313
  @click.option(
315
- "--api-host", envvar="CLOUDSMITH_API_HOST", help="The API host to connect to."
314
+ "--api-host",
315
+ envvar="CLOUDSMITH_API_HOST",
316
+ help="The API host to connect to",
316
317
  )
317
318
  @click.option(
318
319
  "--api-proxy",
@@ -627,6 +628,8 @@ def initialise_mcp(f):
627
628
  @click.pass_context
628
629
  @functools.wraps(f)
629
630
  def wrapper(ctx, *args, **kwargs):
631
+ from ..core.mcp import server
632
+
630
633
  opts = kwargs.get("opts")
631
634
 
632
635
  all_tools = kwargs.pop("all_tools")
@@ -26,6 +26,28 @@ def create_configured_session(opts):
26
26
  return session
27
27
 
28
28
 
29
+ def raise_for_api_error(response):
30
+ """Raise :class:`ApiException` if *response* failed, keeping the API's detail.
31
+
32
+ Without the detail the exception renders as bare status text, so a caller
33
+ reporting it tells the user that something failed but never what.
34
+ """
35
+ try:
36
+ response.raise_for_status()
37
+ except requests.RequestException as exc:
38
+ try:
39
+ body = exc.response.json()
40
+ except ValueError:
41
+ body = None
42
+
43
+ raise ApiException(
44
+ response.status_code,
45
+ detail=body.get("detail") if isinstance(body, dict) else None,
46
+ headers=exc.response.headers,
47
+ body=exc.response.content,
48
+ )
49
+
50
+
29
51
  def get_idp_url(api_host, owner, session):
30
52
  org_saml_url = "{api_host}/orgs/{owner}/saml/?{params}".format(
31
53
  api_host=api_host,
@@ -35,14 +57,7 @@ def get_idp_url(api_host, owner, session):
35
57
 
36
58
  org_saml_response = session.get(org_saml_url, timeout=30)
37
59
 
38
- try:
39
- org_saml_response.raise_for_status()
40
- except requests.RequestException as exc:
41
- raise ApiException(
42
- org_saml_response.status_code,
43
- headers=exc.response.headers,
44
- body=exc.response.content,
45
- )
60
+ raise_for_api_error(org_saml_response)
46
61
 
47
62
  return org_saml_response.json().get("redirect_url")
48
63
 
@@ -60,14 +75,7 @@ def exchange_2fa_token(api_host, two_factor_token, totp_token, session):
60
75
  timeout=30,
61
76
  )
62
77
 
63
- try:
64
- exchange_response.raise_for_status()
65
- except requests.RequestException as exc:
66
- raise ApiException(
67
- exchange_response.status_code,
68
- headers=exc.response.headers,
69
- body=exc.response.content,
70
- )
78
+ raise_for_api_error(exchange_response)
71
79
 
72
80
  exchange_data = exchange_response.json()
73
81
  access_token = exchange_data.get("access_token")
@@ -89,14 +97,7 @@ def refresh_access_token(api_host, access_token, refresh_token, session):
89
97
  timeout=30,
90
98
  )
91
99
 
92
- try:
93
- response.raise_for_status()
94
- except requests.RequestException as exc:
95
- raise ApiException(
96
- response.status_code,
97
- headers=exc.response.headers,
98
- body=exc.response.content,
99
- )
100
+ raise_for_api_error(response)
100
101
 
101
102
  response_data = response.json()
102
103
  access_token = response_data.get("access_token")
@@ -1,6 +1,7 @@
1
1
  """CLI - Validators."""
2
2
 
3
3
  import base64
4
+ import re
4
5
  from datetime import datetime, timezone
5
6
  from urllib.parse import urlsplit
6
7
 
@@ -13,6 +14,8 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
13
14
  BAD_API_HEADERS = ("user-agent", "host")
14
15
  API_HEADER_TRANSFORMS = {}
15
16
  PUBLIC_API_HOST_SUFFIXES = ("cloudsmith.io", "cloudsmith.com")
17
+ DEFAULT_API_HOST_SCHEME = "https"
18
+ API_HOST_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://")
16
19
 
17
20
 
18
21
  class IntOrWildcard(click.ParamType):
@@ -86,6 +89,26 @@ def validate_api_headers(param, value):
86
89
  return headers
87
90
 
88
91
 
92
+ def normalize_api_host(value):
93
+ """Normalise a user-supplied API host into a canonical URL.
94
+
95
+ Removes surrounding whitespace and trailing slashes. Adds the https
96
+ scheme when the value does not give one. Returns None for a blank value,
97
+ so a blank value does not replace a host that is already set.
98
+ """
99
+ if not isinstance(value, str):
100
+ return value
101
+
102
+ host = value.strip()
103
+ if not host:
104
+ return None
105
+
106
+ if not API_HOST_SCHEME_RE.match(host):
107
+ host = f"{DEFAULT_API_HOST_SCHEME}://{host.lstrip('/')}"
108
+
109
+ return host.rstrip("/")
110
+
111
+
89
112
  def host_matches_suffixes(url, suffixes):
90
113
  """True if url's hostname equals or is a subdomain of one of the suffixes.
91
114
 
@@ -13,6 +13,8 @@ from ..core.credentials.models import CredentialResult
13
13
  from ..core.keyring import store_sso_tokens
14
14
  from .saml import exchange_2fa_token
15
15
 
16
+ REJECTED_TWO_FACTOR_STATUSES = frozenset({400, 401, 403, 422})
17
+
16
18
 
17
19
  def get_template_path(template_name):
18
20
  """Get the absolute path to a template file."""
@@ -118,6 +120,10 @@ class AuthenticationWebServer(HTTPServer):
118
120
 
119
121
 
120
122
  class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
123
+ # Set once a response is written, so a later failure doesn't write a
124
+ # second one onto a request the browser already considers finished.
125
+ responded = False
126
+
121
127
  def __init__(self, request, client_address, server, **kwargs):
122
128
  self.owner = kwargs.get("owner")
123
129
  self.debug = kwargs.get("debug", False)
@@ -132,17 +138,64 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
132
138
  """Get the API host from the server instance."""
133
139
  return self.server_instance.api_host if self.server_instance else None
134
140
 
141
+ def _prompt_and_exchange_2fa_token(self, two_factor_token):
142
+ """Prompt for a 2FA code, and prompt again while the API rejects it."""
143
+ click.echo(err=True)
144
+ click.secho(
145
+ "Your organization requires two-factor authentication.",
146
+ fg="yellow",
147
+ bold=True,
148
+ err=True,
149
+ )
150
+ click.echo(
151
+ "Enter the code from your authenticator app to finish signing in.",
152
+ err=True,
153
+ )
154
+
155
+ click.echo("Press Ctrl-C to give up.", err=True)
156
+
157
+ while True:
158
+ totp_token = click.prompt(
159
+ "Please enter your 2FA code", hide_input=True, type=str, err=True
160
+ )
161
+
162
+ try:
163
+ return exchange_2fa_token(
164
+ self.api_host, two_factor_token, totp_token, session=self.session
165
+ )
166
+ except ApiException as exc:
167
+ if exc.status not in REJECTED_TWO_FACTOR_STATUSES:
168
+ raise
169
+
170
+ click.secho(
171
+ f"\nThat 2FA code was rejected ({exc}). Please try again.",
172
+ fg="red",
173
+ err=True,
174
+ )
175
+
135
176
  def _return_response(self, status=200, message=None):
177
+ body = (message or "").encode("utf-8")
178
+
136
179
  self.send_response(status)
137
180
  self.send_header("Content-Type", "text/html; charset=utf-8")
181
+ # Without a length the client can only detect the end of the body when
182
+ # the connection closes, which is after do_GET returns. The 2FA page is
183
+ # sent before a blocking prompt, so it must be readable straight away.
184
+ self.send_header("Content-Length", str(len(body)))
138
185
  self.end_headers()
139
186
 
140
- self.wfile.write(message.encode("utf-8"))
187
+ self.wfile.write(body)
188
+ self.wfile.flush()
189
+ self.responded = True
141
190
 
142
191
  def _return_success_response(self):
143
192
  html_content = render_template("auth_success.html")
144
193
  self._return_response(message=html_content)
145
194
 
195
+ def _return_two_factor_response(self):
196
+ html_content = render_template("auth_two_factor.html")
197
+ self._return_response(message=html_content)
198
+
146
199
  def _return_error_response(self, error_message=None):
147
200
  error_details = ""
148
201
  if error_message:
@@ -210,12 +263,13 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
210
263
  return
211
264
 
212
265
  if two_factor_token:
213
- totp_token = click.prompt(
214
- "Please enter your 2FA token", hide_input=True, type=str, err=True
215
- )
266
+ # Answer the browser before prompting. The prompt blocks, so a
267
+ # request left open until then shows the user a blank page with
268
+ # no sign that the terminal is waiting on them.
269
+ self._return_two_factor_response()
216
270
 
217
- access_token, refresh_token = exchange_2fa_token(
218
- self.api_host, two_factor_token, totp_token, session=self.session
271
+ access_token, refresh_token = self._prompt_and_exchange_2fa_token(
272
+ two_factor_token
219
273
  )
220
274
 
221
275
  # Store the access token on the server instance (same as above)
@@ -232,10 +286,10 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
232
286
  self.server_instance.refresh_api_config_after_auth()
233
287
 
234
288
  click.secho("\nAuthentication complete", fg="green", err=True)
235
- self._return_success_response()
236
289
  return
237
290
  except Exception:
238
- self._return_error_response()
291
+ if not self.responded:
292
+ self._return_error_response()
239
293
  raise
240
294
 
241
295
  click.secho("\nNo valid authentication parameters received", fg="red", err=True)