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,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ PackageExport = Struct.new(:kind, :path, :summary, :only, :except, :default_path) do
5
+ def initialize(kind: "fragment", path: "", summary: nil, only: [], except: [], default_path: nil)
6
+ super
7
+ end
8
+ end
9
+ PackageSkill = Struct.new(:path, :summary)
10
+ PackageTemplate = Struct.new(:path, :summary)
11
+ PackageDependency = Struct.new(:name, :constraint, :optional)
12
+
13
+ PackageSpec = Struct.new(
14
+ :name, :version, :summary, :description, :authors, :license, :homepage,
15
+ :source_code_uri, :changelog_uri, :prayfile_version, :files, :exports,
16
+ :skills, :templates, :adapters, :targets, :dependencies, :metadata
17
+ ) do
18
+ def initialize(
19
+ name: "", version: "", summary: nil, description: nil, authors: [], license: nil,
20
+ homepage: nil, source_code_uri: nil, changelog_uri: nil, prayfile_version: nil,
21
+ files: [], exports: {}, skills: {}, templates: {}, adapters: {}, targets: [],
22
+ dependencies: [], metadata: {}
23
+ )
24
+ super
25
+ end
26
+
27
+ def canonicalized
28
+ dup.tap do |copy|
29
+ copy.files = files.sort
30
+ copy.authors = authors.sort
31
+ copy.targets = targets.sort
32
+ copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
33
+ end
34
+ end
35
+
36
+ def tree_hash_for_root(root)
37
+ file_bytes = {}
38
+ files.each do |file|
39
+ path = File.join(root, file)
40
+ raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
41
+ raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
42
+
43
+ file_bytes[file] = File.binread(path)
44
+ end
45
+ self.class.tree_hash_from_file_bytes(file_bytes)
46
+ end
47
+
48
+ def self.tree_hash_from_file_bytes(file_bytes)
49
+ entries = file_bytes.map { |path, bytes| [path, Hashing.sha256_prefixed(bytes)] }.sort_by(&:first)
50
+ serialized = entries.map do |path, hash|
51
+ "file\0regular\0#{path}\0#{hash}\n"
52
+ end.join
53
+ Hashing.sha256_prefixed(serialized)
54
+ end
55
+ end
56
+
57
+ module PackageSpecParser
58
+ module_function
59
+
60
+ module ParserHelpers
61
+ include ManifestMethods::ParserHelpers
62
+
63
+ def parse_dependency(rest, optional)
64
+ values, keywords = parse_call(rest)
65
+ name = string_from_value(values.first)
66
+ constraint = values[1] ? string_from_value(values[1]) : "*"
67
+ PackageDependency.new(
68
+ name: name,
69
+ constraint: constraint,
70
+ optional: keywords["optional"]&.as_bool || optional
71
+ )
72
+ end
73
+
74
+ def parse_exports(value)
75
+ map = Literal.parse_literal_map(value)
76
+ exports = {}
77
+ map.each do |name, literal|
78
+ entry = literal.as_map
79
+ raise Error.parse("prayspec", "export #{name} must be a map") unless entry
80
+
81
+ exports[name] = PackageExport.new(
82
+ kind: map_string(entry, "type") || "fragment",
83
+ path: map_string(entry, "path") || raise(Error.parse("prayspec", "export #{name} missing path")),
84
+ summary: map_string(entry, "summary"),
85
+ only: map_string_array(entry, "only"),
86
+ except: map_string_array(entry, "except"),
87
+ default_path: map_string(entry, "default_path")
88
+ )
89
+ end
90
+ exports
91
+ end
92
+
93
+ def parse_skills(value)
94
+ map = Literal.parse_literal_map(value)
95
+ map.transform_values do |literal|
96
+ entry = literal.as_map
97
+ raise Error.parse("prayspec", "skill must be a map") unless entry
98
+
99
+ PackageSkill.new(
100
+ path: map_string(entry, "path") || raise(Error.parse("prayspec", "skill missing path")),
101
+ summary: map_string(entry, "summary")
102
+ )
103
+ end
104
+ end
105
+
106
+ def parse_templates(value)
107
+ map = Literal.parse_literal_map(value)
108
+ map.transform_values do |literal|
109
+ entry = literal.as_map
110
+ raise Error.parse("prayspec", "template must be a map") unless entry
111
+
112
+ PackageTemplate.new(
113
+ path: map_string(entry, "path") || raise(Error.parse("prayspec", "template missing path")),
114
+ summary: map_string(entry, "summary")
115
+ )
116
+ end
117
+ end
118
+
119
+ def parse_string_map(value)
120
+ Literal.parse_literal_map(value).transform_values { |literal| string_from_value(literal) }
121
+ end
122
+
123
+ def map_string(map, key)
124
+ map[key]&.as_string
125
+ end
126
+
127
+ def map_string_array(map, key)
128
+ values = map[key]&.as_array
129
+ return [] unless values
130
+
131
+ values.filter_map(&:as_string)
132
+ end
133
+
134
+ def array_of_strings(value)
135
+ array = Literal.parse_literal(value)
136
+ values = array.as_array
137
+ raise Error.parse("prayspec", "expected array") unless values
138
+
139
+ values.map { |entry| string_from_value(entry) }
140
+ end
141
+
142
+ def string_from_value(value)
143
+ text = value.as_string
144
+ raise Error.parse("prayspec", "expected string-like literal, found #{value.inspect}") unless text
145
+
146
+ text
147
+ end
148
+
149
+ def string_from_literal(value)
150
+ string_from_value(Literal.parse_literal(value))
151
+ end
152
+ end
153
+
154
+ def parse_package_spec(text)
155
+ lines = Literal.prepare_parser_lines(text)
156
+ BlockParser.new(lines).parse_root
157
+ end
158
+
159
+ class BlockParser
160
+ include ParserHelpers
161
+
162
+ def initialize(lines)
163
+ @lines = lines
164
+ @cursor = 0
165
+ end
166
+
167
+ def parse_root
168
+ expect_start!
169
+ spec = PackageSpec.new
170
+ while (statement = next_statement)
171
+ return spec.canonicalized if statement == "end"
172
+
173
+ apply_statement(spec, statement)
174
+ end
175
+ raise Error.parse("prayspec", "missing 'end'")
176
+ end
177
+
178
+ def expect_start!
179
+ statement = next_statement
180
+ raise Error.parse("prayspec", "empty package spec") unless statement
181
+ unless statement.start_with?("Package::Specification.new")
182
+ raise Error.parse("prayspec", "expected Package::Specification.new")
183
+ end
184
+ end
185
+
186
+ def apply_statement(spec, statement)
187
+ case statement
188
+ when /\Aspec\.add_dependency (.+)\z/
189
+ spec.dependencies << parse_dependency(Regexp.last_match(1), false)
190
+ when /\Aspec\.add_optional_dependency (.+)\z/
191
+ spec.dependencies << parse_dependency(Regexp.last_match(1), true)
192
+ when /\Aspec\.(.+) = (.+)\z/
193
+ apply_assignment(spec, Regexp.last_match(1).strip, Regexp.last_match(2).strip)
194
+ else
195
+ raise Error.parse("prayspec", "unrecognized statement: #{statement}")
196
+ end
197
+ end
198
+
199
+ def apply_assignment(spec, field, value)
200
+ case field
201
+ when "name" then spec.name = string_from_literal(value)
202
+ when "version" then spec.version = string_from_literal(value)
203
+ when "summary" then spec.summary = string_from_literal(value)
204
+ when "description" then spec.description = string_from_literal(value)
205
+ when "authors" then spec.authors = array_of_strings(value)
206
+ when "license" then spec.license = string_from_literal(value)
207
+ when "homepage" then spec.homepage = string_from_literal(value)
208
+ when "source_code_uri" then spec.source_code_uri = string_from_literal(value)
209
+ when "changelog_uri" then spec.changelog_uri = string_from_literal(value)
210
+ when "prayfile_version" then spec.prayfile_version = string_from_literal(value)
211
+ when "files" then spec.files = array_of_strings(value)
212
+ when "targets" then spec.targets = array_of_strings(value)
213
+ when "exports" then spec.exports = parse_exports(value)
214
+ when "skills" then spec.skills = parse_skills(value)
215
+ when "templates" then spec.templates = parse_templates(value)
216
+ when "adapters" then spec.adapters = parse_string_map(value)
217
+ when "metadata" then spec.metadata = Literal.parse_literal_map(value)
218
+ else
219
+ raise Error.parse("prayspec", "unsupported assignment: #{field}")
220
+ end
221
+ end
222
+
223
+ def next_statement
224
+ while @cursor < @lines.length
225
+ statement = @lines[@cursor].strip
226
+ @cursor += 1
227
+ next if statement.empty?
228
+
229
+ while !statement.end_with?(" do") && statement != "end" && @cursor < @lines.length &&
230
+ (statement.rstrip.end_with?(",") || !Literal.is_balanced?(statement))
231
+ next_line = @lines[@cursor].strip
232
+ @cursor += 1
233
+ next if next_line.empty?
234
+
235
+ statement = "#{statement} #{next_line}"
236
+ end
237
+ return statement
238
+ end
239
+ nil
240
+ end
241
+ end
242
+ end
243
+
244
+ extend PackageSpecParser
245
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Pray
6
+ module PathSafety
7
+ module_function
8
+
9
+ def path_under_root?(root, candidate)
10
+ root_path = Pathname.new(File.expand_path(root)).cleanpath
11
+ candidate_path = Pathname.new(File.expand_path(candidate)).cleanpath
12
+ candidate_path == root_path || candidate_path.to_s.start_with?("#{root_path}#{File::SEPARATOR}")
13
+ end
14
+
15
+ def join_under_root(root, *segments)
16
+ root_path = Pathname.new(File.expand_path(root)).cleanpath
17
+ candidate = root_path.join(*segments).cleanpath
18
+ return candidate.to_s if path_under_root?(root_path.to_s, candidate.to_s)
19
+
20
+ nil
21
+ end
22
+
23
+ def reject_unsafe_package_name!(package_name)
24
+ if package_name.nil? || package_name.empty? || package_name.include?("\0") || package_name.include?("\\") ||
25
+ package_name.include?("..")
26
+ raise Error.resolution("invalid package name: #{package_name.inspect}")
27
+ end
28
+
29
+ package_name
30
+ end
31
+
32
+ def sanitize_relative_path(path)
33
+ cleaned = path.to_s.delete_prefix("/").tr("\\", "/")
34
+ if cleaned.empty? || cleaned.include?("\0") || cleaned.split("/").include?("..")
35
+ raise Error.resolution("unsafe relative path: #{path.inspect}")
36
+ end
37
+
38
+ cleaned
39
+ end
40
+ end
41
+ end
data/lib/pray/plan.rb ADDED
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Pray
6
+ module Plan
7
+ MaterializationPreview = Struct.new(
8
+ :package_lines, :lockfile, :targets, :provisioned, :warnings
9
+ )
10
+
11
+ module_function
12
+
13
+ def build_materialization_preview(project, rendered, lockfile, lockfile_path, previous_lockfile)
14
+ MaterializationPreview.new(
15
+ package_lines: package_summary_lines(previous_lockfile, lockfile, project),
16
+ lockfile: lockfile_change_status(lockfile_path, lockfile),
17
+ targets: rendered.map { |target| target_change(project, target) },
18
+ provisioned: Render.planned_provisioned_files(project).map { |file| provisioned_change(project, file) },
19
+ warnings: []
20
+ )
21
+ end
22
+
23
+ def print_materialization_report(preview, mode)
24
+ puts (mode == :plan) ? "Plan" : "Install"
25
+ preview.package_lines.each { |line| puts line }
26
+ puts "Lockfile: #{preview.lockfile}"
27
+ preview.targets.each do |path, change|
28
+ puts "Target #{path}: #{change}"
29
+ end
30
+ preview.provisioned.each do |path, change|
31
+ puts "Provisioned #{path}: #{change}"
32
+ end
33
+ preview.warnings.each { |warning| puts "Warning: #{warning}" }
34
+ end
35
+
36
+ def package_summary_lines(previous_lockfile, lockfile, _project)
37
+ if previous_lockfile.nil?
38
+ lockfile.package.map do |entry|
39
+ "Package #{entry.name} #{entry.version} (new)"
40
+ end
41
+ else
42
+ lockfile.package.filter_map do |entry|
43
+ previous = previous_lockfile.package.find { |package| package.name == entry.name }
44
+ next "Package #{entry.name} #{entry.version} (new)" unless previous
45
+ next if previous.version == entry.version && previous.tree_hash == entry.tree_hash
46
+
47
+ "Package #{entry.name} #{previous.version} -> #{entry.version}"
48
+ end
49
+ end
50
+ end
51
+
52
+ def lockfile_change_status(lockfile_path, lockfile)
53
+ return "create" unless File.exist?(lockfile_path)
54
+
55
+ existing = Pray.read_lockfile(lockfile_path)
56
+ Pray.lockfiles_equivalent?(lockfile, existing) ? "unchanged" : "update"
57
+ end
58
+
59
+ def target_change(project, target)
60
+ path = File.join(project.project_root, target.path)
61
+ change = if !File.exist?(path)
62
+ "write"
63
+ elsif File.read(path) == target.content
64
+ "unchanged"
65
+ else
66
+ "update"
67
+ end
68
+ [target.path, change]
69
+ end
70
+
71
+ def provisioned_change(project, file)
72
+ destination = File.join(project.project_root, file.path)
73
+ change = if !File.exist?(destination)
74
+ "write"
75
+ elsif FileUtils.compare_file(destination, file.source)
76
+ "unchanged"
77
+ else
78
+ "update"
79
+ end
80
+ [file.path, change]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Pray
6
+ ENV_PROJECT_PATH = "PRAY_PATH"
7
+ ENV_MANIFEST_PATH = "PRAY_FILE_PATH"
8
+ ENV_ENVIRONMENT = "PRAY_ENV"
9
+
10
+ ProjectInvocationContext = Struct.new(:project_root, :manifest_path, :environment) do
11
+ def lockfile_path
12
+ File.join(project_root, "Prayfile.lock")
13
+ end
14
+ end
15
+
16
+ ProjectInvocationOptions = Struct.new(:project_root, :manifest_path, :environment)
17
+
18
+ module ProjectContext
19
+ module_function
20
+
21
+ def from_current_directory
22
+ from_options(ProjectInvocationOptions.new)
23
+ end
24
+
25
+ def from_options(options)
26
+ cwd = Dir.pwd
27
+ dotenv = Dotenv.load_dotenv_variables(cwd)
28
+ project_root_hint = options.project_root ||
29
+ env_value(ENV_PROJECT_PATH) ||
30
+ dotenv[ENV_PROJECT_PATH] ||
31
+ cwd
32
+ project_root = canonicalize_path(cwd, project_root_hint)
33
+ manifest_hint = options.manifest_path ||
34
+ env_value(ENV_MANIFEST_PATH) ||
35
+ dotenv[ENV_MANIFEST_PATH] ||
36
+ "Prayfile"
37
+ manifest_path = if Pathname(manifest_hint).absolute?
38
+ canonicalize_path(cwd, manifest_hint)
39
+ else
40
+ canonicalize_path(project_root, manifest_hint)
41
+ end
42
+ environment = (options.environment || env_value(ENV_ENVIRONMENT) || dotenv[ENV_ENVIRONMENT])&.strip
43
+ environment = nil if environment.nil? || environment.empty?
44
+
45
+ ProjectInvocationContext.new(
46
+ project_root: project_root,
47
+ manifest_path: manifest_path,
48
+ environment: environment
49
+ )
50
+ end
51
+
52
+ def env_value(key)
53
+ value = ENV[key]
54
+ return nil if value.nil? || value.strip.empty?
55
+
56
+ value
57
+ end
58
+
59
+ def canonicalize_path(base, path)
60
+ resolved = Pathname(path).absolute? ? Pathname(path) : Pathname(base).join(path)
61
+ resolved = resolved.expand_path
62
+ resolved.exist? ? resolved.realpath.to_s : resolved.cleanpath.to_s
63
+ rescue
64
+ resolved.to_s
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+
7
+ module Pray
8
+ RegistryIndex = Struct.new(:spec, :packages) do
9
+ def initialize(spec: "prayfile-distribution-1", packages: [])
10
+ super
11
+ end
12
+ end
13
+
14
+ module Publish
15
+ module_function
16
+
17
+ def publish_to_root(project, root, signer: "local", signer_fingerprint: nil)
18
+ root = File.expand_path(root)
19
+ index = load_registry_index(root)
20
+ package_names = index.packages.to_set
21
+
22
+ project.packages.each do |package|
23
+ archive_bytes = Archive.build_package_archive_bytes(package)
24
+ artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
25
+ artifact_output_path = File.join(root, artifact_path)
26
+ write_output_bytes(artifact_output_path, archive_bytes)
27
+
28
+ metadata_path = registry_metadata_path(root, package.declaration.name)
29
+ metadata = load_registry_package_metadata(metadata_path, package.declaration.name)
30
+ version_entry = published_registry_package_version(
31
+ package,
32
+ signer,
33
+ signer_fingerprint,
34
+ archive_bytes,
35
+ artifact_path
36
+ )
37
+ metadata.versions.reject! { |entry| entry.version == version_entry.version }
38
+ metadata.versions << version_entry
39
+ write_registry_package_metadata(metadata_path, metadata)
40
+ package_names << package.declaration.name
41
+ end
42
+
43
+ index.packages = package_names.sort
44
+ write_registry_index(root, index)
45
+ end
46
+
47
+ def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil)
48
+ project.packages.each do |package|
49
+ archive_bytes = Archive.build_package_archive_bytes(package)
50
+ artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
51
+ Registry.http_put(join_url(server_url, artifact_path), "application/octet-stream", archive_bytes)
52
+
53
+ metadata = RegistryPackageMetadata.new(
54
+ name: package.declaration.name,
55
+ versions: [
56
+ published_registry_package_version(
57
+ package,
58
+ signer,
59
+ signer_fingerprint,
60
+ archive_bytes,
61
+ artifact_path
62
+ )
63
+ ]
64
+ )
65
+ Registry.http_put(
66
+ join_url(server_url, "v1/packages/#{package.declaration.name}.json"),
67
+ "application/json",
68
+ JSON.pretty_generate(metadata_to_hash(metadata))
69
+ )
70
+ end
71
+ end
72
+
73
+ def published_registry_package_version(package, signer, signer_fingerprint, archive_bytes, artifact_path)
74
+ RegistryPackageVersion.new(
75
+ version: package.spec.version,
76
+ artifact: artifact_path,
77
+ artifact_hash: Hashing.sha256_prefixed(archive_bytes),
78
+ tree_hash: package.tree_hash,
79
+ yanked: false,
80
+ targets: package.spec.targets,
81
+ exports: package.spec.exports.keys,
82
+ signer: signer,
83
+ signer_fingerprint: signer_fingerprint,
84
+ published_at: Time.now.utc.iso8601,
85
+ signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer)
86
+ )
87
+ end
88
+
89
+ def load_registry_index(root)
90
+ path = File.join(root, "v1", "index.json")
91
+ return RegistryIndex.new unless File.exist?(path)
92
+
93
+ data = JSON.parse(File.read(path))
94
+ RegistryIndex.new(spec: data["spec"], packages: data["packages"] || [])
95
+ end
96
+
97
+ def write_registry_index(root, index)
98
+ path = File.join(root, "v1", "index.json")
99
+ FileUtils.mkdir_p(File.dirname(path))
100
+ File.write(path, JSON.pretty_generate({"spec" => index.spec, "packages" => index.packages}))
101
+ end
102
+
103
+ def load_registry_package_metadata(path, package_name)
104
+ if File.exist?(path)
105
+ data = JSON.parse(File.read(path))
106
+ RegistryPackageMetadata.new(
107
+ name: data["name"],
108
+ versions: Array(data["versions"]).map { |entry| Registry.version_from_hash(entry) }
109
+ )
110
+ else
111
+ RegistryPackageMetadata.new(name: package_name, versions: [])
112
+ end
113
+ end
114
+
115
+ def write_registry_package_metadata(path, metadata)
116
+ FileUtils.mkdir_p(File.dirname(path))
117
+ File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)))
118
+ end
119
+
120
+ def metadata_to_hash(metadata)
121
+ {
122
+ "name" => metadata.name,
123
+ "versions" => metadata.versions.map { |entry| version_to_hash(entry) }
124
+ }
125
+ end
126
+
127
+ def version_to_hash(entry)
128
+ hash = {
129
+ "version" => entry.version,
130
+ "artifact" => entry.artifact,
131
+ "yanked" => entry.yanked,
132
+ "targets" => entry.targets,
133
+ "exports" => entry.exports
134
+ }
135
+ hash["artifact_hash"] = entry.artifact_hash if entry.artifact_hash
136
+ hash["tree_hash"] = entry.tree_hash if entry.tree_hash
137
+ hash["signer"] = entry.signer if entry.signer
138
+ hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
139
+ hash["published_at"] = entry.published_at if entry.published_at
140
+ hash["signature"] = entry.signature if entry.signature
141
+ hash
142
+ end
143
+
144
+ def registry_metadata_path(root, package_name)
145
+ File.join(root, "v1", "packages", "#{package_name}.json")
146
+ end
147
+
148
+ def registry_artifact_path(package_name, version)
149
+ artifact_name = "#{package_name.tr("/", "-")}-#{version}.praypkg"
150
+ "v1/artifacts/#{package_name}/#{version}/#{artifact_name}"
151
+ end
152
+
153
+ def write_output_bytes(path, bytes)
154
+ FileUtils.mkdir_p(File.dirname(path))
155
+ File.binwrite(path, bytes)
156
+ end
157
+
158
+ def join_url(base, path)
159
+ Registry.join_url(base, path)
160
+ end
161
+ end
162
+ end