pray-cli 1.10.0 → 1.11.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d3cea2d193f4cf496417c5098ad4ff57b0a4b55bc7ee26664f72293c2fcbd06
4
- data.tar.gz: 89d8dc8a3c5c65062531e175587d3f86f27ca9630679dac925593be231062e8c
3
+ metadata.gz: cce969add8d5bb73ee6fc4b9910d785a0565e24afaeb6d4b80d9383f9fbe828b
4
+ data.tar.gz: f96cac68f4cf12079056d0ef300c57ab0efa47f3c9ab1e9967a9fe03cb8a5c37
5
5
  SHA512:
6
- metadata.gz: aa2d115c1300f177cd4c9dc8c8e4ab2802baf700be3b7124a644f6889ba4138cafe3a6be16d37005f5c2aba95b803740084139171466ebf7ba126e22f82e7d9c
7
- data.tar.gz: 1d0c8da737a56bcb21ad6798f6cf59c3fd6ad3384e2d423bb492b37ddf42ccb2c6231b6afebf2b2fc9dc8b7a44e748dd505910a0ceb6835b87006870ae3e68d9
6
+ metadata.gz: a0ec1da337a61097a65ebd7dc091879a978bbb1f13d6eba41ee4149735e8d37b3d32a7634e5b79fbc426e7ef068cf49ac5437c71b9492fe09f5ab439b1c5645b
7
+ data.tar.gz: 0ea56f7dd07ddf9be173bc0dd3ed399b91d280c36aaea1f703d0a60b9676dba15398cef53c876f71b3c86347611b9dd5f10af6d716152a69e4bf488d7b82996c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 1.11.0 (2026-09-04)
6
+
7
+ - Read the whole eight byte tar checksum field so `.praypkg` archives whose checksum is written as seven octal digits unpack instead of failing integrity.
8
+ - Share the source-keyed registry cache path with the Rust and TypeScript CLIs.
9
+ - Add `pray clean --unused` for lockfile-driven registry cache cleanup.
10
+ - Reject unsafe registry package and version path segments.
11
+
3
12
  ## 1.10.0 (2026-09-02)
4
13
 
5
14
  - Refuse to overwrite exclusive `file:` and `tree:` destinations that already exist with other bytes.
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Pray
6
+ module CacheClean
7
+ module_function
8
+
9
+ def clean_unused_registry_cache(project_root)
10
+ lockfile = Pray.read_lockfile(File.join(project_root, "Prayfile.lock"))
11
+ validate_lockfile_for_cleanup!(lockfile)
12
+ registry_root = File.expand_path(".pray/cache/registry", project_root)
13
+ retained = lockfile.package.filter_map do |package|
14
+ package_path = File.expand_path(package.path, project_root)
15
+ package_path if PathSafety.path_under_root?(registry_root, package_path)
16
+ end
17
+
18
+ begin
19
+ metadata = File.lstat(registry_root)
20
+ rescue Errno::ENOENT
21
+ return
22
+ end
23
+ if metadata.symlink? || !metadata.directory?
24
+ remove_path(registry_root)
25
+ return
26
+ end
27
+ prune_directory(registry_root, retained, remove_when_empty: false)
28
+ end
29
+
30
+ def validate_lockfile_for_cleanup!(lockfile)
31
+ validate_sha256_digest!("manifest_hash", lockfile.manifest_hash)
32
+ lockfile.package.each do |package|
33
+ if !package.path.is_a?(String) || package.path.empty?
34
+ raise Error.parse("lockfile", "package path must not be empty")
35
+ end
36
+ validate_sha256_digest!("package tree_hash", package.tree_hash)
37
+ validate_sha256_digest!("package artifact_hash", package.artifact_hash)
38
+ end
39
+ end
40
+
41
+ def validate_sha256_digest!(field, value)
42
+ digest = value.to_s.delete_prefix("sha256:")
43
+ return if value.is_a?(String) && value.start_with?("sha256:") && digest.match?(/\A[0-9a-f]{64}\z/)
44
+
45
+ raise Error.parse("lockfile", "#{field} must be a sha256 digest")
46
+ end
47
+
48
+ def prune_directory(path, retained, remove_when_empty:)
49
+ Dir.each_child(path) do |name|
50
+ entry = File.join(path, name)
51
+ protects_entry = retained.include?(entry)
52
+ leads_to_retained = retained.any? { |kept| PathSafety.path_under_root?(entry, kept) }
53
+ next if protects_entry
54
+
55
+ metadata = File.lstat(entry)
56
+ if leads_to_retained && metadata.directory? && !metadata.symlink?
57
+ prune_directory(entry, retained, remove_when_empty: true)
58
+ else
59
+ remove_path(entry)
60
+ end
61
+ end
62
+ Dir.rmdir(path) if remove_when_empty && Dir.empty?(path)
63
+ end
64
+
65
+ def remove_path(path)
66
+ metadata = File.lstat(path)
67
+ if metadata.directory? && !metadata.symlink?
68
+ FileUtils.rm_rf(path)
69
+ else
70
+ File.unlink(path)
71
+ end
72
+ rescue Errno::ENOENT
73
+ nil
74
+ end
75
+ end
76
+ end
@@ -6,10 +6,15 @@ module Pray
6
6
  puts "pray #{Pray::VERSION}"
