pray-cli 1.15.0 → 1.16.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: 4754a8028d48136e6617f016890123bddb43a4fe27e87a05cc0da63ee5a22106
4
- data.tar.gz: 5293f312b5cf2b5adb17d2955b21c447aa7ec1e91b374636b475eef30ad68770
3
+ metadata.gz: 5950b63f50e919bd5864874919afde95413ed095954046604d570d4c3daf090a
4
+ data.tar.gz: 82056f753f0d244afd54de70b7528d1e5d98b0c67f0da18538eb5d906986f12b
5
5
  SHA512:
6
- metadata.gz: 4905aa110a4304e404dfe10e763ec4dee9c30e158659a2390e1af4e9be6aaf07e4cfb909c56023c7c622fdfb4689c250cdb40a632596f4033492bb1cbc7ecdb6
7
- data.tar.gz: 59a29e48784dd55363e8c2f63683fccc388029f4dbd18599a44f8efedb6b939e123e7203f96b1bc751edf2dbed8f49f30ca40842b3d13bdb3304a4d2a6e6e129
6
+ metadata.gz: e3289cc2a08751a1358c37ddfb692d042c2d522374b02ed370dd25e703f5c5fe3be66996bbd008a7e6dfbe26a9ea6b278b87f640035a55a235ef7e1ed1d00967
7
+ data.tar.gz: a54b4dbed546b3f301d42692033552c3878b2281f25fee70e647e02634c19541a7440542bddbade45619a2e6dd4cac8bcefc05ff89b035645195ce90a3bffad6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.16.0 (2026-09-15)
4
+
5
+ - Rewrite a two-component pessimistic Prayfile pin on `pray update --latest` (`~> 2.2` to `~> 2.4` when registry latest is 2.4.0), matching the Rust and TypeScript CLIs. Install a newer version that the current constraint already allows.
6
+ - Resolve a local `.praypkg` from `tarball:`, including offline when the archive is on disk.
7
+ - Refuse two-package dependency cycles during resolve, matching the Rust CLI.
8
+ - Check dest files against `Prayfile.lock` managed spans without resolving packages (`inspect_locked_destinations`, RFC 0106).
9
+
3
10
  ## 1.15.0 (2026-09-15)
4
11
 
5
12
  - Refresh path-fork trees with `pray update` (RFC 0114). Rewrite `spec.upstream` pins with `pray update --latest` when the pin does not admit the latest upstream version. Rewrite Prayfile constraints with `pray update --latest` when they do not admit the registry latest version. `pray update --latest --dry-run` prints the planned rewrite and does not write.
@@ -17,15 +17,28 @@ module Pray
17
17
  end
18
18
 
19
19
  def version_satisfies(version, constraint)
20
- normalized = normalize_version_constraint(constraint)
21
- return true if normalized.empty? || normalized == "*"
20
+ parts = constraint.to_s.split(",").map(&:strip).reject(&:empty?)
21
+ return true if parts.empty?
22
22
 
23
- requirement = Gem::Requirement.new(normalized.strip)
24
- requirement.satisfied_by?(Gem::Version.new(version))
23
+ parsed = Gem::Version.new(version)
24
+ parts.all? { |part| version_satisfies_one(parsed, part) }
25
25
  rescue ArgumentError => error
26
26
  raise Error.resolution(error.message)
27
27
  end
28
28
 
29
+ def version_satisfies_one(version, constraint)
30
+ normalized = normalize_version_constraint(constraint)
31
+ return true if normalized.empty? || normalized == "*"
32
+
33
+ requirement_text = if normalized.lstrip.start_with?("~>")
34
+ ruby_pessimistic_to_semver(normalized)
35
+ else
36
+ normalized.strip
37
+ end
38
+ clauses = requirement_text.split(",").map(&:strip).reject(&:empty?)
39
+ Gem::Requirement.new(*clauses).satisfied_by?(version)
40
+ end
41
+
29
42
  def pessimistic_constraint_for_version(version)
30
43
  parsed = Gem::Version.new(version)
