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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module CLI
|
|
7
|
+
def add_command(name:, constraint: nil, path: nil)
|
|
8
|
+
manifest_path_value = manifest_path
|
|
9
|
+
manifest_text = Pray.read_manifest_text(manifest_path_value)
|
|
10
|
+
manifest = Pray.parse_manifest(manifest_text)
|
|
11
|
+
if manifest.packages.any? { |package| package.name == name }
|
|
12
|
+
raise Error.manifest("package #{name} already exists")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
declaration = Pray.format_package_declaration(
|
|
16
|
+
ManifestPackage.new(name: name, constraint: constraint || "*", path: path)
|
|
17
|
+
)
|
|
18
|
+
File.write(manifest_path_value, insert_manifest_statement(manifest_text, declaration))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove_command(name)
|
|
22
|
+
raise Error.manifest("remove requires a package name") unless name
|
|
23
|
+
|
|
24
|
+
manifest_path_value = manifest_path
|
|
25
|
+
manifest_text = Pray.read_manifest_text(manifest_path_value)
|
|
26
|
+
manifest = Pray.parse_manifest(manifest_text)
|
|
27
|
+
unless manifest.packages.any? { |package| package.name == name }
|
|
28
|
+
raise Error.manifest("package #{name} not found")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
File.write(manifest_path_value, remove_manifest_statement(manifest_text, name))
|
|
32
|
+
install_command({locked: false, frozen: false, offline: false})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def list_command
|
|
36
|
+
project = resolve_current_project
|
|
37
|
+
lines = ["Package list"]
|
|
38
|
+
project.packages.each do |package|
|
|
39
|
+
lines << "#{package.declaration.name} #{package.spec.version} source=#{package_source_summary(package)} exports=#{format_list(package.selected_exports)}"
|
|
40
|
+
end
|
|
41
|
+
puts lines.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tree_command
|
|
45
|
+
project = resolve_current_project
|
|
46
|
+
package_map = project.packages.to_h { |package| [package.declaration.name, package] }
|
|
47
|
+
lines = ["Dependency tree"]
|
|
48
|
+
project.packages.each do |package|
|
|
49
|
+
render_tree_node(package, package_map, 0, Set.new, lines)
|
|
50
|
+
end
|
|
51
|
+
puts lines.join("\n")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def package_command
|
|
55
|
+
project = resolve_current_project
|
|
56
|
+
project.packages.each do |package|
|
|
57
|
+
output_path = Archive.package_archive_path(package.declaration.name, package.spec.version)
|
|
58
|
+
Archive.write_package_archive(package, output_path)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def explain_command(name)
|
|
63
|
+
raise Error.resolution("explain requires a package name") unless name
|
|
64
|
+
|
|
65
|
+
project = resolve_current_project
|
|
66
|
+
package = project.packages.find { |entry| entry.declaration.name == name }
|
|
67
|
+
raise Error.resolution("package #{name} not found") unless package
|
|
68
|
+
|
|
69
|
+
lockfile = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
70
|
+
lockfile_package = lockfile&.package&.find { |entry| entry.name == name }
|
|
71
|
+
|
|
72
|
+
lines = ["Package explanation"]
|
|
73
|
+
lines << "name: #{package.declaration.name}"
|
|
74
|
+
lines << "constraint: #{package.declaration.constraint}"
|
|
75
|
+
lines << "resolved version: #{package.spec.version}"
|
|
76
|
+
if package.registry_latest_version
|
|
77
|
+
lines << "registry latest: #{package.registry_latest_version}"
|
|
78
|
+
end
|
|
79
|
+
lines << "source: #{package_source_summary(package)}"
|
|
80
|
+
lines << "exports: #{format_list(package.selected_exports)}"
|
|
81
|
+
lines << "dependencies: #{format_list(package.spec.dependencies.map(&:name))}"
|
|
82
|
+
lines << "tree hash: #{package.tree_hash}"
|
|
83
|
+
lines << "artifact hash: #{package.artifact_hash}"
|
|
84
|
+
if lockfile_package
|
|
85
|
+
lines << "lockfile version: #{lockfile_package.version}"
|
|
86
|
+
lines << "lockfile path: #{lockfile_package.path}"
|
|
87
|
+
lines << "lockfile exports: #{format_list(lockfile_package.exports)}"
|
|
88
|
+
else
|
|
89
|
+
lines << "lockfile record: missing"
|
|
90
|
+
end
|
|
91
|
+
puts lines.join("\n")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def outdated_command(_arguments)
|
|
95
|
+
previous_lockfile = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
96
|
+
project = resolve_current_project
|
|
97
|
+
rendered = Render.render_project(project)
|
|
98
|
+
latest_lockfile = build_lockfile(project, rendered)
|
|
99
|
+
puts "Outdated packages"
|
|
100
|
+
if previous_lockfile && Pray.lockfiles_equivalent?(latest_lockfile, previous_lockfile)
|
|
101
|
+
puts "All packages up to date"
|
|
102
|
+
else
|
|
103
|
+
Plan.package_summary_lines(previous_lockfile, latest_lockfile, project).each { |line| puts line }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module CLI
|
|
5
|
+
def trust_list_command(scope: :all, source_url: nil)
|
|
6
|
+
puts Trust.list_policy(scope: scope, source_url: source_url)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def trust_show_command
|
|
10
|
+
puts Trust.show_policy_toml
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def trust_add_key_command(key, match_prefix: nil)
|
|
14
|
+
Trust.add_allowed_signing_key(key, match_prefix: match_prefix)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def trust_remove_key_command(key, match_prefix: nil)
|
|
18
|
+
Trust.remove_allowed_signing_key(key, match_prefix: match_prefix)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def trust_set_signed_command(match_prefix:, enabled:)
|
|
22
|
+
Trust.set_require_signed_commit(match_prefix, enabled)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def trust_set_allow_command(match_prefix:, allow:)
|
|
26
|
+
Trust.set_allow(match_prefix, allow)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def trust_import_repo_command(source_url, match_prefix: nil)
|
|
30
|
+
clone_url = source_url.delete_prefix("git+")
|
|
31
|
+
repository = GitSources.git_source_cache_directory(Dir.pwd, clone_url)
|
|
32
|
+
unless File.directory?(File.join(repository, ".git"))
|
|
33
|
+
raise Error.resolution(
|
|
34
|
+
"no cached git repository for #{clone_url} at #{repository}"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
added = Trust.import_repo(
|
|
39
|
+
clone_url, repository, match_prefix: match_prefix || clone_url
|
|
40
|
+
)
|
|
41
|
+
puts "imported #{added} key(s) from #{repository}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def trust_import_registry_command(source_url, match_prefix: nil, include_host_key: false)
|
|
45
|
+
result = Trust.import_registry(
|
|
46
|
+
source_url, match_prefix: match_prefix, include_host_key: include_host_key
|
|
47
|
+
)
|
|
48
|
+
puts "imported #{result[:publishers_added]} publisher fingerprint(s) and " \
|
|
49
|
+
"#{result[:host_keys_added]} host key(s) for #{match_prefix || source_url}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def trust_check_command(source = nil)
|
|
53
|
+
source_description, hits = Trust.check_compromised(source)
|
|
54
|
+
if hits.empty?
|
|
55
|
+
puts "no compromised trusted signing keys detected (checked against #{source_description})"
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
hits.each do |key, scopes, matches|
|
|
60
|
+
puts "[compromised] #{key}"
|
|
61
|
+
puts " scopes: #{scopes.join(", ")}"
|
|
62
|
+
matches.each do |entry|
|
|
63
|
+
puts " reason: #{entry.reason}" if entry.reason
|
|
64
|
+
puts " reference: #{entry.reference}" if entry.reference
|
|
65
|
+
puts " reported_at: #{entry.reported_at}" if entry.reported_at
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
raise Error.integrity(
|
|
69
|
+
"found #{hits.length} compromised trusted key(s) in #{source_description}"
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module CLI
|
|
7
|
+
def install_command(flags)
|
|
8
|
+
Pray.materialize_project(
|
|
9
|
+
manifest_path: manifest_path,
|
|
10
|
+
frozen: flags[:frozen],
|
|
11
|
+
locked: flags[:locked],
|
|
12
|
+
offline: flags[:offline],
|
|
13
|
+
refresh: flags[:refresh]
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render_command(flags)
|
|
18
|
+
project = resolve_current_project
|
|
19
|
+
rendered = Render.render_project(project)
|
|
20
|
+
if flags[:check]
|
|
21
|
+
ensure_rendered_outputs_current(project, rendered)
|
|
22
|
+
else
|
|
23
|
+
Render.write_rendered_targets(project, rendered)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def verify_command(flags)
|
|
28
|
+
project = resolve_current_project
|
|
29
|
+
lockfile = ensure_existing_lockfile(lockfile_path)
|
|
30
|
+
report = Verify.verify_project(project, lockfile, strict: flags[:strict])
|
|
31
|
+
puts format_verification_report(report) unless report.clean?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def drift_command(flags)
|
|
35
|
+
project = resolve_current_project
|
|
36
|
+
lockfile = ensure_existing_lockfile(lockfile_path)
|
|
37
|
+
report = if flags[:semantic]
|
|
38
|
+
Verify.inspect_project(project, lockfile)
|
|
39
|
+
else
|
|
40
|
+
Verify.drift_project(project, lockfile)
|
|
41
|
+
end
|
|
42
|
+
puts format_drift_report(report) unless report.clean?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_command
|
|
46
|
+
path = manifest_path
|
|
47
|
+
original = Pray.read_manifest_text(path)
|
|
48
|
+
manifest = Pray.parse_manifest(original)
|
|
49
|
+
project = begin
|
|
50
|
+
resolve_current_project(ResolveOptions.new(offline: true))
|
|
51
|
+
rescue Error
|
|
52
|
+
resolve_current_project
|
|
53
|
+
end
|
|
54
|
+
hints = FormatManifest.classify_format_hints(project)
|
|
55
|
+
formatted = FormatManifest.format_recommended(manifest, hints)
|
|
56
|
+
File.write(path, formatted) if formatted != original
|
|
57
|
+
|
|
58
|
+
return unless File.exist?(lockfile_path)
|
|
59
|
+
|
|
60
|
+
lockfile = Pray.read_lockfile(lockfile_path)
|
|
61
|
+
lockfile.target.each do |target|
|
|
62
|
+
target.outputs.each do |output|
|
|
63
|
+
next unless File.exist?(output)
|
|
64
|
+
|
|
65
|
+
original_output = File.read(output)
|
|
66
|
+
formatted_output = format_marker_comments(Hashing.normalize_line_endings(original_output))
|
|
67
|
+
File.write(output, formatted_output) if formatted_output != original_output
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def plan_command(_arguments)
|
|
73
|
+
project = Invocation.resolve_current_project_with_git_refresh_fallback(
|
|
74
|
+
ResolveOptions.new,
|
|
75
|
+
allow_git_refresh_fallback: true
|
|
76
|
+
)
|
|
77
|
+
rendered = Render.render_project(project)
|
|
78
|
+
lockfile = build_lockfile(project, rendered)
|
|
79
|
+
previous_lockfile = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
80
|
+
preview = Plan.build_materialization_preview(
|
|
81
|
+
project, rendered, lockfile, lockfile_path, previous_lockfile
|
|
82
|
+
)
|
|
83
|
+
Plan.print_materialization_report(preview, :plan)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def update_command(arguments)
|
|
87
|
+
package = arguments.reject { |argument| argument.start_with?("--") }.first
|
|
88
|
+
offline = arguments.include?("--offline")
|
|
89
|
+
if package
|
|
90
|
+
manifest = Pray.parse_manifest(Pray.read_manifest_text(manifest_path))
|
|
91
|
+
unless manifest.packages.any? { |entry| entry.name == package }
|
|
92
|
+
raise Error.manifest("package #{package} not found")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
install_command({locked: false, frozen: false, offline: offline, refresh: true})
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def unlock_command(name)
|
|
99
|
+
raise Error.manifest("unlock requires a package name") unless name
|
|
100
|
+
|
|
101
|
+
project = resolve_current_project
|
|
102
|
+
unless project.manifest.packages.any? { |entry| entry.name == name }
|
|
103
|
+
raise Error.manifest("package #{name} not found")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
previous_lockfile = Pray.read_lockfile(lockfile_path)
|
|
107
|
+
rendered = Render.render_project(project)
|
|
108
|
+
updated_lockfile = build_lockfile(project, rendered)
|
|
109
|
+
merged_lockfile = merge_selected_package_update(previous_lockfile, updated_lockfile, name)
|
|
110
|
+
Pray.write_lockfile(lockfile_path, merged_lockfile)
|
|
111
|
+
Render.write_rendered_targets(project, rendered)
|
|
112
|
+
puts "Unlocked #{name}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def merge_selected_package_update(previous, updated, selected_package)
|
|
116
|
+
merged = updated.dup
|
|
117
|
+
merged.package = updated.package.map do |package|
|
|
118
|
+
next package if package.name == selected_package
|
|
119
|
+
|
|
120
|
+
previous_package = previous.package.find { |entry| entry.name == package.name }
|
|
121
|
+
previous_package ? package.dup.tap { |copy| copy.version = previous_package.version } : package
|
|
122
|
+
end
|
|
123
|
+
merged
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module CLI
|
|
5
|
+
module Help
|
|
6
|
+
DOCS_URL = "https://github.com/kiskolabs/pray"
|
|
7
|
+
|
|
8
|
+
WORKFLOW_COMMANDS = [
|
|
9
|
+
"install [--locked|--frozen|--offline] resolve, render, and write Prayfile.lock",
|
|
10
|
+
"plan [--remote] preview materialization changes",
|
|
11
|
+
"apply apply the current plan",
|
|
12
|
+
"verify [--strict] check rendered output against the lockfile",
|
|
13
|
+
"drift [--semantic] compare lockfile to current resolution",
|
|
14
|
+
"render [--check] render targets without updating the lockfile",
|
|
15
|
+
"format, fmt rewrite Prayfile to recommended destination DSL"
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
PACKAGE_COMMANDS = [
|
|
19
|
+
"add <name> [constraint] [--path PATH]",
|
|
20
|
+
"remove <name>",
|
|
21
|
+
"update [package] [--major] [--latest] [--dry-run] [--json]",
|
|
22
|
+
"unlock <package>",
|
|
23
|
+
"vendor copy resolved packages locally",
|
|
24
|
+
"clean remove local cache and vendor trees"
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
DISTRIBUTION_COMMANDS = [
|
|
28
|
+
"publish --root PATH [--server URL ...]",
|
|
29
|
+
"login --server URL --email EMAIL",
|
|
30
|
+
"serve [--root PATH] [--host HOST] [--port PORT] [--stdio]",
|
|
31
|
+
"sync [--root PATH] [--peer URL ...]",
|
|
32
|
+
"confess <package> | --from-lock SPAN_ID [--accepted|--rejected]"
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
TRUST_COMMANDS = [
|
|
36
|
+
"trust list|show|add-key|remove-key|set-signed|set-allow|import-repo|import-registry|check"
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
INSPECT_COMMANDS = [
|
|
40
|
+
"list list declared packages",
|
|
41
|
+
"outdated [--remote] show constraint vs resolved versions",
|
|
42
|
+
"explain <package> show why a package was selected",
|
|
43
|
+
"tree print the dependency tree"
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
META_COMMANDS = [
|
|
47
|
+
"init [--targets tool_a,tool_b]",
|
|
48
|
+
"prayer init scaffold a prayer package",
|
|
49
|
+
"repo init scaffold a distribution root",
|
|
50
|
+
"manifest print canonical Prayfile JSON",
|
|
51
|
+
"package build a distributable prayer archive",
|
|
52
|
+
"version | -V | --version"
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
COMMAND_HELP = {
|
|
56
|
+
"install" => <<~TEXT.strip,
|
|
57
|
+
install — resolve packages, render targets, and update Prayfile.lock
|
|
58
|
+
|
|
59
|
+
Usage: pray install [--locked|--frozen|--offline]
|
|
60
|
+
|
|
61
|
+
--locked require an existing lockfile
|
|
62
|
+
--frozen require lockfile to match Prayfile exactly
|
|
63
|
+
--offline use cache only
|
|
64
|
+
TEXT
|
|
65
|
+
"verify" => <<~TEXT.strip,
|
|
66
|
+
verify — check rendered files against Prayfile.lock
|
|
67
|
+
|
|
68
|
+
Usage: pray verify [--strict]
|
|
69
|
+
|
|
70
|
+
Without --strict, orphan-marker warnings print to stderr but exit 0.
|
|
71
|
+
With --strict, any finding fails with exit code 6.
|
|
72
|
+
TEXT
|
|
73
|
+
"drift" => <<~TEXT.strip,
|
|
74
|
+
drift — report differences between lockfile and current resolution
|
|
75
|
+
|
|
76
|
+
Usage: pray drift [--semantic]
|
|
77
|
+
|
|
78
|
+
Exits with code 6 when drift is found.
|
|
79
|
+
TEXT
|
|
80
|
+
"update" => <<~TEXT.strip,
|
|
81
|
+
update — refresh package versions within constraints
|
|
82
|
+
|
|
83
|
+
Usage: pray update [package] [--major] [--latest] [--dry-run] [--json]
|
|
84
|
+
TEXT
|
|
85
|
+
"plan" => <<~TEXT.strip,
|
|
86
|
+
plan — preview install/apply changes
|
|
87
|
+
|
|
88
|
+
Usage: pray plan [--remote]
|
|
89
|
+
TEXT
|
|
90
|
+
"apply" => "apply — materialize the current resolution plan\n\nUsage: pray apply",
|
|
91
|
+
"trust" => <<~TEXT.strip,
|
|
92
|
+
trust — manage client trust policy for remote sources
|
|
93
|
+
|
|
94
|
+
Usage: pray trust <subcommand>
|
|
95
|
+
|
|
96
|
+
Subcommands: list, show, add-key, remove-key, set-signed, set-allow, import-repo, import-registry, check
|
|
97
|
+
TEXT
|
|
98
|
+
"init" => <<~TEXT.strip,
|
|
99
|
+
init — create a starter Prayfile
|
|
100
|
+
|
|
101
|
+
Usage: pray init [--targets tool_a,tool_b]
|
|
102
|
+
TEXT
|
|
103
|
+
"add" => <<~TEXT.strip,
|
|
104
|
+
add — declare a package in Prayfile
|
|
105
|
+
|
|
106
|
+
Usage: pray add <name> [constraint] [--path PATH]
|
|
107
|
+
TEXT
|
|
108
|
+
"publish" => <<~TEXT.strip,
|
|
109
|
+
publish — upload packages to a registry or local root
|
|
110
|
+
|
|
111
|
+
Usage: pray publish --root PATH [--server URL ...]
|
|
112
|
+
TEXT
|
|
113
|
+
"serve" => <<~TEXT.strip
|
|
114
|
+
serve — run a local registry server
|
|
115
|
+
|
|
116
|
+
Usage: pray serve [--root PATH] [--host HOST] [--port PORT] [--stdio]
|
|
117
|
+
TEXT
|
|
118
|
+
}.freeze
|
|
119
|
+
|
|
120
|
+
module_function
|
|
121
|
+
|
|
122
|
+
def print_concise_help
|
|
123
|
+
puts "pray — reproducible inference input for projects"
|
|
124
|
+
puts
|
|
125
|
+
puts "Declare shared instructions in Prayfile, lock versions, and render tool-specific output."
|
|
126
|
+
puts
|
|
127
|
+
puts "Getting started:"
|
|
128
|
+
puts " pray init"
|
|
129
|
+
puts " pray install"
|
|
130
|
+
puts " pray plan"
|
|
131
|
+
puts " pray apply"
|
|
132
|
+
puts " pray verify"
|
|
133
|
+
puts
|
|
134
|
+
print_command_groups
|
|
135
|
+
puts
|
|
136
|
+
puts "Run `pray help <command>` or `pray <command> --help` for one command."
|
|
137
|
+
puts "Documentation: #{DOCS_URL}"
|
|
138
|
+
puts "Exit codes: 0 success; 2 usage/parse; 3 resolution; 4 integrity; 5 render; 6 verify; 8 unsupported."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def print_command_help(command)
|
|
142
|
+
text = COMMAND_HELP[command]
|
|
143
|
+
return false unless text
|
|
144
|
+
|
|
145
|
+
puts text
|
|
146
|
+
puts
|
|
147
|
+
puts "Documentation: #{DOCS_URL}"
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def print_command_groups
|
|
152
|
+
puts "Workflow:"
|
|
153
|
+
WORKFLOW_COMMANDS.each { |line| puts " #{line}" }
|
|
154
|
+
puts "Packages:"
|
|
155
|
+
PACKAGE_COMMANDS.each { |line| puts " #{line}" }
|
|
156
|
+
puts "Distribution:"
|
|
157
|
+
DISTRIBUTION_COMMANDS.each { |line| puts " #{line}" }
|
|
158
|
+
puts "Trust:"
|
|
159
|
+
TRUST_COMMANDS.each { |line| puts " #{line}" }
|
|
160
|
+
puts "Inspect:"
|
|
161
|
+
INSPECT_COMMANDS.each { |line| puts " #{line}" }
|
|
162
|
+
puts "Meta:"
|
|
163
|
+
META_COMMANDS.each { |line| puts " #{line}" }
|
|
164
|
+
puts "Global flags: --no-input (disable prompts), --rm (ephemeral home), --trust [--global]"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module CLI
|
|
7
|
+
def manifest_path
|
|
8
|
+
Invocation.manifest_path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def lockfile_path
|
|
12
|
+
Invocation.lockfile_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def project_root
|
|
16
|
+
Invocation.project_root
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve_current_project(options = ResolveOptions.new)
|
|
20
|
+
Invocation.resolve_current_project(options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def default_output_for_target(target)
|
|
24
|
+
case target
|
|
25
|
+
when "tool_a" then "INSTRUCTIONS"
|
|
26
|
+
when "tool_b" then "TOOL_B"
|
|
27
|
+
else target.upcase
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_lockfile(project, rendered)
|
|
32
|
+
LockfileIO.build_lockfile(
|
|
33
|
+
project.manifest_hash,
|
|
34
|
+
project.environment,
|
|
35
|
+
project.project_root,
|
|
36
|
+
project.manifest.sources,
|
|
37
|
+
project.manifest.targets,
|
|
38
|
+
rendered,
|
|
39
|
+
project.packages,
|
|
40
|
+
project.source_revisions,
|
|
41
|
+
project.source_host_keys
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ensure_existing_lockfile(path)
|
|
46
|
+
raise Error.verify("missing Prayfile.lock; run pray install first") unless File.exist?(path)
|
|
47
|
+
|
|
48
|
+
Pray.read_lockfile(path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ensure_lockfile_current(project, rendered, existing)
|
|
52
|
+
current = build_lockfile(project, rendered)
|
|
53
|
+
return if Pray.lockfiles_equivalent?(current, existing)
|
|
54
|
+
|
|
55
|
+
raise Error.verify("lockfile needs update; rerun pray install to refresh Prayfile.lock")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ensure_rendered_outputs_current(project, rendered)
|
|
59
|
+
rendered.each do |target|
|
|
60
|
+
path = File.join(project.project_root, target.path)
|
|
61
|
+
on_disk = File.read(path)
|
|
62
|
+
if on_disk != target.content
|
|
63
|
+
raise Error.render("#{path} is stale; rerun pray install to regenerate it or pray plan to inspect the diff")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def insert_manifest_statement(text, statement)
|
|
69
|
+
lines = text.lines.map(&:chomp)
|
|
70
|
+
insertion_index = lines.index do |line|
|
|
71
|
+
trimmed = line.lstrip
|
|
72
|
+
trimmed.start_with?("local ", "render ")
|
|
73
|
+
end || lines.length
|
|
74
|
+
lines.insert(insertion_index, statement)
|
|
75
|
+
output = lines.join("\n")
|
|
76
|
+
output += "\n" unless output.end_with?("\n")
|
|
77
|
+
output
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def remove_manifest_statement(text, name)
|
|
81
|
+
lines = text.lines.map(&:chomp)
|
|
82
|
+
package_prefix = "agent \"#{name}\""
|
|
83
|
+
alternate_prefix = "agent '#{name}'"
|
|
84
|
+
index = lines.index do |line|
|
|
85
|
+
trimmed = line.lstrip
|
|
86
|
+
trimmed.start_with?(package_prefix, alternate_prefix)
|
|
87
|
+
end
|
|
88
|
+
if index
|
|
89
|
+
lines.delete_at(index)
|
|
90
|
+
lines.delete_at(index) if index < lines.length && lines[index].strip.empty?
|
|
91
|
+
lines.delete_at(index - 1) if index.positive? && lines[index - 1].strip.empty?
|
|
92
|
+
end
|
|
93
|
+
output = lines.join("\n")
|
|
94
|
+
output += "\n" unless output.end_with?("\n")
|
|
95
|
+
output
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_marker_comments(text)
|
|
99
|
+
lines = text.split("\n", -1).map do |line|
|
|
100
|
+
canonical_marker_line(line) || line
|
|
101
|
+
end
|
|
102
|
+
output = lines.join("\n")
|
|
103
|
+
output += "\n" unless output.end_with?("\n")
|
|
104
|
+
output
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def canonical_marker_line(line)
|
|
108
|
+
trimmed = line.strip
|
|
109
|
+
remainder = trimmed.delete_prefix("<!--").strip
|
|
110
|
+
return nil unless remainder.start_with?("pray:")
|
|
111
|
+
|
|
112
|
+
content = remainder.delete_prefix("pray:").strip.delete_suffix("-->").strip
|
|
113
|
+
return "<!-- pray:0 ignore-comments -->" if content == "0 ignore-comments"
|
|
114
|
+
return "<!-- pray:#{content} -->" if content.match?(/\A[a-z0-9]+\z/)
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def package_source_summary(package)
|
|
120
|
+
if package.declaration.path
|
|
121
|
+
"path:#{package.declaration.path}"
|
|
122
|
+
elsif package.declaration.source
|
|
123
|
+
"source:#{package.declaration.source}"
|
|
124
|
+
else
|
|
125
|
+
"root:#{package.root}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_list(values)
|
|
130
|
+
values.empty? ? "none" : values.join(", ")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_verification_report(report)
|
|
134
|
+
Verify.format_verification_report(report)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def format_drift_report(report)
|
|
138
|
+
Verify.format_drift_report(report)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_tree_node(package, package_map, depth, ancestry, lines)
|
|
142
|
+
indent = " " * depth
|
|
143
|
+
lines << "#{indent}#{package.declaration.name} #{package.spec.version}"
|
|
144
|
+
return unless ancestry.add?(package.declaration.name)
|
|
145
|
+
|
|
146
|
+
package.spec.dependencies.each do |dependency|
|
|
147
|
+
resolved = package_map[dependency.name]
|
|
148
|
+
if resolved
|
|
149
|
+
if ancestry.include?(resolved.declaration.name)
|
|
150
|
+
lines << "#{indent} #{resolved.declaration.name} #{resolved.spec.version} (cycle)"
|
|
151
|
+
else
|
|
152
|
+
render_tree_node(resolved, package_map, depth + 1, ancestry, lines)
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
lines << "#{indent} #{dependency.name} (missing)"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def remove_path_if_exists(path)
|
|
161
|
+
return unless File.exist?(path)
|
|
162
|
+
|
|
163
|
+
File.directory?(path) ? FileUtils.rm_rf(path) : File.delete(path)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|