pray-cli 1.16.0 → 1.18.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/pray/apply_report.rb +49 -0
  4. data/lib/pray/cli/commands/init.rb +1 -20
  5. data/lib/pray/cli/commands/init_prayer.rb +76 -0
  6. data/lib/pray/cli/commands/init_prayer_manifest.rb +102 -0
  7. data/lib/pray/cli/commands/packages.rb +13 -2
  8. data/lib/pray/cli/commands/trust.rb +3 -3
  9. data/lib/pray/cli/commands/update_latest.rb +14 -16
  10. data/lib/pray/cli/commands/workflow.rb +5 -0
  11. data/lib/pray/cli/help.rb +52 -15
  12. data/lib/pray/cli/helpers.rb +13 -5
  13. data/lib/pray/cli/parse.rb +21 -1
  14. data/lib/pray/cli.rb +3 -1
  15. data/lib/pray/destination.rb +32 -2
  16. data/lib/pray/distribution.rb +94 -0
  17. data/lib/pray/format_serialize.rb +1 -4
  18. data/lib/pray/git_cache.rb +192 -0
  19. data/lib/pray/git_clone.rb +32 -0
  20. data/lib/pray/git_materialize.rb +76 -0
  21. data/lib/pray/git_run.rb +41 -0
  22. data/lib/pray/git_sources.rb +60 -145
  23. data/lib/pray/local_prayer.rb +38 -0
  24. data/lib/pray/lockfile.rb +8 -2
  25. data/lib/pray/manifest.rb +6 -3
  26. data/lib/pray/manifest_json.rb +1 -0
  27. data/lib/pray/manifest_parser_blocks.rb +15 -15
  28. data/lib/pray/materialize.rb +7 -1
  29. data/lib/pray/package_spec.rb +25 -13
  30. data/lib/pray/package_spec_render.rb +4 -3
  31. data/lib/pray/package_spec_version.rb +33 -0
  32. data/lib/pray/plan.rb +2 -1
  33. data/lib/pray/publish.rb +27 -2
  34. data/lib/pray/registry.rb +4 -15
  35. data/lib/pray/registry_http_cache.rb +17 -0
  36. data/lib/pray/render.rb +6 -36
  37. data/lib/pray/render_managed.rb +64 -0
  38. data/lib/pray/render_patch.rb +63 -12
  39. data/lib/pray/resolve.rb +6 -37
  40. data/lib/pray/resolve_local.rb +39 -0
  41. data/lib/pray/resolve_source.rb +5 -0
  42. data/lib/pray/serve.rb +15 -4
  43. data/lib/pray/serve_range.rb +36 -0
  44. data/lib/pray/torrent_manifest.rb +78 -0
  45. data/lib/pray/upstream_drift.rb +42 -0
  46. data/lib/pray/upstream_merge.rb +25 -0
  47. data/lib/pray/upstream_refresh.rb +28 -2
  48. data/lib/pray/verify.rb +2 -2
  49. data/lib/pray/version.rb +2 -2
  50. data/lib/pray.rb +10 -0
  51. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5950b63f50e919bd5864874919afde95413ed095954046604d570d4c3daf090a
4
- data.tar.gz: 82056f753f0d244afd54de70b7528d1e5d98b0c67f0da18538eb5d906986f12b
3
+ metadata.gz: a562f35befa229763cc5f2f9be9b2863d35be5002f2269a847261b37d4367736
4
+ data.tar.gz: db714d3beafd859ac0d121c03ccdde09ccc644886e75c8928279cd8bc2aee69d
5
5
  SHA512:
