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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.md +21 -0
- data/README.md +125 -0
- data/SECURITY.md +28 -0
- data/bin/polyrun +108 -0
- data/bin/pray +26 -0
- data/lib/pray/archive.rb +68 -0
- data/lib/pray/auth_client.rb +97 -0
- data/lib/pray/cli/commands/auth.rb +42 -0
- data/lib/pray/cli/commands/distribution.rb +17 -0
- data/lib/pray/cli/commands/init.rb +62 -0
- data/lib/pray/cli/commands/meta.rb +15 -0
- data/lib/pray/cli/commands/packages.rb +107 -0
- data/lib/pray/cli/commands/trust.rb +73 -0
- data/lib/pray/cli/commands/workflow.rb +126 -0
- data/lib/pray/cli/help.rb +168 -0
- data/lib/pray/cli/helpers.rb +166 -0
- data/lib/pray/cli/parse.rb +136 -0
- data/lib/pray/cli/parse_auth.rb +91 -0
- data/lib/pray/cli/parse_trust.rb +152 -0
- data/lib/pray/cli/suggest.rb +57 -0
- data/lib/pray/cli.rb +117 -0
- data/lib/pray/confess.rb +113 -0
- data/lib/pray/config.rb +52 -0
- data/lib/pray/constraint.rb +80 -0
- data/lib/pray/destination.rb +158 -0
- data/lib/pray/dotenv.rb +46 -0
- data/lib/pray/environment.rb +41 -0
- data/lib/pray/error.rb +55 -0
- data/lib/pray/format_manifest.rb +255 -0
- data/lib/pray/format_serialize.rb +135 -0
- data/lib/pray/git_sources.rb +228 -0
- data/lib/pray/hashing.rb +59 -0
- data/lib/pray/invocation.rb +110 -0
- data/lib/pray/literal.rb +382 -0
- data/lib/pray/lockfile.rb +259 -0
- data/lib/pray/lockfile_serialize.rb +125 -0
- data/lib/pray/manifest.rb +126 -0
- data/lib/pray/manifest_formatter.rb +56 -0
- data/lib/pray/manifest_json.rb +128 -0
- data/lib/pray/manifest_parser.rb +152 -0
- data/lib/pray/manifest_parser_blocks.rb +216 -0
- data/lib/pray/manifest_parser_helpers.rb +208 -0
- data/lib/pray/materialize.rb +120 -0
- data/lib/pray/package_spec.rb +245 -0
- data/lib/pray/path_safety.rb +41 -0
- data/lib/pray/plan.rb +83 -0
- data/lib/pray/project_context.rb +67 -0
- data/lib/pray/publish.rb +162 -0
- data/lib/pray/registry.rb +355 -0
- data/lib/pray/render.rb +360 -0
- data/lib/pray/resolve.rb +396 -0
- data/lib/pray/resolve_context.rb +19 -0
- data/lib/pray/serve.rb +130 -0
- data/lib/pray/serve_federation.rb +109 -0
- data/lib/pray/session.rb +90 -0
- data/lib/pray/ssh_agent.rb +115 -0
- data/lib/pray/statement_surface.rb +262 -0
- data/lib/pray/substitute.rb +50 -0
- data/lib/pray/sync.rb +219 -0
- data/lib/pray/terminal.rb +30 -0
- data/lib/pray/trust.rb +201 -0
- data/lib/pray/trust_feed.rb +110 -0
- data/lib/pray/trust_ops.rb +202 -0
- data/lib/pray/verify.rb +257 -0
- data/lib/pray/version.rb +6 -0
- data/lib/pray-cli.rb +4 -0
- data/lib/pray.rb +46 -0
- data/pray-cli.gemspec +48 -0
- metadata +187 -0
data/lib/pray/render.rb
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Pray
|
|
7
|
+
RenderedTarget = Struct.new(:path, :content, :managed_spans) do
|
|
8
|
+
def initialize(path:, content:, managed_spans: [])
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Render
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def render_project(project)
|
|
17
|
+
rendered = []
|
|
18
|
+
project.manifest.targets.each do |target|
|
|
19
|
+
output = target.outputs.first
|
|
20
|
+
next unless output
|
|
21
|
+
|
|
22
|
+
rendered << render_target(project, target, output)
|
|
23
|
+
end
|
|
24
|
+
rendered
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write_rendered_targets(project, rendered)
|
|
28
|
+
rendered.each do |target|
|
|
29
|
+
path = File.join(project.project_root, target.path)
|
|
30
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
31
|
+
File.write(path, target.content)
|
|
32
|
+
end
|
|
33
|
+
materialize_provisioned_exports(project)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def materialize_provisioned_exports(project)
|
|
37
|
+
symbols = project.manifest.symbols || {}
|
|
38
|
+
planned_provisioned_files(project).each do |file|
|
|
39
|
+
destination = File.join(project.project_root, file.path)
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
41
|
+
write_provisioned_file(file.source, destination, symbols)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_provisioned_file(source, destination, symbols)
|
|
46
|
+
bytes = File.binread(source)
|
|
47
|
+
text = bytes.force_encoding(Encoding::UTF_8)
|
|
48
|
+
if text.valid_encoding?
|
|
49
|
+
File.write(destination, Substitute.substitute_pray_symbols(text, symbols))
|
|
50
|
+
else
|
|
51
|
+
File.binwrite(destination, bytes)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def planned_provisioned_files(project)
|
|
56
|
+
planned = []
|
|
57
|
+
collect_exact_file_bindings(project, planned)
|
|
58
|
+
project.manifest.targets.each do |target|
|
|
59
|
+
target.skills.each do |folder_root|
|
|
60
|
+
destination_root = File.join(project.project_root, folder_root)
|
|
61
|
+
project.packages.each do |package|
|
|
62
|
+
next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
|
|
63
|
+
next unless Destination.package_bound_to_tree?(package.declaration, target)
|
|
64
|
+
|
|
65
|
+
collect_legacy_skill_files(project, package, destination_root, planned)
|
|
66
|
+
collect_selected_export_files(project, package, destination_root, planned)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
planned.sort_by(&:path).uniq(&:path)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
PlannedProvisionedFile = Struct.new(:path, :source)
|
|
74
|
+
|
|
75
|
+
def render_target(project, target, output)
|
|
76
|
+
if target.scoped && target.mode == "compose"
|
|
77
|
+
return render_scoped_compose(project, target, output)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
render_legacy_compose(project, target, output)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def render_scoped_compose(project, target, output)
|
|
84
|
+
builder = ContentBuilder.new
|
|
85
|
+
append_header(builder, project, output)
|
|
86
|
+
symbols = project.manifest.symbols || {}
|
|
87
|
+
managed_spans = []
|
|
88
|
+
|
|
89
|
+
(target.entries || []).each do |entry|
|
|
90
|
+
if entry.kind == "local"
|
|
91
|
+
append_scoped_local(builder, project, entry.path, symbols)
|
|
92
|
+
else
|
|
93
|
+
append_scoped_package(builder, managed_spans, project, target, output, entry.name, symbols)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
RenderedTarget.new(path: output, content: builder.finish, managed_spans: managed_spans)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_legacy_compose(project, target, output)
|
|
101
|
+
builder = ContentBuilder.new
|
|
102
|
+
append_header(builder, project, output)
|
|
103
|
+
|
|
104
|
+
unbound_locals = project.local_files.select do |local|
|
|
105
|
+
declaration = project.manifest.local.find { |entry| entry.path == local.manifest_path }
|
|
106
|
+
declaration.nil? || !declaration.bound
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
unless unbound_locals.empty?
|
|
110
|
+
builder.append_line("## Additional instructions")
|
|
111
|
+
builder.append_empty_line
|
|
112
|
+
end
|
|
113
|
+
symbols = project.manifest.symbols || {}
|
|
114
|
+
unbound_locals.each do |local|
|
|
115
|
+
next if local.content.empty? && local.optional
|
|
116
|
+
|
|
117
|
+
builder.append_line("### #{local.manifest_path}")
|
|
118
|
+
builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
|
|
119
|
+
builder.append_empty_line
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
builder.append_line("## Shared instructions")
|
|
123
|
+
builder.append_empty_line
|
|
124
|
+
|
|
125
|
+
managed_spans = []
|
|
126
|
+
project.packages.each do |package|
|
|
127
|
+
next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
|
|
128
|
+
next unless Destination.package_bound_to_compose?(package.declaration, target)
|
|
129
|
+
|
|
130
|
+
package.selected_exports.each do |export|
|
|
131
|
+
next unless should_inline_export?(package, export)
|
|
132
|
+
|
|
133
|
+
append_managed_export(builder, managed_spans, package, export, target, output, symbols)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
RenderedTarget.new(path: output, content: builder.finish, managed_spans: managed_spans)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def append_header(builder, project, output)
|
|
141
|
+
return unless project.manifest.render.header
|
|
142
|
+
|
|
143
|
+
output_name = File.basename(output)
|
|
144
|
+
builder.append_line("<!-- pray:0 ignore-comments -->")
|
|
145
|
+
builder.append_empty_line
|
|
146
|
+
builder.append_line("# Agent context")
|
|
147
|
+
builder.append_empty_line
|
|
148
|
+
builder.append_line("Do not edit managed blocks in `#{output_name}` or provisioned files under `.agents/`.")
|
|
149
|
+
builder.append_line("To change shared guidance, update `Prayfile` and run `pray install`.")
|
|
150
|
+
builder.append_empty_line
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def append_scoped_local(builder, project, path, symbols)
|
|
154
|
+
local = project.local_files.find { |candidate| candidate.manifest_path == path }
|
|
155
|
+
return unless local
|
|
156
|
+
return if local.content.empty? && local.optional
|
|
157
|
+
|
|
158
|
+
builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
|
|
159
|
+
builder.append_empty_line
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def append_scoped_package(builder, managed_spans, project, target, output, package_name, symbols)
|
|
163
|
+
package = project.packages.find { |candidate| candidate.declaration.name == package_name }
|
|
164
|
+
return unless package
|
|
165
|
+
return unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
|
|
166
|
+
|
|
167
|
+
package.selected_exports.each do |export|
|
|
168
|
+
next unless should_inline_export?(package, export)
|
|
169
|
+
|
|
170
|
+
append_managed_export(builder, managed_spans, package, export, target, output, symbols)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
|
|
175
|
+
body = package.export_bodies[export]
|
|
176
|
+
raise Error.render("package #{package.declaration.name} is missing cached export #{export}") unless body
|
|
177
|
+
|
|
178
|
+
body = Substitute.substitute_pray_symbols(body, symbols)
|
|
179
|
+
identifier = Hashing.marker_id("#{package.declaration.name}:#{export}:#{target.name}")
|
|
180
|
+
open_line = builder.next_line_number
|
|
181
|
+
builder.append_line("<!-- pray:#{identifier} -->")
|
|
182
|
+
builder.append_body(body)
|
|
183
|
+
close_line = builder.next_line_number
|
|
184
|
+
builder.append_line("<!-- pray:#{identifier} -->")
|
|
185
|
+
managed_spans << ManagedSpanRecord.new(
|
|
186
|
+
id: identifier,
|
|
187
|
+
target: output,
|
|
188
|
+
open_line: open_line,
|
|
189
|
+
close_line: close_line,
|
|
190
|
+
ideal_checksum: Hashing.checksum_managed_span_content(body),
|
|
191
|
+
package: package.declaration.name,
|
|
192
|
+
export: export,
|
|
193
|
+
source_checksum: package.source_checksum,
|
|
194
|
+
silenced: false
|
|
195
|
+
)
|
|
196
|
+
builder.append_empty_line
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def should_inline_export?(package, export_name)
|
|
200
|
+
export = package.spec.exports[export_name]
|
|
201
|
+
export.nil? || export.kind == "fragment"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def collect_exact_file_bindings(project, planned)
|
|
205
|
+
project.packages.each do |package|
|
|
206
|
+
destination = package.declaration.file
|
|
207
|
+
next unless destination
|
|
208
|
+
next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
|
|
209
|
+
|
|
210
|
+
matched = false
|
|
211
|
+
package.selected_exports.each do |export_name|
|
|
212
|
+
export = package.spec.exports[export_name]
|
|
213
|
+
next unless export&.kind == "file"
|
|
214
|
+
|
|
215
|
+
source = File.join(package.root, export.path)
|
|
216
|
+
raise Error.render("file export source missing: #{source}") unless File.file?(source)
|
|
217
|
+
|
|
218
|
+
planned << PlannedProvisionedFile.new(path: destination, source: source)
|
|
219
|
+
matched = true
|
|
220
|
+
break
|
|
221
|
+
end
|
|
222
|
+
unless matched
|
|
223
|
+
raise Error.render(
|
|
224
|
+
"package #{package.declaration.name} has file: \"#{destination}\" but no selected file export"
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def collect_legacy_skill_files(project, package, destination_root, planned)
|
|
231
|
+
package.spec.skills.each do |skill_name, skill|
|
|
232
|
+
next if legacy_skill_covered_by_export?(package, skill)
|
|
233
|
+
|
|
234
|
+
skill_files = package.skill_files[skill_name]
|
|
235
|
+
raise Error.render("package #{package.declaration.name} has no indexed files for legacy skill #{skill_name}") unless skill_files
|
|
236
|
+
|
|
237
|
+
collect_tree_files(
|
|
238
|
+
project,
|
|
239
|
+
File.join(package.root, skill.path),
|
|
240
|
+
File.join(destination_root, skill_name),
|
|
241
|
+
skill_files,
|
|
242
|
+
[],
|
|
243
|
+
[],
|
|
244
|
+
planned
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def legacy_skill_covered_by_export?(package, skill)
|
|
250
|
+
package.spec.exports.any? do |export_name, export|
|
|
251
|
+
package.selected_exports.include?(export_name) &&
|
|
252
|
+
%w[folder skill].include?(export.kind) &&
|
|
253
|
+
export.path.delete_suffix("/") == skill.path.delete_suffix("/")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def collect_selected_export_files(project, package, destination_root, planned)
|
|
258
|
+
package.selected_exports.each do |export_name|
|
|
259
|
+
export = package.spec.exports[export_name]
|
|
260
|
+
next unless export
|
|
261
|
+
|
|
262
|
+
case export.kind
|
|
263
|
+
when "folder", "skill"
|
|
264
|
+
indexed_files = package.skill_files[export_name]
|
|
265
|
+
unless indexed_files
|
|
266
|
+
raise Error.render("package #{package.declaration.name} has no indexed files for folder export #{export_name}")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
destination_name = folder_destination_name(export_name, export.path)
|
|
270
|
+
collect_tree_files(
|
|
271
|
+
project,
|
|
272
|
+
File.join(package.root, export.path),
|
|
273
|
+
File.join(destination_root, destination_name),
|
|
274
|
+
indexed_files,
|
|
275
|
+
export.only || [],
|
|
276
|
+
export.except || [],
|
|
277
|
+
planned
|
|
278
|
+
)
|
|
279
|
+
when "file"
|
|
280
|
+
next if package.declaration.file
|
|
281
|
+
|
|
282
|
+
source = File.join(package.root, export.path)
|
|
283
|
+
raise Error.render("file export source missing: #{source}") unless File.file?(source)
|
|
284
|
+
|
|
285
|
+
destination = File.join(destination_root, export_name, File.basename(source))
|
|
286
|
+
planned << PlannedProvisionedFile.new(
|
|
287
|
+
path: relative_project_path(project, destination),
|
|
288
|
+
source: source
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def folder_destination_name(export_name, export_path)
|
|
295
|
+
File.basename(export_path.delete_suffix("/")).empty? ? export_name : File.basename(export_path.delete_suffix("/"))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def collect_tree_files(project, source_root, destination_root, relative_files, only, except, planned)
|
|
299
|
+
raise Error.render("folder source directory missing: #{source_root}") unless File.directory?(source_root)
|
|
300
|
+
raise Error.render("no files listed in package manifest for #{source_root}") if relative_files.empty?
|
|
301
|
+
|
|
302
|
+
matched = false
|
|
303
|
+
relative_files.each do |relative|
|
|
304
|
+
next if !only.empty? && !only.include?(relative)
|
|
305
|
+
next if except.include?(relative)
|
|
306
|
+
|
|
307
|
+
source = File.join(source_root, relative)
|
|
308
|
+
raise Error.render("provisioned file missing: #{source}") unless File.file?(source)
|
|
309
|
+
|
|
310
|
+
destination = File.join(destination_root, relative)
|
|
311
|
+
planned << PlannedProvisionedFile.new(
|
|
312
|
+
path: relative_project_path(project, destination),
|
|
313
|
+
source: source
|
|
314
|
+
)
|
|
315
|
+
matched = true
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
if !matched && only.empty? && except.empty?
|
|
319
|
+
raise Error.render("no files listed in package manifest for #{source_root}")
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def relative_project_path(project, absolute)
|
|
324
|
+
Pathname(absolute).relative_path_from(Pathname(project.project_root)).to_s
|
|
325
|
+
rescue ArgumentError
|
|
326
|
+
absolute
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
class ContentBuilder
|
|
330
|
+
def initialize
|
|
331
|
+
@content = +""
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def next_line_number
|
|
335
|
+
@content.count("\n") + 1
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def append_line(line)
|
|
339
|
+
@content << line << "\n"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def append_empty_line
|
|
343
|
+
@content << "\n"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def append_body(body)
|
|
347
|
+
trimmed = body.sub(/\n+\z/, "")
|
|
348
|
+
return if trimmed.empty?
|
|
349
|
+
|
|
350
|
+
trimmed.each_line(chomp: true) { |line| append_line(line) }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def finish
|
|
354
|
+
@content.sub(/\n\n+\z/, "\n")
|
|
355
|
+
@content << "\n" unless @content.end_with?("\n")
|
|
356
|
+
@content
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|