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,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Literal
5
+ LiteralValue = Struct.new(:kind, :value) do
6
+ def string? = kind == :string || kind == :symbol
7
+ def as_string = string? ? value : nil
8
+ def as_bool = (kind == :bool) ? value : nil
9
+ def as_integer = (kind == :integer) ? value : nil
10
+ def as_array = (kind == :array) ? value : nil
11
+ def as_map = (kind == :map) ? value : nil
12
+ end
13
+
14
+ module_function
15
+
16
+ def split_top_level(input, separator)
17
+ output = []
18
+ start = 0
19
+ depth = 0
20
+ quote = nil
21
+ escaped = false
22
+
23
+ input.each_char.with_index do |character, index|
24
+ if quote
25
+ if escaped
26
+ escaped = false
27
+ elsif character == "\\"
28
+ escaped = true
29
+ elsif character == quote
30
+ quote = nil
31
+ end
32
+ next
33
+ end
34
+
35
+ case character
36
+ when '"', "'"
37
+ quote = character
38
+ when "[", "{", "("
39
+ depth += 1
40
+ when "]", "}", ")"
41
+ depth -= 1
42
+ else
43
+ if character == separator && depth.zero?
44
+ segment = input[start...index].strip
45
+ output << segment unless segment.empty?
46
+ start = index + 1
47
+ end
48
+ end
49
+ end
50
+
51
+ tail = input[start..].strip
52
+ output << tail unless tail.empty?
53
+ output
54
+ end
55
+
56
+ def find_top_level(input, token)
57
+ depth = 0
58
+ quote = nil
59
+ escaped = false
60
+ index = 0
61
+
62
+ while index < input.length
63
+ character = input[index]
64
+ if quote
65
+ if escaped
66
+ escaped = false
67
+ elsif character == "\\"
68
+ escaped = true
69
+ elsif character == quote
70
+ quote = nil
71
+ end
72
+ index += 1
73
+ next
74
+ end
75
+
76
+ case character
77
+ when '"', "'"
78
+ quote = character
79
+ when "[", "{", "("
80
+ depth += 1
81
+ when "]", "}", ")"
82
+ depth -= 1
83
+ else
84
+ return index if depth.zero? && input[index..].start_with?(token)
85
+ end
86
+ index += 1
87
+ end
88
+ nil
89
+ end
90
+
91
+ def is_balanced?(input)
92
+ depth = 0
93
+ quote = nil
94
+ escaped = false
95
+
96
+ input.each_char do |character|
97
+ if quote
98
+ if escaped
99
+ escaped = false
100
+ elsif character == "\\"
101
+ escaped = true
102
+ elsif character == quote
103
+ quote = nil
104
+ end
105
+ next
106
+ end
107
+
108
+ case character
109
+ when '"', "'"
110
+ quote = character
111
+ when "[", "{", "("
112
+ depth += 1
113
+ when "]", "}", ")"
114
+ depth -= 1
115
+ end
116
+ end
117
+
118
+ depth.zero? && quote.nil?
119
+ end
120
+
121
+ def parse_literal(input)
122
+ parser = Parser.new(input)
123
+ value = parser.parse_value
124
+ parser.skip_whitespace
125
+ unless parser.finished?
126
+ raise Error.parse("literal", "unexpected trailing input near #{parser.remaining.inspect}")
127
+ end
128
+ value
129
+ end
130
+
131
+ def parse_literal_map(input)
132
+ value = parse_literal(input)
133
+ raise Error.parse("literal", "expected map literal, found #{value.inspect}") unless value.kind == :map
134
+
135
+ value.value
136
+ end
137
+
138
+ def parse_literal_array(input)
139
+ value = parse_literal(input)
140
+ raise Error.parse("literal", "expected array literal, found #{value.inspect}") unless value.kind == :array
141
+
142
+ value.value
143
+ end
144
+
145
+ def prepare_parser_lines(text)
146
+ text.lines.map { |line| prepare_parser_line(line) }
147
+ end
148
+
149
+ def prepare_parser_line(line)
150
+ strip_line_comment(line).rstrip
151
+ end
152
+
153
+ def strip_line_comment(line)
154
+ quote = nil
155
+ escaped = false
156
+ line.each_char.with_index do |character, index|
157
+ if quote
158
+ if escaped
159
+ escaped = false
160
+ elsif character == "\\"
161
+ escaped = true
162
+ elsif character == quote
163
+ quote = nil
164
+ end
165
+ next
166
+ end
167
+
168
+ case character
169
+ when '"', "'"
170
+ quote = character
171
+ when "#"
172
+ return line[0...index]
173
+ end
174
+ end
175
+ line
176
+ end
177
+
178
+ class Parser
179
+ def initialize(input)
180
+ @input = input
181
+ @cursor = 0
182
+ end
183
+
184
+ def finished? = @cursor >= @input.length
185
+ def remaining = @input[@cursor..]
186
+
187
+ def skip_whitespace
188
+ while (character = peek) && character.match?(/\s/)
189
+ @cursor += 1
190
+ end
191
+ end
192
+
193
+ def peek = remaining[0]
194
+
195
+ def next_character
196
+ character = peek
197
+ @cursor += 1 if character
198
+ character
199
+ end
200
+
201
+ def parse_value
202
+ skip_whitespace
203
+ case peek
204
+ when '"', "'"
205
+ parse_string
206
+ when ":"
207
+ parse_symbol
208
+ when "["
209
+ parse_array
210
+ when "{"
211
+ parse_map
212
+ when "-", "0".."9"
213
+ parse_integer_or_identifier
214
+ when nil
215
+ raise Error.parse("literal", "unexpected end of input")
216
+ else
217
+ parse_identifier
218
+ end
219
+ end
220
+
221
+ def parse_string
222
+ quote = next_character
223
+ output = +""
224
+ escaped = false
225
+ while (character = next_character)
226
+ if escaped
227
+ output << case character
228
+ when "n" then "\n"
229
+ when "r" then "\r"
230
+ when "t" then "\t"
231
+ when "\\" then "\\"
232
+ when "\"", "'" then character
233
+ else character
234
+ end
235
+ escaped = false
236
+ next
237
+ end
238
+ if character == "\\"
239
+ escaped = true
240
+ next
241
+ end
242
+ if character == quote
243
+ return LiteralValue.new(kind: :string, value: output)
244
+ end
245
+ output << character
246
+ end
247
+ raise Error.parse("literal", "unterminated string literal")
248
+ end
249
+
250
+ def parse_symbol
251
+ next_character
252
+ output = +""
253
+ while (character = peek) && character.match?(%r{[[:alnum:]_\-.\\/]})
254
+ output << character
255
+ next_character
256
+ end
257
+ raise Error.parse("literal", "empty symbol") if output.empty?
258
+
259
+ LiteralValue.new(kind: :symbol, value: output)
260
+ end
261
+
262
+ def parse_array
263
+ next_character
264
+ values = []
265
+ loop do
266
+ skip_whitespace
267
+ break if peek == "]" && next_character
268
+
269
+ values << parse_value
270
+ skip_whitespace
271
+ case peek
272
+ when ","
273
+ next_character
274
+ when "]"
275
+ next_character
276
+ break
277
+ else
278
+ raise Error.parse("literal", "expected ',' or ']'")
279
+ end
280
+ end
281
+ LiteralValue.new(kind: :array, value: values)
282
+ end
283
+
284
+ def parse_map
285
+ next_character
286
+ entries = {}
287
+ loop do
288
+ skip_whitespace
289
+ break if peek == "}" && next_character
290
+
291
+ key = parse_map_key
292
+ skip_whitespace
293
+ if remaining.start_with?("=>")
294
+ @cursor += 2
295
+ elsif peek == ":"
296
+ next_character
297
+ else
298
+ raise Error.parse("literal", "expected ':' or '=>' after map key")
299
+ end
300
+ entries[key] = parse_value
301
+ skip_whitespace
302
+ case peek
303
+ when ","
304
+ next_character
305
+ when "}"
306
+ next_character
307
+ break
308
+ else
309
+ raise Error.parse("literal", "expected ',' or '}'")
310
+ end
311
+ end
312
+ LiteralValue.new(kind: :map, value: entries)
313
+ end
314
+
315
+ def parse_map_key
316
+ skip_whitespace
317
+ case peek
318
+ when '"', "'"
319
+ value = parse_string
320
+ value.value
321
+ when ":"
322
+ parse_symbol.value
323
+ when "a".."z", "A".."Z", "_"
324
+ parse_identifier_name
325
+ else
326
+ raise Error.parse("literal", "invalid map key")
327
+ end
328
+ end
329
+
330
+ def parse_integer_or_identifier
331
+ start = @cursor
332
+ next_character if peek == "-"
333
+ while peek&.match?(/[0-9_]/)
334
+ next_character
335
+ end
336
+ if peek == "."
337
+ return parse_identifier_from(start)
338
+ end
339
+ text = @input[start...@cursor].delete("_")
340
+ LiteralValue.new(kind: :integer, value: Integer(text))
341
+ rescue ArgumentError => error
342
+ raise Error.parse("literal", error.message)
343
+ end
344
+
345
+ def parse_identifier
346
+ identifier = parse_identifier_name
347
+ case identifier
348
+ when "true" then LiteralValue.new(kind: :bool, value: true)
349
+ when "false" then LiteralValue.new(kind: :bool, value: false)
350
+ when "nil" then LiteralValue.new(kind: :null, value: nil)
351
+ else LiteralValue.new(kind: :string, value: identifier)
352
+ end
353
+ end
354
+
355
+ def parse_identifier_name
356
+ raise Error.parse("literal", "expected identifier") unless identifier_start?(peek)
357
+
358
+ start = @cursor
359
+ next_character
360
+ while peek && identifier_continue?(peek)
361
+ next_character
362
+ end
363
+ @input[start...@cursor]
364
+ end
365
+
366
+ def parse_identifier_from(start)
367
+ while peek && (identifier_continue?(peek) || peek == ".")
368
+ next_character
369
+ end
370
+ LiteralValue.new(kind: :string, value: @input[start...@cursor])
371
+ end
372
+
373
+ def identifier_start?(character)
374
+ character && (character.match?(/[[:alpha:]]/) || character == "_")
375
+ end
376
+
377
+ def identifier_continue?(character)
378
+ character.match?(%r{[[:alnum:]_\-.\\/]})
379
+ end
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+
5
+ require_relative "lockfile_serialize"
6
+
7
+ module Pray
8
+ LockSource = Struct.new(:name, :kind, :url, :revision, :host_key_fingerprint)
9
+ LockedPackage = Struct.new(
10
+ :name, :version, :source, :path, :tree_hash, :artifact_hash, :artifact,
11
+ :exports, :dependencies, :signer_fingerprint
12
+ ) do
13
+ def initialize(exports: [], dependencies: [], signer_fingerprint: nil, source: nil, **kwargs)
14
+ super(**kwargs, exports: exports, dependencies: dependencies, signer_fingerprint: signer_fingerprint, source: source)
15
+ end
16
+ end
17
+
18
+ LockedTarget = Struct.new(:name, :outputs)
19
+ ManagedSpanRecord = Struct.new(
20
+ :id, :target, :open_line, :close_line, :ideal_checksum, :package, :export,
21
+ :source_checksum, :silenced
22
+ )
23
+
24
+ Lockfile = Struct.new(
25
+ :prayfile_lock, :spec, :generated_by, :manifest_hash, :environment, :source, :package,
26
+ :target, :managed_span
27
+ ) do
28
+ def initialize(
29
+ prayfile_lock: "1", spec: "0.1", generated_by: Pray::GENERATED_BY,
30
+ manifest_hash: "", environment: nil, source: [], package: [], target: [], managed_span: []
31
+ )
32
+ super
33
+ end
34
+
35
+ def canonicalized
36
+ dup.tap do |copy|
37
+ copy.source = source.sort_by(&:name)
38
+ copy.package = package.sort_by { |entry| [entry.name, entry.source.to_s, entry.version] }
39
+ copy.target = target.sort_by(&:name)
40
+ copy.managed_span = managed_span.sort_by { |span| [span.target, span.open_line, span.id] }
41
+ end
42
+ end
43
+
44
+ def serialized
45
+ LockfileIO.lockfile_to_toml(canonicalized)
46
+ end
47
+
48
+ def file_hash
49
+ Hashing.sha256_prefixed(serialized)
50
+ end
51
+
52
+ def equivalent_to?(other)
53
+ canonicalized == other.canonicalized
54
+ end
55
+ end
56
+
57
+ module LockfileIO
58
+ module_function
59
+
60
+ def lockfile_to_toml(lockfile)
61
+ LockfileSerialize.lockfile_to_toml(lockfile)
62
+ end
63
+
64
+ def lockfile_to_hash(lockfile)
65
+ {
66
+ "prayfile_lock" => lockfile.prayfile_lock,
67
+ "spec" => lockfile.spec,
68
+ "generated_by" => lockfile.generated_by,
69
+ "manifest_hash" => lockfile.manifest_hash,
70
+ "source" => lockfile.source.map { |entry| source_to_hash(entry) },
71
+ "package" => lockfile.package.map { |entry| package_to_hash(entry) },
72
+ "target" => lockfile.target.map { |entry| {"name" => entry.name, "outputs" => entry.outputs} },
73
+ "managed_span" => lockfile.managed_span.map { |entry| managed_span_to_hash(entry) }
74
+ }
75
+ end
76
+
77
+ def source_to_hash(entry)
78
+ hash = {"name" => entry.name, "kind" => entry.kind, "url" => entry.url}
79
+ hash["revision"] = entry.revision if entry.revision
80
+ hash["host_key_fingerprint"] = entry.host_key_fingerprint if entry.host_key_fingerprint
81
+ hash
82
+ end
83
+
84
+ def package_to_hash(entry)
85
+ hash = {
86
+ "name" => entry.name,
87
+ "version" => entry.version,
88
+ "path" => entry.path,
89
+ "tree_hash" => entry.tree_hash,
90
+ "artifact_hash" => entry.artifact_hash,
91
+ "artifact" => entry.artifact,
92
+ "exports" => entry.exports,
93
+ "dependencies" => entry.dependencies
94
+ }
95
+ hash["source"] = entry.source unless entry.source.nil?
96
+ hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
97
+ hash
98
+ end
99
+
100
+ def managed_span_to_hash(entry)
101
+ {
102
+ "id" => entry.id,
103
+ "target" => entry.target,
104
+ "open_line" => entry.open_line,
105
+ "close_line" => entry.close_line,
106
+ "ideal_checksum" => entry.ideal_checksum,
107
+ "package" => entry.package,
108
+ "export" => entry.export,
109
+ "source_checksum" => entry.source_checksum,
110
+ "silenced" => entry.silenced
111
+ }
112
+ end
113
+
114
+ def parse_lockfile(text)
115
+ data = TomlRB.parse(text)
116
+ from_hash(data)
117
+ rescue TomlRB::ParseError => error
118
+ raise Error.parse("lockfile", error.message)
119
+ end
120
+
121
+ def read_lockfile(path)
122
+ parse_lockfile(File.read(path))
123
+ end
124
+
125
+ def serialize_lockfile(lockfile)
126
+ lockfile_to_toml(lockfile.canonicalized)
127
+ end
128
+
129
+ def lockfile_hash(lockfile)
130
+ Hashing.sha256_prefixed(serialize_lockfile(lockfile))
131
+ end
132
+
133
+ def write_lockfile(path, lockfile)
134
+ File.write(path, serialize_lockfile(lockfile))
135
+ end
136
+
137
+ def write_lockfile_if_changed(path, lockfile)
138
+ serialized = serialize_lockfile(lockfile)
139
+ if File.exist?(path) && File.binread(path) == serialized
140
+ return
141
+ end
142
+
143
+ File.write(path, serialized)
144
+ end
145
+
146
+ def lockfiles_equivalent?(left, right)
147
+ left.equivalent_to?(right)
148
+ end
149
+
150
+ def build_lockfile(manifest_hash, environment, project_root, manifest_sources, manifest_targets, rendered, packages, source_revisions, source_host_keys)
151
+ Lockfile.new(
152
+ manifest_hash: manifest_hash,
153
+ environment: environment,
154
+ source: manifest_sources.map do |source|
155
+ LockSource.new(
156
+ name: source.name,
157
+ kind: source.kind,
158
+ url: source.url,
159
+ revision: source_revisions[source.name],
160
+ host_key_fingerprint: source_host_keys[source.name]
161
+ )
162
+ end,
163
+ package: packages.map do |package|
164
+ LockedPackage.new(
165
+ name: package.declaration.name,
166
+ version: package.spec.version,
167
+ source: package.declaration.source,
168
+ path: relative_lockfile_path(project_root, package.root),
169
+ tree_hash: package.tree_hash,
170
+ artifact_hash: package.artifact_hash,
171
+ artifact: normalize_lockfile_artifact(project_root, package.artifact, package.root),
172
+ exports: package.selected_exports,
173
+ dependencies: package.spec.dependencies.map(&:name),
174
+ signer_fingerprint: package.signer_fingerprint
175
+ )
176
+ end,
177
+ target: manifest_targets.map { |target| LockedTarget.new(name: target.name, outputs: target.outputs) },
178
+ managed_span: rendered.flat_map(&:managed_spans)
179
+ ).canonicalized
180
+ end
181
+
182
+ def from_hash(data)
183
+ Lockfile.new(
184
+ prayfile_lock: data["prayfile_lock"],
185
+ spec: data["spec"],
186
+ generated_by: data["generated_by"],
187
+ manifest_hash: data["manifest_hash"],
188
+ environment: data["environment"],
189
+ source: Array(data["source"]).map do |entry|
190
+ LockSource.new(
191
+ name: entry["name"],
192
+ kind: entry["kind"],
193
+ url: entry["url"],
194
+ revision: entry["revision"],
195
+ host_key_fingerprint: entry["host_key_fingerprint"]
196
+ )
197
+ end,
198
+ package: Array(data["package"]).map do |entry|
199
+ LockedPackage.new(
200
+ name: entry["name"],
201
+ version: entry["version"],
202
+ source: entry["source"],
203
+ path: entry["path"],
204
+ tree_hash: entry["tree_hash"],
205
+ artifact_hash: entry["artifact_hash"],
206
+ artifact: entry["artifact"],
207
+ exports: entry["exports"] || [],
208
+ dependencies: entry["dependencies"] || [],
209
+ signer_fingerprint: entry["signer_fingerprint"]
210
+ )
211
+ end,
212
+ target: Array(data["target"]).map { |entry| LockedTarget.new(name: entry["name"], outputs: entry["outputs"] || []) },
213
+ managed_span: Array(data["managed_span"]).map do |entry|
214
+ ManagedSpanRecord.new(
215
+ id: entry["id"],
216
+ target: entry["target"],
217
+ open_line: entry["open_line"],
218
+ close_line: entry["close_line"],
219
+ ideal_checksum: entry["ideal_checksum"],
220
+ package: entry["package"],
221
+ export: entry["export"],
222
+ source_checksum: entry["source_checksum"],
223
+ silenced: entry["silenced"]
224
+ )
225
+ end
226
+ )
227
+ end
228
+
229
+ def relative_lockfile_path(project_root, path)
230
+ absolute = Pathname(path).absolute? ? Pathname(path) : Pathname(project_root).join(path)
231
+ normalized_root = Pathname(project_root).cleanpath
232
+ normalized_absolute = absolute.cleanpath
233
+ relative = normalized_absolute.relative_path_from(normalized_root)
234
+ format_relative_lockfile_path(relative)
235
+ rescue ArgumentError
236
+ format_relative_lockfile_path(Pathname(path).cleanpath)
237
+ end
238
+
239
+ def format_relative_lockfile_path(relative)
240
+ text = relative.to_s.tr("\\", "/")
241
+ (text == "." || text.start_with?("./")) ? text : "./#{text}"
242
+ end
243
+
244
+ def normalize_lockfile_artifact(project_root, artifact, package_root)
245
+ return artifact unless artifact.start_with?("path:")
246
+
247
+ path_text = artifact.delete_prefix("path:")
248
+ path = Pathname(path_text)
249
+ relative = if path.absolute?
250
+ relative_lockfile_path(project_root, path)
251
+ else
252
+ relative_lockfile_path(project_root, package_root)
253
+ end
254
+ "path:#{relative}"
255
+ end
256
+ end
257
+
258
+ extend LockfileIO
259
+ end