6
- metadata.gz: e3289cc2a08751a1358c37ddfb692d042c2d522374b02ed370dd25e703f5c5fe3be66996bbd008a7e6dfbe26a9ea6b278b87f640035a55a235ef7e1ed1d00967
7
- data.tar.gz: a54b4dbed546b3f301d42692033552c3878b2281f25fee70e647e02634c19541a7440542bddbade45619a2e6dd4cac8bcefc05ff89b035645195ce90a3bffad6
6
+ metadata.gz: 866b36dd793f81c0cd9632005aa11c9d061db091ee250d39d6cfdce8aed5906e30ad29f635b5e5fb7a4f969098141bc5f6baadc18eb334256b34102682b612b3
7
+ data.tar.gz: 8c03862357b6bc6755ac648f1f171b4471c62fb0c0c1e401abccaff51d07dfe1f3728f57633725366bc4d2b2d617f9f2fc1a03d2d80b05ce3be2ac96899ed0ff
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.18.0 (2026-09-17)
4
+
5
+ - Clone a git catalog without unused package blobs, and fetch a used package file when install needs it.
6
+ - Skip cloning a git catalog when no package uses that source.
7
+ - Fetch a used git catalog once on `pray update` instead of fetching it twice.
8
+ - Keep packages from two git sources that share a clone URL when each source names a different `subdir`.
9
+ - Share one git object store for those subdirectory checkouts.
10
+ - Import signing keys with `pray trust import-repo` after a catalog was cloned only through `subdir` sources.
11
+ - Reuse registry package metadata during one command so `pray update --latest` does not download the same package JSON twice.
12
+ - Skip a second resolve when `pray update --latest` does not rewrite constraints or upstream pins, and still refresh path-fork files when needed.
13
+
14
+ ## 1.17.0 (2026-09-16)
15
+
16
+ - Locate local prayers through a path source of any directory name (RFC 0117). `pray prayer init` adds `source "local"` when needed and declares `pray "local/project"` once. Version is optional until `pray package` or `pray publish`. `.agents/project.md` remains a compose shortcut for one local file.
17
+ - Copy upstream files into an empty path-fork tree on `pray install` and `pray update` (RFC 0116). Extra listed overlay files stay. `pray outdated` lists fork files that differ from the locked upstream. An identity-only fork uses empty `spec.files`; refresh lists content paths, not the prayspec file.
18
+ - Refuse a local path with `file:` and a local path inside `tree`. Compose still embeds local fragments. Package `file:` and package `tree` stay.
19
+ - Name dest versus lock on `pray verify`, dest versus a fresh render on `pray drift`, and dest write on `pray render`. Dry-run of a write is `pray plan`. Listed local compose files stay on `pray install`.
20
+ - Write `.praytorrent.json` when `v1/distribution.json` lists `protocols: ["torrent"]` (RFC 0062). `pray repo init` writes empty protocols.
21
+ - Parse `spec.maintainers` as a string array, and accept `spec.pray_version` as an alias for `spec.prayfile_version`.
22
+ - Re-embed a changed local compose source on `pray install`, `pray update`, and `pray plan`, and print its `sha256` as `checked` or `was`. List local checksum drift under `pray outdated`.
23
+
3
24
  ## 1.16.0 (2026-09-15)
4
25
 
5
26
  - Rewrite a two-component pessimistic Prayfile pin on `pray update --latest` (`~> 2.2` to `~> 2.4` when registry latest is 2.4.0), matching the Rust and TypeScript CLIs. Install a newer version that the current constraint already allows.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ApplyReport
5
+ module_function
6
+
7
+ def local_summary_lines(previous, project)
8
+ previous_checksums = previous_local_checksums(previous)
9
+ project.local_files.filter_map do |local|
10
+ next if local.content.empty? && local.optional
11
+
12
+ checksum = local.source_checksum
13
+ previous_checksum = previous_checksums[local.manifest_path]
14
+ if previous_checksum.nil?
15
+ "Installing #{local.manifest_path} (#{checksum})"
16
+ elsif previous_checksum == checksum
17
+ "Using #{local.manifest_path} (#{checksum} checked)"
18
+ else
19
+ "Updating #{local.manifest_path} (#{checksum} was #{previous_checksum})"
20
+ end
21
+ end
22
+ end
23
+
24
+ def outdated_local_lines(previous, project)
25
+ previous_checksums = previous_local_checksums(previous)
26
+ project.local_files.filter_map do |local|
27
+ next if local.content.empty? && local.optional
28
+
29
+ checksum = local.source_checksum
30
+ previous_checksum = previous_checksums[local.manifest_path]
31
+ if previous_checksum && previous_checksum != checksum
32
+ "#{local.manifest_path} #{previous_checksum} -> #{checksum}"
33
+ elsif previous && previous_checksum.nil?
34
+ "#{local.manifest_path} (new) -> #{checksum}"
35
+ end
36
+ end
37
+ end
38
+
39
+ def previous_local_checksums(previous)
40
+ return {} unless previous
41
+
42
+ previous.managed_span.each_with_object({}) do |span, checksums|
43
+ next unless span.package == LOCAL_EMBED_PACKAGE
44
+
45
+ checksums[span.export] = span.source_checksum
46
+ end
47
+ end
48
+ end
49
+ end
@@ -25,26 +25,6 @@ module Pray
25
25
  File.write(path, "#{lines.join("\n")}\n")