31
44
  if parsed.segments[1].to_i.zero? && parsed.segments[2].to_i.zero?
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module DependencyGraph
5
+ module_function
6
+
7
+ def find_dependency_cycle(edges)
8
+ visiting = {}
9
+ visited = {}
10
+ stack = []
11
+ edges.keys.sort.each do |name|
12
+ next if visited[name]
13
+
14
+ cycle = depth_first_search(name, edges, visiting, visited, stack)
15
+ return cycle if cycle
16
+ end
17
+ nil
18
+ end
19
+
20
+ def depth_first_search(name, edges, visiting, visited, stack)
21
+ return nil if visited[name]
22
+ if visiting[name]
23
+ start = stack.index(name)
24
+ return nil unless start
25
+
26
+ return stack[start..] + [name]
27
+ end
28
+
29
+ visiting[name] = true
30
+ stack << name
31
+ Array(edges[name]).each do |dependency|
32
+ next unless edges.key?(dependency)
33
+
34
+ cycle = depth_first_search(dependency, edges, visiting, visited, stack)
35
+ return cycle if cycle
36
+ end
37
+ stack.pop
38
+ visiting.delete(name)
39
+ visited[name] = true
40
+ nil
41
+ end
42
+ end
43
+ end
@@ -16,8 +16,8 @@ module Pray
16
16
  next unless source.kind == "git"
17
17
 
18
18
  clone_url = source.url.delete_prefix("git+")
19
- if local_filesystem_source?(clone_url) && !local_git_repo_path(clone_url)
20
- source_root = local_git_source_root(clone_url)
19
+ if local_filesystem_source?(clone_url) && !local_git_repo_path(project_root, clone_url)
20
+ source_root = local_git_source_root(project_root, clone_url)
21
21
  if source_root
