cloudsmith-cli 1.14.0__tar.gz → 1.15.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 (134) hide show
  1. {cloudsmith_cli-1.14.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.15.0}/PKG-INFO +11 -1
  2. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/README.md +10 -0
  3. cloudsmith_cli-1.15.0/cloudsmith_cli/cli/commands/download.py +620 -0
  4. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_download.py +169 -2
  5. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/download.py +149 -37
  6. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_download.py +195 -3
  7. cloudsmith_cli-1.15.0/cloudsmith_cli/data/VERSION +1 -0
  8. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0/cloudsmith_cli.egg-info}/PKG-INFO +11 -1
  9. cloudsmith_cli-1.14.0/cloudsmith_cli/cli/commands/download.py +0 -459
  10. cloudsmith_cli-1.14.0/cloudsmith_cli/data/VERSION +0 -1
  11. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/LICENSE +0 -0
  12. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/MANIFEST.in +0 -0
  13. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/__init__.py +0 -0
  14. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/__main__.py +0 -0
  15. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/__init__.py +0 -0
  16. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/command.py +0 -0
  17. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
  18. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/auth.py +0 -0
  19. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/check.py +0 -0
  20. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
  21. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
  22. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
  23. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
  24. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
  25. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
  26. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
  27. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/login.py +0 -0
  28. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/logout.py +0 -0
  29. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/main.py +0 -0
  30. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
  31. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
  32. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
  33. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
  34. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
  35. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/move.py +0 -0
  36. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
  37. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
  38. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
  39. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
  40. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
  41. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/push.py +0 -0
  42. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
  43. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
  44. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
  45. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
  46. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
  47. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
  48. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
  49. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/status.py +0 -0
  50. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
  51. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
  52. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
  53. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
  54. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
  55. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/config.py +0 -0
  56. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/decorators.py +0 -0
  57. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/exceptions.py +0 -0
  58. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/saml.py +0 -0
  59. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/table.py +0 -0
  60. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/__init__.py +0 -0
  61. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/__init__.py +0 -0
  62. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/conftest.py +0 -0
  63. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/__init__.py +0 -0
  64. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_deny.py +0 -0
  65. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_licence.py +0 -0
  66. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py +0 -0
  67. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_auth.py +0 -0
  68. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_check.py +0 -0
  69. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_entitlements.py +0 -0
  70. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_login.py +0 -0
  71. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_logout.py +0 -0
  72. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_main.py +0 -0
  73. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_mcp.py +0 -0
  74. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_package_commands.py +0 -0
  75. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_repos.py +0 -0
  76. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_tokens.py +0 -0
  77. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_upstream.py +0 -0
  78. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_vulnerabilities.py +0 -0
  79. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/conftest.py +0 -0
  80. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_push.py +0 -0
  81. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_saml.py +0 -0
  82. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_utils.py +0 -0
  83. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_webserver.py +0 -0
  84. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/utils.py +0 -0
  85. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/types.py +0 -0
  86. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/utils.py +0 -0
  87. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/validators.py +0 -0
  88. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/webserver.py +0 -0
  89. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/__init__.py +0 -0
  90. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/__init__.py +0 -0
  91. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/distros.py +0 -0
  92. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
  93. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
  94. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/files.py +0 -0
  95. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/init.py +0 -0
  96. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/metrics.py +0 -0
  97. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/orgs.py +0 -0
  98. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/packages.py +0 -0
  99. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/quota.py +0 -0
  100. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/rates.py +0 -0
  101. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/repos.py +0 -0
  102. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/status.py +0 -0
  103. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
  104. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/user.py +0 -0
  105. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/version.py +0 -0
  106. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
  107. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/config.py +0 -0
  108. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/keyring.py +0 -0
  109. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
  110. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/data.py +0 -0
  111. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/server.py +0 -0
  112. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/pagination.py +0 -0
  113. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/ratelimits.py +0 -0
  114. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/rest.py +0 -0
  115. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/__init__.py +0 -0
  116. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_init.py +0 -0
  117. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_keyring.py +0 -0
  118. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_rest.py +0 -0
  119. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_version.py +0 -0
  120. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/utils.py +0 -0
  121. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/version.py +0 -0
  122. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/data/config.ini +0 -0
  123. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/data/credentials.ini +0 -0
  124. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/__init__.py +0 -0
  125. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/auth_error.html +0 -0
  126. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/auth_success.html +0 -0
  127. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/SOURCES.txt +0 -0
  128. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
  129. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
  130. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/not-zip-safe +0 -0
  131. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/requires.txt +0 -0
  132. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
  133. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/setup.cfg +0 -0
  134. {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudsmith-cli
3
- Version: 1.14.0
3
+ Version: 1.15.0
4
4
  Summary: Cloudsmith Command-Line Interface (CLI)
5
5
  Home-page: https://github.com/cloudsmith-io/cloudsmith-cli
6
6
  Author: Cloudsmith Ltd
@@ -333,6 +333,16 @@ cloudsmith download your-account/your-repo package-name --tag latest
333
333
  # Combine tag with metadata filters
334
334
  cloudsmith download your-account/your-repo package-name --tag stable --format deb --arch arm64
335
335
 
336
+ # Filter by filename (exact or glob pattern)
337
+ cloudsmith download your-account/your-repo package-name --filename '*.nupkg'
338
+ cloudsmith download your-account/your-repo package-name --filename 'mypackage-1.0.0.snupkg'
339
+
340
+ # Download all matching packages (when multiple packages share the same name/version)
341
+ cloudsmith download your-account/your-repo package-name --download-all
342
+
343
+ # Combine --download-all with --filename to download a subset
344
+ cloudsmith download your-account/your-repo package-name --download-all --filename '*.snupkg'
345
+
336
346
  # Download all associated files (POM, sources, javadoc, etc.)
337
347
  cloudsmith download your-account/your-repo package-name --all-files
338
348
 
@@ -277,6 +277,16 @@ cloudsmith download your-account/your-repo package-name --tag latest
277
277
  # Combine tag with metadata filters
278
278
  cloudsmith download your-account/your-repo package-name --tag stable --format deb --arch arm64
279
279
 
280
+ # Filter by filename (exact or glob pattern)
281
+ cloudsmith download your-account/your-repo package-name --filename '*.nupkg'
282
+ cloudsmith download your-account/your-repo package-name --filename 'mypackage-1.0.0.snupkg'
283
+
284
+ # Download all matching packages (when multiple packages share the same name/version)
285
+ cloudsmith download your-account/your-repo package-name --download-all
286
+
287
+ # Combine --download-all with --filename to download a subset
288
+ cloudsmith download your-account/your-repo package-name --download-all --filename '*.snupkg'
289
+
280
290
  # Download all associated files (POM, sources, javadoc, etc.)
281
291
  cloudsmith download your-account/your-repo package-name --all-files
282
292
 
@@ -0,0 +1,620 @@
1
+ """CLI/Commands - Download packages."""
2
+
3
+ # Copyright 2025 Cloudsmith Ltd
4
+
5
+ import os
6
+
7
+ import click
8
+
9
+ from ...core.download import (
10
+ get_download_url,
11
+ get_package_detail,
12
+ get_package_files,
13
+ resolve_all_packages,
14
+ resolve_auth,
15
+ resolve_package,
16
+ stream_download,
17
+ )
18
+ from .. import decorators, utils, validators
19
+ from ..exceptions import handle_api_exceptions
20
+ from ..utils import maybe_spinner
21
+ from .main import main
22
+
23
+
24
+ @main.command()
25
+ @decorators.common_cli_config_options
26
+ @decorators.common_cli_output_options
27
+ @decorators.common_api_auth_options
28
+ @decorators.initialise_api
29
+ @click.argument(
30
+ "owner_repo", metavar="OWNER/REPO", callback=validators.validate_owner_repo
31
+ )
32
+ @click.argument("name", required=True)
33
+ @click.option(
34
+ "--version",
35
+ help="Package version to download (e.g., '1.0.0'). If not specified, searches all versions.",
36
+ )
37
+ @click.option(
38
+ "--format",
39
+ "format_filter",
40
+ help="Package format filter (e.g., 'deb', 'rpm', 'python', 'npm').",
41
+ )
42
+ @click.option(
43
+ "--os", "os_filter", help="Operating system filter (e.g., 'ubuntu', 'centos')."
44
+ )
45
+ @click.option(
46
+ "--arch", "arch_filter", help="Architecture filter (e.g., 'amd64', 'arm64')."
47
+ )
48
+ @click.option(
49
+ "--tag",
50
+ "tag_filter",
51
+ help="Filter by package tag (e.g., 'latest', 'stable'). Use --format, --arch, --os for metadata filters.",
52
+ )
53
+ @click.option(
54
+ "--filename",
55
+ "filename_filter",
56
+ help="Filter by package filename (e.g., 'mypackage.nupkg'). Supports glob patterns (e.g., '*.snupkg').",
57
+ )
58
+ @click.option(
59
+ "--download-all",
60
+ is_flag=True,
61
+ help="Download all matching packages instead of erroring on multiple matches.",
62
+ )
63
+ @click.option(
64
+ "--outfile",
65
+ type=click.Path(),
66
+ help="Output file path. If not specified, uses the package filename.",
67
+ )
68
+ @click.option(
69
+ "--overwrite/--no-overwrite",
70
+ default=False,
71
+ help="Overwrite existing files (default: fail if file exists).",
72
+ )
73
+ @click.option(
74
+ "--all-files",
75
+ is_flag=True,
76
+ help="Download all associated files (POM, sources, javadoc, etc.) into a folder.",
77
+ )
78
+ @click.option(
79
+ "--dry-run",
80
+ is_flag=True,
81
+ help="Show what would be downloaded without actually downloading.",
82
+ )
83
+ @click.option(
84
+ "-y",
85
+ "--yes",
86
+ is_flag=True,
87
+ help="Automatically select the best match when multiple packages are found.",
88
+ )
89
+ @click.pass_context
90
+ def download(
91
+ ctx,
92
+ opts,
93
+ owner_repo,
94
+ name,
95
+ version,
96
+ format_filter,
97
+ os_filter,
98
+ arch_filter,
99
+ tag_filter,
100
+ filename_filter,
101
+ download_all,
102
+ outfile,
103
+ overwrite,
104
+ all_files,
105
+ dry_run,
106
+ yes,
107
+ ):
108
+ """
109
+ Download a package from a Cloudsmith repository.
110
+
111
+ This command downloads a package binary from a Cloudsmith repository. You can
112
+ filter packages by version, format, operating system, architecture, tags, and
113
+ filename.
114
+
115
+ Examples:
116
+
117
+ \b
118
+ # Download the latest version of 'mypackage'
119
+ cloudsmith download myorg/myrepo mypackage
120
+
121
+ \b
122
+ # Download a specific version
123
+ cloudsmith download myorg/myrepo mypackage --version 1.2.3
124
+
125
+ \b
126
+ # Download with filters and custom output name
127
+ cloudsmith download myorg/myrepo mypackage --format deb --arch amd64 --outfile my-package.deb
128
+
129
+ \b
130
+ # Download a package with a specific tag
131
+ cloudsmith download myorg/myrepo mypackage --tag latest
132
+
133
+ \b
134
+ # Download by filename (exact or glob pattern)
135
+ cloudsmith download myorg/myrepo TestSymbolPkg --filename '*.nupkg'
136
+ cloudsmith download myorg/myrepo TestSymbolPkg --filename 'TestSymbolPkg.1.0.24406.nupkg'
137
+
138
+ \b
139
+ # Download all matching packages (e.g., .nupkg and .snupkg with same name/version)
140
+ cloudsmith download myorg/myrepo TestSymbolPkg --version 1.0.24406 --download-all
141
+
142
+ \b
143
+ # Download all associated files (POM, sources, javadoc, etc.) for a Maven/NuGet package
144
+ cloudsmith download myorg/myrepo mypackage --all-files
145
+
146
+ \b
147
+ # Download all files to a custom directory
148
+ cloudsmith download myorg/myrepo mypackage --all-files --outfile ./my-package-dir
149
+
150
+ For private repositories, set: export CLOUDSMITH_API_KEY=your_api_key
151
+
152
+ If multiple packages match your criteria, you'll see a selection table unless
153
+ you use --yes to automatically select the best match (highest version, then newest),
154
+ or --download-all to download all matches.
155
+
156
+ When using --all-files, all associated files (such as POM files, sources, javadoc,
157
+ SBOM, etc.) will be downloaded into a folder named {package-name}-{version} unless
158
+ you specify a custom directory with --outfile.
159
+ """
160
+ owner, repo = owner_repo
161
+ use_stderr = utils.should_use_stderr(opts)
162
+
163
+ if not use_stderr:
164
+ click.echo(
165
+ f"Looking for package '{click.style(name, bold=True)}' in "
166
+ f"{click.style(owner, bold=True)}/{click.style(repo, bold=True)} ...",
167
+ )
168
+
169
+ # Step 1: Authenticate
170
+ session, auth_headers, auth_source = resolve_auth(opts)
171
+ if opts.debug:
172
+ click.echo(f"Using authentication: {auth_source}", err=True)
173
+
174
+ # Step 2: Find package(s)
175
+ filter_kwargs = dict(
176
+ owner=owner,
177
+ repo=repo,
178
+ name=name,
179
+ version=version,
180
+ format_filter=format_filter,
181
+ os_filter=os_filter,
182
+ arch_filter=arch_filter,
183
+ tag_filter=tag_filter,
184
+ filename_filter=filename_filter,
185
+ )
186
+ packages = _find_packages(ctx, opts, filter_kwargs, download_all, yes, use_stderr)
187
+
188
+ # Step 3: Resolve download items (url + output path for each file)
189
+ download_items = _resolve_download_items(
190
+ ctx, opts, packages, owner, repo, all_files, outfile, use_stderr
191
+ )
192
+
193
+ # Step 4: Dry-run or download
194
+ if dry_run:
195
+ _display_dry_run(packages, download_items, all_files)
196
+ return
197
+
198
+ _perform_downloads(
199
+ ctx,
200
+ opts,
201
+ packages,
202
+ download_items,
203
+ session,
204
+ auth_headers,
205
+ overwrite,
206
+ all_files,
207
+ use_stderr,
208
+ )
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Step 2: Find packages
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ def _find_packages(
217
+ ctx: click.Context,
218
+ opts,
219
+ filter_kwargs: dict,
220
+ download_all: bool,
221
+ yes: bool,
222
+ use_stderr: bool,
223
+ ) -> list:
224
+ """Find matching packages using the API."""
225
+ if download_all:
226
+ context_msg = "Failed to find packages!"
227
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
228
+ with maybe_spinner(opts):
229
+ packages = resolve_all_packages(**filter_kwargs)
230
+ else:
231
+ context_msg = "Failed to find package!"
232
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
233
+ with maybe_spinner(opts):
234
+ packages = [resolve_package(**filter_kwargs, yes=yes)]
235
+
236
+ if not use_stderr:
237
+ click.secho("OK", fg="green")
238
+
239
+ return packages
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Step 3: Resolve download items
244
+ # ---------------------------------------------------------------------------
245
+
246
+
247
+ def _resolve_download_items(
248
+ ctx: click.Context,
249
+ opts,
250
+ packages: list,
251
+ owner: str,
252
+ repo: str,
253
+ all_files: bool,
254
+ outfile: str,
255
+ use_stderr: bool,
256
+ ) -> list:
257
+ """
258
+ Resolve each package into a list of download items.
259
+
260
+ Returns a list of dicts, each with keys:
261
+ filename, url, output_path, tag, is_primary, size, package_name,
262
+ package_version, status
263
+ """
264
+ items = []
265
+
266
+ for pkg in packages:
267
+ if all_files:
268
+ items.extend(
269
+ _resolve_all_files_items(
270
+ ctx, opts, pkg, owner, repo, outfile, use_stderr
271
+ )
272
+ )
273
+ else:
274
+ items.append(
275
+ _resolve_single_file_item(
276
+ ctx,
277
+ opts,
278
+ pkg,
279
+ owner,
280
+ repo,
281
+ outfile,
282
+ len(packages) > 1,
283
+ use_stderr,
284
+ )
285
+ )
286
+
287
+ return items
288
+
289
+
290
+ def _resolve_all_files_items(
291
+ ctx: click.Context,
292
+ opts,
293
+ pkg: dict,
294
+ owner: str,
295
+ repo: str,
296
+ outfile: str,
297
+ use_stderr: bool,
298
+ ) -> list:
299
+ """Resolve all sub-files for a single package (--all-files mode)."""
300
+ pkg_name = pkg.get("name", "unknown")
301
+ pkg_version = pkg.get("version", "unknown")
302
+
303
+ if not use_stderr:
304
+ click.echo("Getting package details ...", nl=False)
305
+
306
+ context_msg = f"Failed to get details for {pkg_name}!"
307
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
308
+ with maybe_spinner(opts):
309
+ detail = get_package_detail(owner=owner, repo=repo, identifier=pkg["slug"])
310
+
311
+ if not use_stderr:
312
+ click.secho("OK", fg="green")
313
+
314
+ sub_files = get_package_files(detail)
315
+ if not sub_files:
316
+ raise click.ClickException("No downloadable files found for this package.")
317
+
318
+ # Determine output directory
319
+ if outfile:
320
+ output_dir = os.path.abspath(outfile)
321
+ else:
322
+ output_dir = os.path.abspath(f"{pkg_name}-{pkg_version}")
323
+
324
+ if not os.path.exists(output_dir):
325
+ os.makedirs(output_dir)
326
+ elif not os.path.isdir(output_dir):
327
+ raise click.ClickException(
328
+ f"Output path '{output_dir}' exists but is not a directory."
329
+ )
330
+
331
+ items = []
332
+ for f in sub_files:
333
+ items.append(
334
+ {
335
+ "filename": f["filename"],
336
+ "url": f["cdn_url"],
337
+ "output_path": _safe_join(output_dir, f["filename"]),
338
+ "tag": f.get("tag", "file"),
339
+ "is_primary": f.get("is_primary", False),
340
+ "size": f.get("size", 0),
341
+ "package_name": pkg_name,
342
+ "package_version": pkg_version,
343
+ "status": None,
344
+ }
345
+ )
346
+ return items
347
+
348
+
349
+ def _resolve_single_file_item(
350
+ ctx: click.Context,
351
+ opts,
352
+ pkg: dict,
353
+ owner: str,
354
+ repo: str,
355
+ outfile: str,
356
+ multi_package: bool,
357
+ use_stderr: bool,
358
+ ) -> dict:
359
+ """Resolve a single primary file for a package."""
360
+ pkg_name = pkg.get("name", "unknown")
361
+ pkg_version = pkg.get("version", "unknown")
362
+
363
+ download_url = get_download_url(pkg)
364
+
365
+ if not download_url:
366
+ # Fall back to detailed package info
367
+ if not use_stderr:
368
+ click.echo("Getting package details ...", nl=False)
369
+ context_msg = "Failed to get package details!"
370
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
371
+ with maybe_spinner(opts):
372
+ detail = get_package_detail(
373
+ owner=owner, repo=repo, identifier=pkg["slug"]
374
+ )
375
+ download_url = get_download_url(detail or pkg)
376
+ if not use_stderr:
377
+ click.secho("OK", fg="green")
378
+
379
+ # Determine output path
380
+ if outfile and not multi_package:
381
+ output_path = os.path.abspath(outfile)
382
+ elif multi_package:
383
+ output_dir = os.path.abspath(outfile) if outfile else os.path.abspath(".")
384
+ if not os.path.exists(output_dir):
385
+ os.makedirs(output_dir)
386
+ filename = pkg.get("filename") or f"{pkg_name}-{pkg_version}"
387
+ output_path = _safe_join(output_dir, filename)
388
+ elif pkg.get("filename"):
389
+ output_path = os.path.abspath(os.path.basename(pkg["filename"]))
390
+ else:
391
+ pkg_format = pkg.get("format", "bin")
392
+ extension = _get_extension_for_format(pkg_format)
393
+ output_path = os.path.abspath(f"{pkg_name}-{pkg_version}.{extension}")
394
+
395
+ return {
396
+ "filename": os.path.basename(output_path),
397
+ "url": download_url,
398
+ "output_path": output_path,
399
+ "tag": "file",
400
+ "is_primary": True,
401
+ "size": pkg.get("size", 0),
402
+ "package_name": pkg_name,
403
+ "package_version": pkg_version,
404
+ "status": None,
405
+ }
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Step 4a: Dry-run display (shared by all paths)
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ def _display_dry_run(packages: list, download_items: list, all_files: bool) -> None:
414
+ """Display what would be downloaded without actually downloading."""
415
+ click.echo()
416
+ click.echo(
417
+ f"Dry run - would download {len(download_items)} file(s) "
418
+ f"from {len(packages)} package(s):"
419
+ )
420
+ click.echo()
421
+
422
+ for item in download_items:
423
+ primary_marker = " (primary)" if item.get("is_primary") else ""
424
+ size = _format_file_size(item.get("size", 0))
425
+ tag = item.get("tag", "file")
426
+ click.echo(f" [{tag}] {item['filename']}{primary_marker} - {size}")
427
+ click.echo(f" Package: {item['package_name']} v{item['package_version']}")
428
+ click.echo(f" To: {item['output_path']}")
429
+
430
+
431
+ # ---------------------------------------------------------------------------
432
+ # Step 4b: Perform downloads
433
+ # ---------------------------------------------------------------------------
434
+
435
+
436
+ def _perform_downloads(
437
+ ctx: click.Context,
438
+ opts,
439
+ packages: list,
440
+ download_items: list,
441
+ session,
442
+ auth_headers: dict,
443
+ overwrite: bool,
444
+ all_files: bool,
445
+ use_stderr: bool,
446
+ ) -> None:
447
+ """Download all resolved items and report results."""
448
+ total = len(download_items)
449
+ if not use_stderr:
450
+ click.echo(f"\nDownloading {total} file(s):")
451
+ click.echo()
452
+
453
+ results = []
454
+
455
+ for idx, item in enumerate(download_items, 1):
456
+ filename = item["filename"]
457
+ url = item["url"]
458
+ output_path = item["output_path"]
459
+ tag = item.get("tag", "file")
460
+ primary_marker = " (primary)" if item.get("is_primary") else ""
461
+
462
+ # Handle missing download URL as a skip, not a failure
463
+ if not url:
464
+ _echo_progress(
465
+ use_stderr, f"[{idx}/{total}] [{tag}] {filename}{primary_marker} ..."
466
+ )
467
+ _echo_status(use_stderr, " SKIPPED", fg="yellow")
468
+ results.append({**item, "status": "SKIPPED", "error": "No download URL"})
469
+ continue
470
+
471
+ _echo_progress(
472
+ use_stderr, f"[{idx}/{total}] [{tag}] {filename}{primary_marker} ..."
473
+ )
474
+
475
+ try:
476
+ context_msg = f"Failed to download {filename}!"
477
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
478
+ stream_download(
479
+ url=url,
480
+ outfile=output_path,
481
+ session=session,
482
+ headers=auth_headers,
483
+ overwrite=overwrite,
484
+ quiet=True,
485
+ )
486
+ _echo_status(use_stderr, " OK", fg="green")
487
+ results.append({**item, "status": "OK"})
488
+ except Exception as e: # pylint: disable=broad-except
489
+ _echo_status(use_stderr, " FAILED", fg="red")
490
+ results.append({**item, "status": "FAILED", "error": str(e)})
491
+
492
+ # Report results
493
+ _report_results(opts, packages, results, all_files)
494
+
495
+
496
+ def _echo_progress(use_stderr: bool, message: str) -> None:
497
+ """Print progress message to stdout or stderr."""
498
+ click.echo(message, nl=False, err=use_stderr)
499
+
500
+
501
+ def _echo_status(use_stderr: bool, message: str, fg: str = None) -> None:
502
+ """Print styled status message to stdout or stderr."""
503
+ if fg and not use_stderr:
504
+ click.secho(message, fg=fg)
505
+ elif use_stderr:
506
+ click.echo(message, err=True)
507
+ else:
508
+ click.echo(message)
509
+
510
+
511
+ def _report_results(opts, packages: list, results: list, all_files: bool) -> None:
512
+ """Build JSON output and print summary."""
513
+ success = [r for r in results if r["status"] == "OK"]
514
+ failed = [r for r in results if r["status"] == "FAILED"]
515
+ skipped = [r for r in results if r["status"] == "SKIPPED"]
516
+
517
+ json_output = {
518
+ "packages": [
519
+ {
520
+ "name": p.get("name"),
521
+ "version": p.get("version"),
522
+ "format": p.get("format"),
523
+ "filename": p.get("filename"),
524
+ "slug": p.get("slug"),
525
+ }
526
+ for p in packages
527
+ ],
528
+ "files": [
529
+ {
530
+ "filename": r["filename"],
531
+ "path": r["output_path"],
532
+ "package": r["package_name"],
533
+ "version": r["package_version"],
534
+ "tag": r.get("tag", "file"),
535
+ "is_primary": r.get("is_primary", False),
536
+ "size": r.get("size", 0),
537
+ "status": r["status"],
538
+ **({"error": r["error"]} if "error" in r else {}),
539
+ }
540
+ for r in results
541
+ ],
542
+ "summary": {
543
+ "total_packages": len(packages),
544
+ "total_files": len(results),
545
+ "success": len(success),
546
+ "failed": len(failed),
547
+ "skipped": len(skipped),
548
+ },
549
+ }
550
+
551
+ if utils.maybe_print_as_json(opts, json_output):
552
+ return
553
+
554
+ click.echo()
555
+ if not failed and not skipped:
556
+ click.secho(f"All {len(success)} file(s) downloaded successfully!", fg="green")
557
+ else:
558
+ click.secho(f"Downloaded {len(success)}/{len(results)} file(s).", fg="yellow")
559
+ if failed:
560
+ click.echo("\nFailed files:")
561
+ for r in failed:
562
+ click.echo(f" - {r['filename']}: {r.get('error', 'Unknown error')}")
563
+ if skipped:
564
+ click.echo("\nSkipped files (no download URL):")
565
+ for r in skipped:
566
+ click.echo(f" - {r['filename']}")
567
+
568
+
569
+ # ---------------------------------------------------------------------------
570
+ # Utilities
571
+ # ---------------------------------------------------------------------------
572
+
573
+
574
+ def _safe_join(base_dir: str, filename: str) -> str:
575
+ """Safely join base_dir and filename, preventing path traversal."""
576
+ safe_name = os.path.basename(filename)
577
+ if not safe_name:
578
+ raise click.ClickException(f"Invalid filename '{filename}'.")
579
+ result = os.path.join(base_dir, safe_name)
580
+ if not os.path.realpath(result).startswith(os.path.realpath(base_dir) + os.sep):
581
+ raise click.ClickException(
582
+ f"Filename '{filename}' resolves outside the target directory."
583
+ )
584
+ return result
585
+
586
+
587
+ def _get_extension_for_format(pkg_format: str) -> str:
588
+ """Get appropriate file extension for package format."""
589
+ format_extensions = {
590
+ "deb": "deb",
591
+ "rpm": "rpm",
592
+ "python": "whl",
593
+ "npm": "tgz",
594
+ "maven": "jar",
595
+ "nuget": "nupkg",
596
+ "gem": "gem",
597
+ "go": "tar.gz",
598
+ "docker": "tar",
599
+ "helm": "tgz",
600
+ "raw": "bin",
601
+ "terraform": "zip",
602
+ }
603
+ return format_extensions.get(pkg_format.lower(), "bin")
604
+
605
+
606
+ def _format_package_size(package: dict) -> str:
607
+ """Format package size for display."""
608
+ size = package.get("size", 0)
609
+ return _format_file_size(size)
610
+
611
+
612
+ def _format_file_size(size: int) -> str:
613
+ """Format file size in bytes to human-readable format."""
614
+ if size == 0:
615
+ return "Unknown"
616
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
617
+ if size < 1024.0:
618
+ return f"{size:.1f} {unit}"
619
+ size /= 1024.0
620
+ return f"{size:.1f} PB"