26
26
  end
27
27
 
28
- def prayer_init_command
29
- root = Dir.pwd
30
- package_name = File.basename(root)
31
- package_name = "prayer-package" if package_name.strip.empty?
32
- prayspec_path = File.join(root, "#{package_name}.prayspec")
33
- raise Error.manifest("package spec already exists: #{prayspec_path}") if File.exist?(prayspec_path)
34
-
35
- File.write(
36
- prayspec_path,
37
- <<~SPEC
38
- Package::Specification.new do |spec|
39
- spec.name = "#{package_name}"
40
- spec.version = "0.1.0"
41
- spec.summary = "Prayer package"
42
- spec.files = []
43
- end
44
- SPEC
45
- )
46
- end
47
-
48
28
  def repo_init_command
49
29
  distribution_root = repo_distribution_root(Dir.pwd)
50
30
  index_path = File.join(distribution_root, "v1", "index.json")
@@ -53,6 +33,7 @@ module Pray
53
33
  FileUtils.mkdir_p(File.join(distribution_root, "v1", "packages"))
54
34
  FileUtils.mkdir_p(File.join(distribution_root, "v1", "artifacts"))
55
35
  Publish.write_registry_index(distribution_root, RegistryIndex.new)
36
+ Distribution.write_settings(distribution_root)
56
37
  end
57
38
 
58
39
  def repo_distribution_root(root)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Pray