22
22
  checkouts[source.name] = GitSourceCheckout.new(
23
23
  cache_directory: source_root,
@@ -76,12 +76,8 @@ module Pray
76
76
  File.directory?(File.join(path, "v1", "packages"))
77
77
  end
78
78
 
79
- def local_git_source_root(clone_url)
80
- path = if clone_url.start_with?("file://")
81
- clone_url.delete_prefix("file://")
82
- else
83
- clone_url
84
- end
79
+ def local_git_source_root(project_root, clone_url)
80
+ path = clone_url_filesystem_path(project_root, clone_url)
85
81
  return nil unless File.exist?(path)
86
82
 
87
83
  discover_distribution_root(path)
@@ -136,12 +132,17 @@ module Pray
136
132
  clone_url.start_with?("file://") || Pathname.new(clone_url).absolute?
137
133
  end
138
134
 
139
- def local_git_repo_path(clone_url)
140
- path = clone_url.delete_prefix("file://")
135
+ def local_git_repo_path(project_root, clone_url)
136
+ path = clone_url_filesystem_path(project_root, clone_url)
141
137
  git_directory = File.join(path, ".git")
142
138
  File.directory?(git_directory) ? path : nil
143
139
  end
144
140
 
141
+ def clone_url_filesystem_path(project_root, clone_url)
142
+ path = clone_url.delete_prefix("file://")
143
+ Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
144
+ end
145
+
145
146
  def global_cache_root
146
147
  return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
147
148
  return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
@@ -133,6 +133,7 @@ module Pray
133
133
  def resolve_project(...) = Resolve.resolve_project(...)
134
134
  def render_project(...) = Render.render_project(...)
135
135
  def inspect_project(...) = Verify.inspect_project(...)
136
+ def inspect_locked_destinations(...) = Verify.inspect_locked_destinations(...)
136
137
  def verify_project(...) = Verify.verify_project(...)
137
138
  def drift_project(...) = Verify.drift_project(...)
138
139
  end
data/lib/pray/registry.rb CHANGED
@@ -8,6 +8,7 @@ require "pathname"
8
8
  require "time"
9
9
  require_relative "path_safety"
10
10
  require_relative "http_body"
11
+ require_relative "registry_paths"
11
12
 
12
13
  module Pray
13
14
  RegistryPackageVersion = Struct.new(
@@ -40,6 +41,17 @@ module Pray
40
41
  module_function
41
42
 
42
43
  def resolve_registry_package_root(project_root, source_url, declaration, preferred_version: nil, offline: false)
44
+ if local_source?(source_url)
45
+ return resolve_local_registry_package_root(
46
+ project_root,
47
+ source_url,
48
+ RegistryPaths.local_registry_root(project_root, source_url),
49
+ declaration,
50
+ preferred_version: preferred_version,
51
+ offline: offline
52
+ )
53
+ end
54
+
43
55
  metadata = fetch_package_metadata(source_url, declaration.name)
44
56
  registry_latest_version = registry_latest_version_label(metadata)
45
57
  selected = select_package_version(metadata, declaration.constraint, preferred_version)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Pray
6
+ module RegistryPaths
7
+ module_function
8
+
9
+ def local_registry_root(project_root, source_url)
10
+ path = source_url.delete_prefix("file://")
11
+ Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
12
+ end
13
+ end
14
+ end
data/lib/pray/resolve.rb CHANGED
@@ -98,6 +98,8 @@ module Pray
98
98
  end
99
99
  raise Error.resolution(local_errors.join("\n")) unless local_errors.empty?
100
100
 
101
+ ResolveDeps.reject_dependency_cycles(packages)
102
+
101
103
  ResolvedProject.new(
102
104
  manifest_path: manifest_path,
103
105
  project_root: project_root,
@@ -152,7 +154,7 @@ module Pray
152
154
  def resolve_package(project_root, sources, git_sources, user_config, declaration, lockfile, offline: false, options: nil)
153
155
  options ||= ResolveOptions.new(offline: offline)
154
156
  root, registry_latest_version = resolve_package_root_with_metadata(
155
- project_root, sources, git_sources, user_config, declaration, lockfile, offline: options.offline
157
+ project_root, sources, git_sources, user_config, declaration, lockfile, options
156
158
  )
157
159
  spec_path = find_prayspec_file(root)
158
160
  spec_text = File.read(spec_path)
@@ -194,12 +196,16 @@ module Pray
194
196
  end
195
197
 
196
198
  def resolve_package_root_with_metadata(
197
- project_root, sources, git_sources, user_config, declaration, lockfile, offline: false
199
+ project_root, sources, git_sources, user_config, declaration, lockfile, options = nil
198
200
  )
201
+ options ||= ResolveOptions.new
202
+ offline = options.offline
203
+ preferred_version = options.preferred_lock_version(lockfile, declaration.name)
199
204
  if (local_path = user_config.local.package[declaration.name])
200
205
  return [File.expand_path(local_path, project_root), nil]
201
206
  end
202
207
  return [File.expand_path(declaration.path, project_root), nil] if declaration.path
208
+ return [ResolveTarball.package_root(project_root, declaration.tarball, offline: offline), nil] if declaration.tarball
203
209
  source_name = ResolveSource.implied_source_name(declaration, sources)
204
210
  if source_name
205
211
  source = sources[source_name]
@@ -212,7 +218,7 @@ module Pray
212
218
  "local:#{source_name}",
213
219
  source_root,
214
220
  declaration,
215
- preferred_version: lockfile_preferred_version(lockfile, declaration.name),
221
+ preferred_version: preferred_version,
216
222
  offline: offline
217
223
  )
218
224
  return [resolved.root, resolved.registry_latest_version]
@@ -226,7 +232,7 @@ module Pray
226
232
  project_root,
227
233
  source.url,
228
234
  declaration,
229
- preferred_version: lockfile_preferred_version(lockfile, declaration.name),
235
+ preferred_version: preferred_version,
230
236
  offline: offline
231
237
  )
232
238
  return [resolved.root, resolved.registry_latest_version]
@@ -245,7 +251,7 @@ module Pray
245
251
  source_key,
246
252
  distribution_root,
247
253
  declaration,
248
- preferred_version: lockfile_preferred_version(lockfile, declaration.name),
254
+ preferred_version: preferred_version,
249
255
  offline: offline
250
256
  )
251
257
  rescue Error => error
@@ -262,9 +268,7 @@ module Pray
262
268
  end
263
269
  end
264
270
 
265
- if declaration.git || declaration.tarball || declaration.oci
266
- raise Error.unsupported("remote sources are not implemented yet")
267
- end
271
+ raise Error.unsupported("remote sources are not implemented yet") if declaration.git || declaration.oci
268
272
 
269
273
  [File.join(project_root, declaration.name.tr("/", "-")), nil]
270
274
  end
@@ -273,12 +277,6 @@ module Pray
273
277
  resolve_package_root_with_metadata(project_root, sources, git_sources, user_config, declaration, lockfile).first
274
278
  end
275
279
 
276
- def lockfile_preferred_version(lockfile, package_name)
277
- return nil unless lockfile
278
-
279
- lockfile.package.find { |entry| entry.name == package_name }&.version
280
- end
281
-
282
280
  def resolve_local_file(project_root, declaration)