7
7
  end
8
8
 
9
- def clean_command
10
- remove_path_if_exists(".pray/cache")
11
- remove_path_if_exists(".pray/vendor")
12
- remove_path_if_exists(".pray/state.json")
9
+ def clean_command(unused: false)
10
+ project_root = Invocation.project_root
11
+ if unused
12
+ CacheClean.clean_unused_registry_cache(project_root)
13
+ return
14
+ end
15
+ remove_path_if_exists(File.join(project_root, ".pray/cache"))
16
+ remove_path_if_exists(File.join(project_root, ".pray/vendor"))
17
+ remove_path_if_exists(File.join(project_root, ".pray/state.json"))
13
18
  end
14
19
  end
15
20
  end
data/lib/pray/cli/help.rb CHANGED
@@ -117,7 +117,7 @@ module Pray
117
117
  "remove" => "remove a package from Prayfile\n\nUsage: pray remove <name>",
118
118
  "unlock" => "clear a locked package pin\n\nUsage: pray unlock <package>",
119
119
  "vendor" => "copy resolved packages locally\n\nUsage: pray vendor",
120
- "clean" => "remove local cache and vendor trees\n\nUsage: pray clean",
120
+ "clean" => "remove local cache and vendor trees, or only unused registry entries\n\nUsage: pray clean [--unused]",
121
121
  "publish" => <<~TEXT.strip,
122
122
  upload packages to a registry or local root
123
123
 
@@ -67,7 +67,7 @@ module Pray
67
67
  when "outdated" then [:outdated, arguments]
68
68
  when "explain" then [:explain, arguments.shift]
69
69
  when "vendor" then [:vendor]
70
- when "clean" then [:clean]
70
+ when "clean" then [:clean, parse_clean_arguments(arguments, flags)]
71
71
  when "tree" then [:tree]
72
72
  when "sync" then [:sync, parse_sync_arguments(arguments)]
73
73
  when "trust" then parse_trust_command(arguments)
@@ -83,6 +83,24 @@ module Pray
83
83
  found
84
84
  end
85
85
 
86
+ def parse_clean_arguments(arguments, flags)
87
+ if flags.values_at(:check, :strict, :semantic, :locked, :frozen, :offline).any?
88
+ raise Error.unsupported("unknown clean flag")
89
+ end
90
+
91
+ unused = false
92
+ arguments.each do |argument|
93
+ if argument == "--unused" && !unused
94
+ unused = true
95
+ elsif argument.start_with?("--")
96
+ raise Error.unsupported("unknown clean flag: #{argument}")
97
+ else
98
+ raise Error.unsupported("unexpected clean argument: #{argument}")
99
+ end
100
+ end
101
+ {unused: unused}
102
+ end
103
+
86
104
  def parse_add_arguments(arguments)
87
105
  name = arguments.shift
88
106
  raise Error.unsupported("add requires a package name") unless name
data/lib/pray/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
 
5
5
  require_relative "invocation"
6
+ require_relative "cache_clean"
6
7
  require_relative "cli/parse"
7
8
  require_relative "cli/help"
8
9
  require_relative "cli/suggest"
@@ -97,7 +98,7 @@ module Pray
97
98
  in [:outdated, arguments] then outdated_command(arguments)
98
99
  in [:explain, name] then explain_command(name)