6
+ module CLI
7
+ def prayer_init_command(name = nil, directory = nil)
8
+ if File.exist?(manifest_path)
9
+ scaffold_local_prayer(name, directory)
10
+ else
11
+ scaffold_standalone_package(name)
12
+ end
13
+ end
14
+
15
+ def scaffold_local_prayer(name, directory)
16
+ requested = name.to_s.strip
17
+ package_name = LocalPrayer.validate_name!(requested.empty? ? DEFAULT_LOCAL_PRAYER_NAME : requested)
18
+ source_name, source_directory = resolve_local_prayer_directory(directory)
19
+ root = File.join(project_root, source_directory, package_name)
20
+ prayspec_path = File.join(root, "#{package_name}.prayspec")
21
+ raise Error.manifest("package spec already exists: #{prayspec_path}") if File.exist?(prayspec_path)
22
+ raise Error.manifest("prayer directory already exists: #{root}") if File.exist?(root)
23
+
24
+ FileUtils.mkdir_p(File.join(root, "exports"))
25
+ File.write(prayspec_path, local_prayspec(source_name, package_name))
26
+ File.write(File.join(root, "README.md"), "# #{package_name}\n")
27
+ File.write(File.join(root, "exports", "#{package_name}.md"), "# #{package_name}\n")
28
+ declare_local_prayer("#{source_name}/#{package_name}")
29
+ end
30
+
31
+ def scaffold_standalone_package(name)
32
+ root = Dir.pwd
33
+ package_name = if name.nil? || name.strip.empty?
34
+ basename = File.basename(root)
35
+ basename.strip.empty? ? "prayer-package" : basename
36
+ else
37
+ LocalPrayer.validate_name!(name)
38
+ end
39
+ prayspec_path = File.join(root, "#{package_name}.prayspec")
40
+ raise Error.manifest("package spec already exists: #{prayspec_path}") if File.exist?(prayspec_path)
41
+
42
+ File.write(prayspec_path, standalone_prayspec(package_name))
43
+ readme = File.join(root, "README.md")
44
+ File.write(readme, "# #{package_name}\n") unless File.exist?(readme)
45
+ FileUtils.mkdir_p(File.join(root, "exports"))
46
+ end
47
+
48
+ def local_prayspec(source_name, name)
49
+ <<~SPEC
50
+ Package::Specification.new do |spec|
51
+ spec.name = "#{source_name}/#{name}"
52
+ spec.summary = "Describe this package"
53
+ spec.files = ["README.md", "exports/#{name}.md"]
54
+ spec.exports = {
55
+ "#{name}" => {
56
+ type: "fragment",
57
+ path: "exports/#{name}.md"
58
+ }
59
+ }
60
+ end
61
+ SPEC
62
+ end
63
+
64
+ def standalone_prayspec(name)
65
+ <<~SPEC
66
+ Package::Specification.new do |spec|
67
+ spec.name = "#{name}"
68
+ spec.version = "0.1.0"
69
+ spec.summary = "Describe this package"
70
+ spec.files = ["README.md"]
71
+ spec.exports = {}
72
+ end
73
+ SPEC
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module CLI
5
+ def resolve_local_prayer_directory(requested)
6
+ manifest = Pray.parse_manifest(Pray.read_manifest_text(manifest_path))
7
+ path_sources = path_sources_for(manifest)
8
+ if requested
9
+ directory = requested.to_s.strip
10
+ raise Error.usage("--path requires a directory") if directory.empty?
11
+
12
+ matching = path_sources.find { |source| source.url == directory }
13
+ return [matching.name, matching.url] if matching
14
+ if path_sources.length == 1
15
+ raise Error.usage("path source already uses #{path_sources.first.url}")
16
+ end
17
+
18
+ name = ensure_path_source(directory)
19
+ return [name, directory]
20
+ end
21
+
22
+ case path_sources.length
23
+ when 0
24
+ name = ensure_path_source(DEFAULT_PATH_SOURCE_DIRECTORY)
25
+ [name, DEFAULT_PATH_SOURCE_DIRECTORY]
26
+ when 1
27
+ [path_sources.first.name, path_sources.first.url]
28
+ else
29
+ raise Error.usage("say which directory with --path")
30
+ end
31
+ end
32
+
33
+ def declare_local_prayer(package_name)
34
+ manifest_text = Pray.read_manifest_text(manifest_path)
35
+ manifest = Pray.parse_manifest(manifest_text)
36
+ return if manifest.packages.any? { |package| package.name == package_name }
37
+
38
+ statement = "pray \"#{package_name}\""
39
+ updated = insert_into_first_compose(manifest_text, statement) ||
40
+ insert_manifest_statement(manifest_text, statement)
41
+ Transaction.write_file(manifest_path, updated)
42
+ end
43
+
44
+ def path_sources_for(manifest)
45
+ manifest.sources.select { |source| source.kind == "path" }
46
+ end
47
+
48
+ def ensure_path_source(directory)
49
+ manifest_text = Pray.read_manifest_text(manifest_path)
50
+ manifest = Pray.parse_manifest(manifest_text)
51
+ existing = manifest.sources.find { |source| source.kind == "path" && source.url == directory }
52
+ return existing.name if existing
53
+
54
+ name = unused_source_name(manifest, directory)
55
+ statement = "source \"#{name}\", path: \"#{directory}\""
56
+ Transaction.write_file(manifest_path, insert_source_statement(manifest_text, statement))
57
+ name
58
+ end
59
+
60
+ def unused_source_name(manifest, directory)
61
+ taken = manifest.sources.map(&:name)
62
+ return DEFAULT_PATH_SOURCE_NAME unless taken.include?(DEFAULT_PATH_SOURCE_NAME)
63
+
64
+ fallback = directory.to_s.split(%r{[/\\]}).reject(&:empty?).last || DEFAULT_PATH_SOURCE_NAME
65
+ raise Error.manifest("source name #{fallback} is already used") if taken.include?(fallback)
66
+
67
+ fallback
68
+ end
69
+
70
+ def insert_source_statement(text, statement)
71
+ lines = text.lines.map(&:chomp)
72
+ last_source = lines.rindex { |line| line.lstrip.start_with?("source ") }
73
+ insertion_index = if last_source
74
+ last_source + 1
75
+ else
76
+ prayfile = lines.index { |line| line.lstrip.start_with?("prayfile ") }
77
+ prayfile ? prayfile + 1 : 0
78
+ end
79
+ lines.insert(insertion_index, statement)
80
+ join_manifest_lines(lines)
81
+ end
82
+
83
+ def insert_into_first_compose(text, statement)
84
+ lines = text.lines.map(&:chomp)
85
+ index = lines.index do |line|
86
+ trimmed = line.lstrip
87
+ trimmed.start_with?("compose ") && trimmed.end_with?(" do")
88
+ end
89
+ return unless index
90
+
91
+ indent = lines[index][/\A\s*/].to_s
92
+ lines.insert(index + 1, "#{indent} #{statement}")
93
+ join_manifest_lines(lines)
94
+ end
95
+
96
+ def join_manifest_lines(lines)
97
+ output = lines.join("\n")
98
+ output += "\n" unless output.end_with?("\n")
99
+ output
100
+ end
101
+ end
102
+ end
@@ -36,7 +36,7 @@ module Pray
36
36
  project = resolve_current_project