283
281
  path = File.join(project_root, declaration.path)
284
282
  unless File.exist?(path)
@@ -15,5 +15,12 @@ module Pray
15
15
  )
16
16
  super
17
17
  end
18
+
19
+ def preferred_lock_version(lockfile, package_name)
20
+ return nil unless lockfile
21
+ return nil if ignore_locked_versions || unlocked_packages.include?(package_name)
22
+
23
+ lockfile.package.find { |entry| entry.name == package_name }&.version
24
+ end
18
25
  end
19
26
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResolveDeps
5
+ module_function
6
+
7
+ def reject_dependency_cycles(packages)
8
+ edges = {}
9
+ packages.each do |package|
10
+ edges[package.declaration.name] = package.spec.dependencies.map(&:name)
11
+ end
12
+ cycle = DependencyGraph.find_dependency_cycle(edges)
13
+ return unless cycle
14
+
15
+ raise Error.resolution("dependency cycle detected: #{cycle.join(" -> ")}")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require_relative "archive_unpack"
6
+ require_relative "hashing"
7
+ require_relative "resource_limits"
8
+
9
+ module Pray
10
+ module ResolveTarball
11
+ module_function
12
+
13
+ def package_root(project_root, tarball, offline:)
14
+ artifact_bytes = read_tarball_bytes(project_root, tarball, offline: offline)
15
+ if artifact_bytes.bytesize > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
16
+ raise Error.integrity(
17
+ "package archive exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes"
18
+ )
19
+ end
20
+
21
+ cache_key = Hashing.sha256_prefixed(artifact_bytes).delete_prefix("sha256:")[0, 16]
22
+ cache_directory = File.join(project_root, ".pray", "cache", "tarball", cache_key)
23
+ if File.directory?(cache_directory)
24
+ begin
25
+ Resolve.find_prayspec_file(cache_directory)
26
+ return cache_directory
27
+ rescue Error
28
+ nil
29
+ end
30
+ end
31
+
32
+ staging_directory = "#{cache_directory}.staging"
33
+ FileUtils.rm_rf(staging_directory)
34
+ FileUtils.mkdir_p(staging_directory)
35
+ ArchiveUnpack.unpack_praypkg(artifact_bytes, staging_directory)
36
+ Resolve.find_prayspec_file(staging_directory)
37
+ FileUtils.mkdir_p(File.dirname(cache_directory))
38
+ FileUtils.rm_rf(cache_directory)
39
+ FileUtils.mv(staging_directory, cache_directory)
40
+ cache_directory
41
+ end
42
+
43
+ def read_tarball_bytes(project_root, tarball, offline:)
44
+ if tarball.start_with?("http://", "https://")
45
+ if offline
46
+ raise Error.resolution(
47
+ "tarball #{tarball} is not cached locally and offline mode is enabled"
48
+ )
49
+ end
50
+ return Registry.http_get(tarball)
51
+ end
52
+
53
+ path = local_tarball_path(project_root, tarball)
54
+ unless File.file?(path)
55
+ raise Error.resolution("tarball missing at #{path}")
56
+ end
57
+
58
+ File.binread(path)
59
+ end
60
+
61
+ def local_tarball_path(project_root, tarball)
62
+ path = tarball.delete_prefix("file://")
63
+ Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Verify
5
+ module_function
6
+
7
+ def inspect_locked_destinations(project_root, lockfile)
8
+ report = VerificationReport.new
9
+ lockfile.managed_span.group_by(&:target).each do |target_path, spans|
10
+ absolute_path = File.join(project_root, target_path)
11
+ unless File.exist?(absolute_path)
12
+ report.findings << VerificationFinding.new(
13
+ kind: "verify_error",
14
+ message: "Rendered file `#{target_path}` is missing. Run `pray install` to generate it."
15
+ )
16
+ next
17
+ end
18
+
19
+ text = RenderDest.decode_utf8(
20
+ RenderDest.read_regular_bytes(absolute_path, target_path),
21
+ target_path
22
+ )
23
+ markers = marker_positions(text.lines(chomp: true))
24
+ spans.each do |span|
25
+ marker = markers[span.id]
26
+ unless marker
27
+ report.findings << VerificationFinding.new(
28
+ kind: "removed_prayer",
29
+ message: "`#{target_path}` is missing managed marker `#{span.id}` for `#{span.package}::#{span.export}`. Run `pray install` to restore the managed span."
30
+ )
31
+ next
32
+ end
33
+ next if marker[2] == span.ideal_checksum
34
+
35
+ report.findings << VerificationFinding.new(
36
+ kind: "custom_implementation",
37
+ message: "`#{target_path}` marker `#{span.id}` (`#{span.package}::#{span.export}`) was edited. Restore the managed block or run `pray install` to regenerate it."
38
+ )
39
+ end
40
+ find_orphan_marker_findings(spans, markers, target_path).each do |finding|
41
+ report.findings << finding
42
+ end
43
+ end
44
+ report
45
+ end
46
+ end
47
+ end
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.15.0"
5
- GENERATED_BY = "pray 1.15.0"
4
+ VERSION = "1.16.0"
5
+ GENERATED_BY = "pray 1.16.0"
6
6
  end
