pray-cli 1.6.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +125 -0
  5. data/SECURITY.md +28 -0
  6. data/bin/polyrun +108 -0
  7. data/bin/pray +26 -0
  8. data/lib/pray/archive.rb +68 -0
  9. data/lib/pray/auth_client.rb +97 -0
  10. data/lib/pray/cli/commands/auth.rb +42 -0
  11. data/lib/pray/cli/commands/distribution.rb +17 -0
  12. data/lib/pray/cli/commands/init.rb +62 -0
  13. data/lib/pray/cli/commands/meta.rb +15 -0
  14. data/lib/pray/cli/commands/packages.rb +107 -0
  15. data/lib/pray/cli/commands/trust.rb +73 -0
  16. data/lib/pray/cli/commands/workflow.rb +126 -0
  17. data/lib/pray/cli/help.rb +168 -0
  18. data/lib/pray/cli/helpers.rb +166 -0
  19. data/lib/pray/cli/parse.rb +136 -0
  20. data/lib/pray/cli/parse_auth.rb +91 -0
  21. data/lib/pray/cli/parse_trust.rb +152 -0
  22. data/lib/pray/cli/suggest.rb +57 -0
  23. data/lib/pray/cli.rb +117 -0
  24. data/lib/pray/confess.rb +113 -0
  25. data/lib/pray/config.rb +52 -0
  26. data/lib/pray/constraint.rb +80 -0
  27. data/lib/pray/destination.rb +158 -0
  28. data/lib/pray/dotenv.rb +46 -0
  29. data/lib/pray/environment.rb +41 -0
  30. data/lib/pray/error.rb +55 -0
  31. data/lib/pray/format_manifest.rb +255 -0
  32. data/lib/pray/format_serialize.rb +135 -0
  33. data/lib/pray/git_sources.rb +228 -0
  34. data/lib/pray/hashing.rb +59 -0
  35. data/lib/pray/invocation.rb +110 -0
  36. data/lib/pray/literal.rb +382 -0
  37. data/lib/pray/lockfile.rb +259 -0
  38. data/lib/pray/lockfile_serialize.rb +125 -0
  39. data/lib/pray/manifest.rb +126 -0
  40. data/lib/pray/manifest_formatter.rb +56 -0
  41. data/lib/pray/manifest_json.rb +128 -0
  42. data/lib/pray/manifest_parser.rb +152 -0
  43. data/lib/pray/manifest_parser_blocks.rb +216 -0
  44. data/lib/pray/manifest_parser_helpers.rb +208 -0
  45. data/lib/pray/materialize.rb +120 -0
  46. data/lib/pray/package_spec.rb +245 -0
  47. data/lib/pray/path_safety.rb +41 -0
  48. data/lib/pray/plan.rb +83 -0
  49. data/lib/pray/project_context.rb +67 -0
  50. data/lib/pray/publish.rb +162 -0
  51. data/lib/pray/registry.rb +355 -0
  52. data/lib/pray/render.rb +360 -0
  53. data/lib/pray/resolve.rb +396 -0
  54. data/lib/pray/resolve_context.rb +19 -0
  55. data/lib/pray/serve.rb +130 -0
  56. data/lib/pray/serve_federation.rb +109 -0
  57. data/lib/pray/session.rb +90 -0
  58. data/lib/pray/ssh_agent.rb +115 -0
  59. data/lib/pray/statement_surface.rb +262 -0
  60. data/lib/pray/substitute.rb +50 -0
  61. data/lib/pray/sync.rb +219 -0
  62. data/lib/pray/terminal.rb +30 -0
  63. data/lib/pray/trust.rb +201 -0
  64. data/lib/pray/trust_feed.rb +110 -0
  65. data/lib/pray/trust_ops.rb +202 -0
  66. data/lib/pray/verify.rb +257 -0
  67. data/lib/pray/version.rb +6 -0
  68. data/lib/pray-cli.rb +4 -0
  69. data/lib/pray.rb +46 -0
  70. data/pray-cli.gemspec +48 -0
  71. metadata +187 -0
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module FormatSerialize
5
+ module_function
6
+
7
+ def serialize_recommended(manifest)
8
+ lines = [%(prayfile "#{manifest.prayfile_version}")]
9
+
10
+ unless manifest.sources.empty?
11
+ lines << ""
12
+ manifest.sources.each { |source| lines << format_source(source) }
13
+ end
14
+
15
+ unless manifest.symbols.empty?
16
+ lines << ""
17
+ lines << "pray do"
18
+ manifest.symbols.sort.each do |key, value|
19
+ lines << %( #{key} "#{value}")
20
+ end
21
+ lines << "end"
22
+ end
23
+
24
+ manifest.targets.each do |target|
25
+ next unless target.scoped
26
+
27
+ lines << ""
28
+ case target.mode
29
+ when "compose"
30
+ path = target.outputs.first || ""
31
+ lines << %(compose "#{path}" do)
32
+ target.entries.each do |entry|
33
+ lines << " #{format_destination_entry(entry, manifest)}"
34
+ end
35
+ lines << "end"
36
+ when "tree"
37
+ path = target.skills.first || ""
38
+ lines << %(tree "#{path}" do)
39
+ target.entries.each do |entry|
40
+ next unless entry.kind == "package"
41
+
42
+ package = find_package(manifest, entry.name)
43
+ lines << " #{ManifestMethods.format_package_declaration(package)}" if package
44
+ end
45
+ lines << "end"
46
+ end
47
+ end
48
+
49
+ file_packages = manifest.packages.select(&:file)
50
+ unless file_packages.empty?
51
+ lines << ""
52
+ file_packages.each { |package| lines << ManifestMethods.format_package_declaration(package) }
53
+ end
54
+
55
+ unbound = manifest.packages.select { |package| !package.bound && !package.file && package.groups.empty? }
56
+ unless unbound.empty?
57
+ lines << ""
58
+ unbound.each { |package| lines << ManifestMethods.format_package_declaration(package) }
59
+ end
60
+
61
+ grouped_packages(manifest).each do |group_names, packages|
62
+ lines << ""
63
+ lines << "group #{group_names.map { |name| ":#{name}" }.join(", ")} do"
64
+ packages.each { |package| lines << " #{ManifestMethods.format_package_declaration(package)}" }
65
+ lines << "end"
66
+ end
67
+
68
+ manifest.targets.each do |target|
69
+ next if target.scoped || !target_has_extras?(target)
70
+
71
+ lines << ""
72
+ lines << "target :#{target.name} do"
73
+ target.commands.each { |command| lines << %( commands "#{command}") }
74
+ target.rules.each { |rule| lines << %( rules "#{rule}") }
75
+ lines << " max_bytes #{target.max_bytes}" if target.max_bytes
76
+ lines << "end"
77
+ end
78
+
79
+ if manifest.render != RenderPolicy.default
80
+ lines << ""
81
+ lines << "render mode: :#{manifest.render.mode}, conflict: :#{manifest.render.conflict}, " \
82
+ "churn: :#{manifest.render.churn}"
83
+ end
84
+
85
+ lines << ""
86
+ lines.join("\n")
87
+ end
88
+
89
+ def target_has_extras?(target)
90
+ !target.commands.empty? || !target.rules.empty? || !target.max_bytes.nil?
91
+ end
92
+
93
+ def format_source(source)
94
+ parts = [%(source "#{source.name}")]
95
+ case source.kind
96
+ when "path"
97
+ parts << %(path: "#{source.url}")
98
+ when "git"
99
+ url = source.url.delete_prefix("git+")
100
+ parts << %(git: "#{url}")
101
+ else
102
+ parts << %("#{source.url}")
103
+ end
104
+ parts << %(distribution: "#{source.subdir}") if source.subdir
105
+ parts << %(tag: "#{source.tag}") if source.tag
106
+ parts << %(rev: "#{source.rev}") if source.rev
107
+ parts.join(", ")
108
+ end
109
+
110
+ def format_destination_entry(entry, manifest)
111
+ if entry.kind == "local"
112
+ return %(pray "#{entry.path}")
113
+ end
114
+
115
+ package = find_package(manifest, entry.name)
116
+ package ? ManifestMethods.format_package_declaration(package) : %(pray "#{entry.name}")
117
+ end
118
+
119
+ def find_package(manifest, name)
120
+ manifest.packages.find { |package| package.name == name }
121
+ end
122
+
123
+ def grouped_packages(manifest)
124
+ groups = {}
125
+ manifest.packages.each do |package|
126
+ next if package.groups.empty?
127
+
128
+ key = package.groups.dup
129
+ groups[key] ||= []
130
+ groups[key] << package
131
+ end
132
+ groups.sort_by { |names, _| names }.map { |names, packages| [names, packages] }
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+ require "pathname"
6
+
7
+ module Pray
8
+ GitSourceCheckout = Struct.new(:cache_directory, :revision, :subdir)
9
+
10
+ module GitSources
11
+ module_function
12
+
13
+ def prepare_git_sources(project_root, sources, lockfile, refresh: false)
14
+ checkouts = {}
15
+ sources.each do |source|
16
+ next unless source.kind == "git"
17
+
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)
21
+ if source_root
22
+ checkouts[source.name] = GitSourceCheckout.new(
23
+ cache_directory: source_root,
24
+ revision: "",
25
+ subdir: source.subdir
26
+ )
27
+ end
28
+ next
29
+ end
30
+
31
+ pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
32
+ cache_directory, revision = ensure_git_repository(
33
+ project_root,
34
+ clone_url,
35
+ refresh: refresh,
36
+ pinned_revision: pinned_revision,
37
+ sparse_subdir: source.subdir
38
+ )
39
+ checkouts[source.name] = GitSourceCheckout.new(
40
+ cache_directory: cache_directory,
41
+ revision: revision,
42
+ subdir: source.subdir
43
+ )
44
+ end
45
+ checkouts
46
+ end
47
+
48
+ def resolve_distribution_root(repo_root, subdir)
49
+ if subdir
50
+ path = File.join(repo_root, subdir)
51
+ return path if local_distribution_root?(path)
52
+
53
+ raise Error.resolution(
54
+ "no pray distribution root at subdir #{path.inspect} in git source #{repo_root.inspect}"
55
+ )
56
+ end
57
+
58
+ discover_distribution_root(repo_root) ||
59
+ raise(Error.resolution(
60
+ "no pray distribution root in git source #{repo_root.inspect}. " \
61
+ "Expected v1/packages at the repository root or under prayers/. " \
62
+ "Publish with `pray publish --root ./prayers` or point the source at a distribution repository."
63
+ ))
64
+ end
65
+
66
+ def discover_distribution_root(path)
67
+ return path if local_distribution_root?(path)
68
+
69
+ prayers_root = File.join(path, "prayers")
70
+ return prayers_root if local_distribution_root?(prayers_root)
71
+
72
+ nil
73
+ end
74
+
75
+ def local_distribution_root?(path)
76
+ File.directory?(File.join(path, "v1", "packages"))
77
+ end
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
85
+ return nil unless File.exist?(path)
86
+
87
+ discover_distribution_root(path)
88
+ end
89
+
90
+ def ensure_git_repository(project_root, clone_url, refresh:, pinned_revision:, sparse_subdir:)
91
+ cache_directory = git_source_cache_directory(project_root, clone_url)
92
+ if File.directory?(File.join(cache_directory, ".git"))
93
+ refresh_global_git_cache(clone_url) if refresh
94
+ if pinned_revision
95
+ checkout_git_revision(cache_directory, clone_url, pinned_revision, refresh)
96
+ elsif refresh
97
+ refresh_git_worktree(cache_directory, clone_url)
98
+ end
99
+ apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
100
+ revision = git_head_revision(cache_directory)
101
+ return [cache_directory, revision]
102
+ end
103
+
104
+ FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
105
+ FileUtils.mkdir_p(File.dirname(cache_directory))
106
+ if seed_git_cache_from_global(clone_url, cache_directory, project_root)
107
+ run_git_in_repo(cache_directory, "remote", "set-url", "origin", clone_url)
108
+ else
109
+ run_git(project_root, "clone", "--depth", "1", clone_url, cache_directory)
110
+ mirror_git_cache_to_global(clone_url, cache_directory)
111
+ end
112
+ checkout_git_revision(cache_directory, clone_url, pinned_revision, true) if pinned_revision
113
+ apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
114
+ [cache_directory, git_head_revision(cache_directory)]
115
+ end
116
+
117
+ def git_source_cache_directory(project_root, clone_url)
118
+ File.join(project_root, ".pray", "cache", "git", cache_key(clone_url))
119
+ end
120
+
121
+ def cache_key(text)
122
+ Hashing.sha256_prefixed(text)[7, 16]
123
+ end
124
+
125
+ def pinned_revision_for_source(lockfile, source)
126
+ if lockfile
127
+ entry = lockfile.source.find { |item| item.name == source.name && item.kind == "git" }
128
+ return entry.revision if entry&.revision
129
+ end
130
+ return source.rev if source.kind == "git" && source.rev
131
+
132
+ source.tag if source.kind == "git"
133
+ end
134
+
135
+ def local_filesystem_source?(clone_url)
136
+ clone_url.start_with?("file://") || Pathname.new(clone_url).absolute?
137
+ end
138
+
139
+ def local_git_repo_path(clone_url)
140
+ path = clone_url.delete_prefix("file://")
141
+ git_directory = File.join(path, ".git")
142
+ File.directory?(git_directory) ? path : nil
143
+ end
144
+
145
+ def global_cache_root
146
+ return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
147
+ return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
148
+
149
+ home = ENV["HOME"]
150
+ home ? File.join(home, ".cache", "pray") : nil
151
+ end
152
+
153
+ def global_git_cache_directory(clone_url)
154
+ root = global_cache_root
155
+ root ? File.join(root, "git", cache_key(clone_url)) : nil
156
+ end
157
+
158
+ def global_git_cache_ready?(global_cache)
159
+ File.directory?(File.join(global_cache, ".git")) || File.file?(File.join(global_cache, "HEAD"))
160
+ end
161
+
162
+ def seed_git_cache_from_global(clone_url, destination, working_directory)
163
+ global_cache = global_git_cache_directory(clone_url)
164
+ return false unless global_cache && global_git_cache_ready?(global_cache)
165
+
166
+ run_git(working_directory, "clone", "--depth", "1", "--quiet", global_cache, destination)
167
+ true
168
+ end
169
+
170
+ def mirror_git_cache_to_global(clone_url, project_cache)
171
+ global_cache = global_git_cache_directory(clone_url)
172
+ return unless global_cache
173
+ return if global_git_cache_ready?(global_cache)
174
+
175
+ FileUtils.mkdir_p(File.dirname(global_cache))
176
+ FileUtils.rm_rf(global_cache) if File.exist?(global_cache)
177
+ run_git(File.dirname(project_cache), "clone", "--bare", "--quiet", File.basename(project_cache), global_cache)
178
+ end
179
+
180
+ def apply_sparse_checkout(repository, subdir)
181
+ run_git_in_repo(repository, "sparse-checkout", "init", "--cone")
182
+ run_git_in_repo(repository, "sparse-checkout", "set", subdir)
183
+ end
184
+
185
+ def checkout_git_revision(repository, _clone_url, revision, refresh)
186
+ run_git_in_repo(repository, "fetch", "--depth", "1", "origin", revision) if refresh
187
+ run_git_in_repo(repository, "checkout", "--force", revision)
188
+ end
189
+
190
+ def refresh_git_worktree(repository, _clone_url)
191
+ run_git_in_repo(repository, "fetch", "--depth", "1", "origin")
192
+ run_git_in_repo(repository, "reset", "--hard", "origin/HEAD")
193
+ end
194
+
195
+ def refresh_global_git_cache(clone_url)
196
+ global_cache = global_git_cache_directory(clone_url)
197
+ return unless global_cache && global_git_cache_ready?(global_cache)
198
+
199
+ run_git_in_repo(global_cache, "fetch", "--depth", "1", "origin")
200
+ end
201
+
202
+ def git_head_revision(repository)
203
+ output, status = Open3.capture2e("git", "-C", repository, "rev-parse", "HEAD")
204
+ raise Error.resolution(command_error("git rev-parse HEAD", output)) unless status.success?
205
+
206
+ revision = output.strip
207
+ raise Error.resolution("git repository has no HEAD revision") if revision.empty?
208
+
209
+ revision
210
+ end
211
+
212
+ def run_git(cwd, *arguments)
213
+ output, status = Open3.capture2e("git", "-C", cwd, *arguments)
214
+ return if status.success?
215
+
216
+ raise Error.resolution(command_error("git #{arguments.join(" ")}", output))
217
+ end
218
+
219
+ def run_git_in_repo(repository, *arguments)
220
+ run_git(repository, *arguments)
221
+ end
222
+
223
+ def command_error(program, output)
224
+ message = output.strip
225
+ message.empty? ? "#{program} failed" : "#{program} failed: #{message}"
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "openssl"
5
+
6
+ module Pray
7
+ module Hashing
8
+ module_function
9
+
10
+ def sha256_hex(bytes)
11
+ Digest::SHA256.hexdigest(bytes)
12
+ end
13
+
14
+ def sha256_prefixed(bytes)
15
+ prefixed_hex_digest(Digest::SHA256.digest(bytes))
16
+ end
17
+
18
+ def prefixed_hex_digest(digest_bytes)
19
+ "sha256:#{digest_bytes.unpack1("H*")}"
20
+ end
21
+
22
+ def marker_id(seed)
23
+ sha256_hex(seed)[0, 8]
24
+ end
25
+
26
+ def normalize_line_endings(text)
27
+ text.gsub("\r\n", "\n").tr("\r", "\n")
28
+ end
29
+
30
+ def checksum_managed_span_content(body)
31
+ normalized = normalize_line_endings(body).sub(/\n+\z/, "")
32
+ sha256_prefixed(normalized)
33
+ end
34
+
35
+ def checksum_managed_body_line_refs(body_lines)
36
+ lines = trim_trailing_empty_lines(body_lines)
37
+ digest = OpenSSL::Digest::SHA256.new
38
+ lines.each_with_index do |line, index|
39
+ digest << "\n" if index.positive?
40
+ update_line_endings_normalized(digest, line)
41
+ end
42
+ prefixed_hex_digest(digest.digest)
43
+ end
44
+
45
+ def trim_trailing_empty_lines(lines)
46
+ trimmed = lines.dup
47
+ trimmed.pop while trimmed.last == ""
48
+ trimmed
49
+ end
50
+
51
+ def update_line_endings_normalized(digest, line)
52
+ digest << if line.include?("\r")
53
+ normalize_line_endings(line)
54
+ else
55
+ line
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Pray
6
+ module Invocation
7
+ module_function
8
+
9
+ def initialize(arguments)
10
+ options, remaining = parse_global_options(arguments)
11
+ self.context = ProjectContext.from_options(options)
12
+ remaining
13
+ end
14
+
15
+ def context
16
+ Thread.current[:pray_invocation_context]
17
+ end
18
+
19
+ def context=(value)
20
+ Thread.current[:pray_invocation_context] = value
21
+ end
22
+
23
+ def invocation_context
24
+ context || ProjectContext.from_current_directory
25
+ end
26
+
27
+ def manifest_path
28
+ invocation_context.manifest_path
29
+ end
30
+
31
+ def lockfile_path
32
+ invocation_context.lockfile_path
33
+ end
34
+
35
+ def project_root
36
+ invocation_context.project_root
37
+ end
38
+
39
+ def resolve_current_project(options = ResolveOptions.new)
40
+ context = invocation_context
41
+ resolved_options = options.dup
42
+ resolved_options.environment ||= context.environment
43
+ Resolve.resolve_project_in_context(context.manifest_path, context.project_root, resolved_options)
44
+ end
45
+
46
+ def resolve_current_project_with_git_refresh_fallback(options = ResolveOptions.new, allow_git_refresh_fallback: false)
47
+ resolve_current_project(options)
48
+ rescue Error => error
49
+ if allow_git_refresh_fallback &&
50
+ !options.offline &&
51
+ !options.refresh &&
52
+ Resolve.resolution_may_benefit_from_git_source_refresh?(error)
53
+ refreshed = options.dup
54
+ refreshed.refresh = true
55
+ resolve_current_project(refreshed)
56
+ else
57
+ raise
58
+ end
59
+ end
60
+
61
+ def parse_global_options(arguments)
62
+ options = ProjectInvocationOptions.new
63
+ remaining = []
64
+ index = 0
65
+ while index < arguments.length
66
+ argument = arguments[index]
67
+ case argument
68
+ when "--path"
69
+ index += 1
70
+ options.project_root = require_option_value("--path", arguments[index])
71
+ when "--file-path"
72
+ index += 1
73
+ options.manifest_path = require_option_value("--file-path", arguments[index])
74
+ when "--env", "--environment"
75
+ index += 1
76
+ options.environment = require_environment_value(argument, arguments[index])
77
+ else
78
+ if top_level_command?(argument) || argument.start_with?("-")
79
+ remaining.concat(arguments[index..])
80
+ break
81
+ end
82
+ remaining.concat(arguments[index..])
83
+ break
84
+ end
85
+ index += 1
86
+ end
87
+ [options, remaining]
88
+ end
89
+
90
+ def require_option_value(flag, value)
91
+ raise Error.usage("#{flag} requires a value") if value.nil? || value.empty?
92
+
93
+ value
94
+ end
95
+
96
+ def require_environment_value(flag, value)
97
+ raise Error.usage("#{flag} requires a value") if value.nil?
98
+
99
+ value
100
+ end
101
+
102
+ def top_level_command?(token)
103
+ %w[
104
+ manifest init prayer repo install add remove update unlock render plan apply verify drift
105
+ format fmt package publish login serve confess list outdated explain vendor clean tree sync trust
106
+ version -V --version -h --help help
107
+ ].include?(token)
108
+ end
109
+ end
110
+ end