37
37
  lines = ["Package list"]
38
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)}"
39
+ lines << "#{package.declaration.name} #{package.spec.recorded_version} source=#{package_source_summary(package)} exports=#{format_list(package.selected_exports)}"
40
40
  end
41
41
  puts lines.join("\n")
42
42
  end
@@ -54,6 +54,7 @@ module Pray
54
54
  def package_command
55
55
  project = resolve_current_project
56
56
  project.packages.each do |package|
57
+ package.spec.require_release_version!
57
58
  output_path = Archive.package_archive_path(package.declaration.name, package.spec.version)
58
59
  Archive.write_package_archive(package, output_path)
59
60
  end
@@ -72,7 +73,7 @@ module Pray
72
73
  lines = ["Package explanation"]
73
74
  lines << "name: #{package.declaration.name}"
74
75
  lines << "constraint: #{package.declaration.constraint}"
75
- lines << "resolved version: #{package.spec.version}"
76
+ lines << "resolved version: #{package.spec.recorded_version}"
76
77
  if package.registry_latest_version
77
78
  lines << "registry latest: #{package.registry_latest_version}"
78
79
  end
@@ -102,6 +103,16 @@ module Pray
102
103
  else
103
104
  Plan.package_summary_lines(previous_lockfile, latest_lockfile, project).each { |line| puts line }
104
105
  end
106
+ local_lines = ApplyReport.outdated_local_lines(previous_lockfile, project)
107
+ unless local_lines.empty?
108
+ puts "Outdated local files"
109
+ local_lines.each { |line| puts line }
110
+ end
111
+ fork_lines = Upstream.path_fork_drift_lines(project, previous_lockfile, ResolveOptions.new)
112
+ unless fork_lines.empty?
113
+ puts "Outdated path forks"
114
+ fork_lines.each { |line| puts line }
115
+ end
105
116
  end
106
117
  end
107
118
  end
@@ -28,10 +28,10 @@ module Pray
28
28
 
29
29
  def trust_import_repo_command(source_url, match_prefix: nil)
30
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"))
31
+ repository = GitSources.git_source_cached_repository(Dir.pwd, clone_url)
32
+ unless repository
33
33
  raise Error.resolution(
34
- "no cached git repository for #{clone_url} at #{repository}"
34
+ "no cached git repository for #{clone_url}"
35
35
  )
36
36
  end
37
37
 
@@ -6,13 +6,15 @@ module Pray
6
6
  path = manifest_path
7
7
  original_text = Pray.read_manifest_text(path)
8
8
  manifest_text = original_text