99
100
  in [:vendor] then raise Error.unsupported("vendor is not implemented yet in pray-cli Ruby")
100
- in [:clean] then clean_command
101
+ in [:clean, options] then clean_command(**options)
101
102
  in [:tree] then tree_command
102
103
  in [:trust_list, options] then trust_list_command(**options)
103
104
  in [:trust_show] then trust_show_command
@@ -80,5 +80,25 @@ module Pray
80
80
 
81
81
  validate_project_relative_path!(value)
82
82
  end
83
+
84
+ def validate_registry_cache_identity!(package_name, version)
85
+ namespace, name, extra_segment = package_name.to_s.split("/", -1)
86
+ unless extra_segment.nil? && registry_cache_segment_safe?(namespace) && registry_cache_segment_safe?(name)
87
+ raise Error.integrity("invalid registry package name: #{package_name}")
88
+ end
89
+ unless registry_cache_segment_safe?(version)
90
+ raise Error.integrity("invalid registry package version: #{version}")
91
+ end
92
+
93
+ [namespace, name]
94
+ end
95
+
96
+ def registry_cache_segment_safe?(value)
97
+ value.is_a?(String) &&
98
+ !value.empty? &&
99
+ ![".", ".."].include?(value) &&
100
+ !value.match?(%r{[/\\\0]})
101
+ end
102
+ private_class_method :registry_cache_segment_safe?
83
103
  end
84
104
  end
data/lib/pray/registry.rb CHANGED
@@ -47,8 +47,7 @@ module Pray
47
47
  project_root,
48
48
  source_url,
49
49
  declaration.name,
50
- selected.version,
51
- selected.artifact_hash
50
+ selected.version
52
51
  )
53
52
 
54
53
  if cache_ready?(cache_directory, selected)
@@ -91,8 +90,7 @@ module Pray
91
90
  project_root,
92
91
  source_key,
93
92
  declaration.name,
94
- selected.version,
95
- selected.artifact_hash
93
+ selected.version
96
94
  )
97
95
 
98
96
  if cache_ready?(cache_directory, selected)
@@ -238,15 +236,10 @@ module Pray
238
236
  )
239
237
  end
240
238
 
241
- def registry_cache_directory(project_root, source_key, package_name, version, artifact_hash)
242
- identifier = [
243
- source_key,
244
- package_name,
245
- version,
246
- artifact_hash || "no-artifact-hash"
247
- ].join(":")
248
- digest = Hashing.sha256_hex(identifier)[0, 16]
249
- File.join(project_root, ".pray", "cache", "registry", package_name.tr("/", "-"), version, digest)
239
+ def registry_cache_directory(project_root, source_key, package_name, version)
240
+ namespace, name = PathSafety.validate_registry_cache_identity!(package_name, version)
241
+ source_hash = Hashing.sha256_hex(source_key)[0, 16]
242
+ File.join(project_root, ".pray", "cache", "registry", namespace, name, version, source_hash)
250
243
  end
251
244
 
252
245
  def offline_package_error(package_name, version)
@@ -102,7 +102,10 @@ module Pray
102
102
  end
103
103
 
104
104
  def verify_checksum!(header)
105
- stored = parse_octal(header.byteslice(148, 6), "checksum")
105
+ # The checksum field is eight bytes. Writers fill it differently: six octal
106
+ # digits then NUL and space, seven digits then NUL, or space padding. Read the
107
+ # whole field and let parse_octal strip the terminator.
108
+ stored = parse_octal(header.byteslice(148, 8), "checksum")
106
109
  sum = 0
107
110
  header.bytes.each_with_index do |byte, index|
108
111
  sum += (index >= 148 && index < 156) ? 32 : byte
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.10.0"
5
- GENERATED_BY = "pray 1.10.0"
4
+ VERSION = "1.11.0"
5
+ GENERATED_BY = "pray 1.11.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pray-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -99,6 +99,7 @@ files:
99
99
  - lib/pray/archive.rb
100
100
  - lib/pray/archive_unpack.rb
101
101
  - lib/pray/auth_client.rb
102
+ - lib/pray/cache_clean.rb
102
103
  - lib/pray/cli.rb
103
104
  - lib/pray/cli/commands/auth.rb
104
105
  - lib/pray/cli/commands/distribution.rb