data/lib/pray.rb CHANGED
@@ -21,6 +21,9 @@ require_relative "pray/environment"
21
21
  require_relative "pray/resolve_context"
22
22
  require_relative "pray/resolve_source"
23
23
  require_relative "pray/resolve_exports"
24
+ require_relative "pray/dependency_graph"
25
+ require_relative "pray/resolve_deps"
26
+ require_relative "pray/resolve_tarball"
24
27
  require_relative "pray/invocation"
25
28
  require_relative "pray/git_refresh"
26
29
  require_relative "pray/resolve"
@@ -38,6 +41,7 @@ require_relative "pray/render_layout"
38
41
  require_relative "pray/verify_position"
39
42
  require_relative "pray/verify_provisioned"
40
43
  require_relative "pray/verify"
44
+ require_relative "pray/verify_locked_dest"
41
45
  require_relative "pray/resource_limits"
42
46
  require_relative "pray/http_body"
43
47
  require_relative "pray/registry_install"
@@ -61,6 +65,6 @@ class << Pray
61
65
  :parse_package_spec, :parse_lockfile, :read_lockfile, :serialize_lockfile, :lockfile_hash,
62
66
  :write_lockfile, :write_lockfile_if_changed, :lockfiles_equivalent?, :build_lockfile,
63
67
  :resolve_project, :render_project, :materialize_project,
64
- :inspect_project, :verify_project, :drift_project,
68
+ :inspect_project, :inspect_locked_destinations, :verify_project, :drift_project,
65
69
  :default_manifest_path, :default_lockfile_path, :project_root_from_manifest
66
70
  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.15.0
4
+ version: 1.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -125,6 +125,7 @@ files:
125
125
  - lib/pray/confess.rb
126
126
  - lib/pray/config.rb
127
127
  - lib/pray/constraint.rb
128
+ - lib/pray/dependency_graph.rb
128
129
  - lib/pray/destination.rb
129
130
  - lib/pray/dotenv.rb
130
131
  - lib/pray/environment.rb
@@ -156,6 +157,7 @@ files:
156
157
  - lib/pray/registry.rb
157
158
  - lib/pray/registry_install.rb
158
159
  - lib/pray/registry_integrity.rb
160
+ - lib/pray/registry_paths.rb
159
161
  - lib/pray/render.rb
160
162
  - lib/pray/render_content.rb
161
163
  - lib/pray/render_dest.rb
@@ -163,8 +165,10 @@ files:
163
165
  - lib/pray/render_patch.rb
164
166
  - lib/pray/resolve.rb
165
167
  - lib/pray/resolve_context.rb
168
+ - lib/pray/resolve_deps.rb
166
169
  - lib/pray/resolve_exports.rb
167
170
  - lib/pray/resolve_source.rb
171
+ - lib/pray/resolve_tarball.rb
168
172
  - lib/pray/resource_limits.rb
169
173
  - lib/pray/serve.rb
170
174
  - lib/pray/serve_federation.rb
@@ -187,6 +191,7 @@ files:
187
191
  - lib/pray/upstream_merge.rb
188
192
  - lib/pray/upstream_refresh.rb
189
193
  - lib/pray/verify.rb
194
+ - lib/pray/verify_locked_dest.rb
190
195
  - lib/pray/verify_position.rb
191
196
  - lib/pray/verify_provisioned.rb
192
197
  - lib/pray/version.rb