9
- preview_options = ResolveOptions.new(
9
+ unlocked = package ? Set[package] : Set.new
10
+ options = ResolveOptions.new(
10
11
  offline: offline,
11
12
  refresh: true,
12
13
  refresh_source_revisions: true,
13
- ignore_locked_versions: true
14
+ ignore_locked_versions: package.nil?,
15
+ unlocked_packages: unlocked
14
16
  )
15
- project = resolve_current_project(preview_options)
17
+ project = resolve_current_project(options)
16
18
  if package && project.manifest.packages.none? { |entry| entry.name == package }
17
19
  raise Error.manifest("package #{package} not found")
18
20
  end
@@ -46,24 +48,20 @@ module Pray
46
48
 
47
49
  previous = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
48
50
  upstream_plans = Upstream.plan_path_upstream_latest_constraints(
49
- project, previous, package, preview_options
51
+ project, previous, package, options
50
52
  )
51
53
  print_latest_constraint_plans(manifest_updates, upstream_plans)
52
54
 
53
55
  return if dry_run
54
56
 
55
- Upstream.apply_path_upstream_latest_constraints(upstream_plans)
56
- Transaction.write_file(path, manifest_text) if manifest_text != original_text
57
- unlocked = package ? Set[package] : Set.new
58
- options = ResolveOptions.new(
59
- offline: offline,
60
- refresh: true,
61
- refresh_source_revisions: true,
62
- ignore_locked_versions: package.nil?,
63
- unlocked_packages: unlocked
64
- )
65
- current = resolve_current_project(options)
66
- Upstream.apply_path_upstream_refreshes(current, previous, package, options)
57
+ if !manifest_updates.empty? || !upstream_plans.empty?
58
+ Upstream.apply_path_upstream_latest_constraints(upstream_plans)
59
+ Transaction.write_file(path, manifest_text) if manifest_text != original_text
60
+ current = resolve_current_project(options)
61
+ Upstream.apply_path_upstream_refreshes(current, previous, package, options)
62
+ else
63
+ Upstream.apply_path_upstream_refreshes(project, previous, package, options)
64
+ end
67
65
  install_command(
68
66
  {
69
67
  locked: false,
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  module Pray
6
6
  module CLI
7
7
  def install_command(flags)
8
+ previous_lockfile = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
8
9
  Pray.materialize_project(
9
10
  manifest_path: manifest_path,
10
11
  frozen: flags[:frozen],
@@ -14,6 +15,10 @@ module Pray
14
15
  ignore_locked_versions: flags[:ignore_locked_versions],
15
16
  unlocked_packages: flags[:unlocked_packages]
16
17
  )
18
+ return if flags[:frozen] || flags[:locked]
19
+
20
+ project = resolve_current_project
21
+ ApplyReport.local_summary_lines(previous_lockfile, project).each { |line| puts line }
17
22
  end
18
23
 
19
24
  def render_command(flags)
data/lib/pray/cli/help.rb CHANGED
@@ -4,19 +4,19 @@ module Pray
4
4
  module CLI
5
5
  module Help
6
6
  WORKFLOW_COMMANDS = [
7
- "install [--locked|--frozen|--offline] resolve, render, and write Prayfile.lock",
8
- "plan [--remote] preview materialization changes",
7
+ "install [--locked|--frozen|--offline] keep locked versions, re-embed listed locals",
8
+ "plan [--remote] dry-run of install: dest after patch",
9
9
  "apply apply the current plan",
10
- "verify [--strict] check rendered output against the lockfile",
11
- "drift [--semantic] compare lockfile to current resolution",
12
- "render [--check] render targets without updating the lockfile",
10
+ "verify [--strict] dest managed spans versus Prayfile.lock",
11
+ "drift [--semantic] dest versus lock, then dest versus a fresh render",
12
+ "render [--check] write dest from current inputs",
13
13
  "format, fmt rewrite Prayfile to recommended destination DSL"
14
14
  ].freeze
15
15
 
16
16
  PACKAGE_COMMANDS = [
17
17
  "add <name> [constraint] [--path PATH] declare a package in Prayfile",
18
18
  "remove <name> remove a package from Prayfile",
19
- "update [package]",
19
+ "update [package] re-resolve packages and git sources within constraints",
20
20
  "unlock <package> clear a locked package pin",
21
21
  "vendor copy resolved packages locally",
22
22
  "clean remove local cache and vendor trees"
@@ -43,7 +43,7 @@ module Pray
43
43
 
44
44
  META_COMMANDS = [
45
45
  "init [--targets tool_a,tool_b] create a starter Prayfile",
46
- "prayer init scaffold a prayer package",
46
+ "prayer init [name] [--path DIR] scaffold a local prayer",
47
47
  "repo init scaffold a distribution root",
48
48
  "manifest print canonical Prayfile JSON",
49
49
  "package build a distributable prayer archive",
@@ -58,33 +58,49 @@ module Pray
58
58
 
59
59
  COMMAND_HELP = {
60
60
  "install" => <<~TEXT.strip,
61
- resolve packages, render targets, and update Prayfile.lock
61
+ keep locked package versions and re-embed listed local compose sources
62
62
 
63
63
  Usage: pray install [--locked|--frozen|--offline]
64
64
 
65
+ Install renders dest and writes Prayfile.lock. Package versions stay at the lock.
66
+ Listed local compose files are re-embedded and their span checksums refresh.
67
+ A path package with spec.upstream copies upstream files when the path tree has no content yet.
68
+ Use pray update to re-resolve package versions and git sources.
69
+ Use pray plan to preview a write.
70
+
65
71
  --locked require an existing lockfile
66
- --frozen require lockfile to match Prayfile exactly
72
+ --frozen fail if dest or lock would change
67
73
  --offline use cache only
68
74
  TEXT
69
75
  "verify" => <<~TEXT.strip,
70
- check rendered files against Prayfile.lock
76
+ compare dest managed spans versus Prayfile.lock
71
77
 
72
78
  Usage: pray verify [--strict]
73
79
 
80
+ Read-only integrity check. It does not compare dest to a fresh render from current sources.
81
+ A changed local compose file is clean here when dest and lock still match.
82
+ Use pray drift to compare dest with a fresh render. Use pray plan to preview a write.
83
+
74
84
  Without --strict, orphan-marker warnings print to stderr but exit 0.
75
85
  With --strict, any finding fails with exit code 6.
76
86
  TEXT
77
87
  "drift" => <<~TEXT.strip,
78
- report differences between lockfile and current resolution
88
+ compare dest versus Prayfile.lock, then dest versus a fresh render
79
89
 
80
90
  Usage: pray drift [--semantic]
81
91
 
92
+ Read-only finding report from current packages and listed local files. Not a line diff.
82
93
  Exits with code 6 when drift is found.
94
+ Use pray plan to preview a write. Use pray verify for dest versus lock alone.
83
95
  TEXT
84
96
  "render" => <<~TEXT.strip,
85
- render targets without updating the lockfile
97
+ write dest from current inputs
86
98
 
87
99
  Usage: pray render [--check]
100
+
101
+ Without --check, render writes dest. It does not write Prayfile.lock.
102
+ Use pray plan to preview a write.
103
+ --check writes nothing. It fails unless dest already matches a fresh render from current inputs.
88
104
  TEXT
89
105
  "format" => <<~TEXT.strip,
90
106
  rewrite Prayfile to recommended destination DSL
@@ -99,18 +115,25 @@ module Pray
99
115
  pray fmt
100
116
  TEXT
101
117
  "update" => <<~TEXT.strip,
102
- refresh package versions within constraints
118
+ re-resolve package versions and git sources within current constraints
103
119
 
104
120
  Usage: pray update [package] [--latest] [--latest --dry-run]
105
121
 
122
+ With no package name, update re-resolves the whole graph within constraints
123
+ and refreshes git source revisions. A package name unlocks that package only.
124
+ Path-fork packages can refresh from upstream.
125
+ Listed local compose files are re-embedded by pray install, not by update.
126
+
106
127
  --latest adjusts constraints to allow the latest package versions.
107
128
  --latest also rewrites exact spec.upstream pins in path packages, then refreshes those trees.
108
129
  --latest --dry-run prints the planned rewrite and does not write.
109
130
  TEXT
110
131
  "plan" => <<~TEXT.strip,
111
- preview install/apply changes
132
+ preview what install or apply would write after patch
112
133
 
113
134
  Usage: pray plan [--remote]
135
+
136
+ This is the dry-run of a write. It does not write dest or the lock.
114
137
  TEXT
115
138
  "apply" => "materialize the current resolution plan\n\nUsage: pray apply",
116
139
  "add" => <<~TEXT.strip,
@@ -159,6 +182,8 @@ module Pray
159
182
  show constraint vs resolved versions
160
183
 
161
184
  Usage: pray outdated [--remote]
185
+
186
+ Path-fork files that differ from the locked upstream are listed.
162
187
  TEXT
163
188
  "explain" => <<~TEXT.strip,
164
189
  show why a package was selected
@@ -171,7 +196,19 @@ module Pray
171
196
 
172
197
  Usage: pray init [--targets tool_a,tool_b]
173
198
  TEXT
174
- "prayer" => "scaffold a prayer package\n\nUsage: pray prayer init",
199
+ "prayer" => <<~TEXT.strip,
200
+ scaffold a local prayer under a path source
201
+
202
+ Usage: pray prayer init [name] [--path DIR]
203
+
204
+ With a Prayfile, writes <name>/ under the path source directory without a version.
205
+ The default directory is prayers/. Use --path to choose another.
206
+ Declares pray "<source>/<name>" once, such as pray "local/project", inside compose when a compose block exists.
207
+ The default name is project. The name v1 is reserved for the distribution layout.
208
+ Without a Prayfile, writes a versioned package spec in the current directory.
209
+ Add spec.version before pray package or pray publish.
210
+ A compose file such as .agents/project.md remains a shortcut for one local file.
211
+ TEXT
175
212
  "repo" => "scaffold a distribution root\n\nUsage: pray repo init",
176
213
  "manifest" => "print canonical Prayfile JSON\n\nUsage: pray manifest",
177
214
  "package" => "build a distributable prayer archive\n\nUsage: pray package",
@@ -80,11 +80,19 @@ module Pray
80
80
 
81
81
  def remove_manifest_statement(text, name)
82
82
  lines = text.lines.map(&:chomp)
83
- package_prefix = "agent \"#{name}\""
84
- alternate_prefix = "agent '#{name}'"
83
+ prefixes = [
84
+ %(pray "#{name}"),
85
+ %(pray '#{name}'),
86
+ %(use "#{name}"),
87
+ %(include "#{name}"),
88
+ %(agent "#{name}"),
89
+ %(agent '#{name}'),
90
+ %(package "#{name}"),
91
+ %(package '#{name}')
92
+ ]
85
93
  index = lines.index do |line|
86
94
  trimmed = line.lstrip
87
- trimmed.start_with?(package_prefix, alternate_prefix)
95
+ prefixes.any? { |prefix| trimmed.start_with?(prefix) }
88
96
  end
89
97
  if index
90
98
  lines.delete_at(index)
@@ -141,14 +149,14 @@ module Pray
141
149
 
142
150
  def render_tree_node(package, package_map, depth, ancestry, lines)
143
151
  indent = " " * depth
144
- lines << "#{indent}#{package.declaration.name} #{package.spec.version}"
152
+ lines << "#{indent}#{package.declaration.name} #{package.spec.recorded_version}"
145
153
  return unless ancestry.add?(package.declaration.name)
146
154
 
147
155
  package.spec.dependencies.each do |dependency|
148
156
  resolved = package_map[dependency.name]
149
157
  if resolved
150
158
  if ancestry.include?(resolved.declaration.name)
151
- lines << "#{indent} #{resolved.declaration.name} #{resolved.spec.version} (cycle)"
159
+ lines << "#{indent} #{resolved.declaration.name} #{resolved.spec.recorded_version} (cycle)"
152
160
  else
153
161
  render_tree_node(resolved, package_map, depth + 1, ancestry, lines)
154
162
  end
@@ -42,7 +42,7 @@ module Pray
42
42
  when "prayer"
43
43
  raise Error.unsupported("prayer requires init") unless arguments.shift == "init"
44
44
 
45
- [:prayer_init]
45
+ [:prayer_init, *parse_prayer_init_arguments(arguments)]
46
46
  when "repo"
47
47
  raise Error.unsupported("repo requires init") unless arguments.shift == "init"
48
48
 
@@ -77,6 +77,26 @@ module Pray
77
77
  end
78
78
  end
79
79
 
80
+ def parse_prayer_init_arguments(arguments)
81
+ name = nil
82
+ directory = nil
83
+ while (argument = arguments.shift)
84
+ if argument == "--path"
85
+ directory = arguments.shift
86
+ if directory.nil? || directory.start_with?("-")
87
+ raise Error.usage("--path requires a directory")
88
+ end
89
+ elsif argument.start_with?("-")
90
+ raise Error.unsupported("unexpected prayer argument: #{argument}")
91
+ elsif name.nil?
92
+ name = argument
93
+ else
94
+ raise Error.unsupported("unexpected prayer argument: #{argument}")
95
+ end
96
+ end
97
+ [name, directory]
98
+ end
99
+
80
100
  def extract_flag!(arguments, flag)
81
101
  found = arguments.include?(flag)
82
102
  arguments.reject! { |argument| argument == flag }