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,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module LockfileSerialize
5
+ module_function
6
+
7
+ def lockfile_to_toml(lockfile)
8
+ serialize_lockfile_text(lockfile.canonicalized)
9
+ end
10
+
11
+ def serialize_lockfile_text(lockfile)
12
+ lines = [
13
+ "prayfile_lock = #{format_string(lockfile.prayfile_lock)}",
14
+ "spec = #{format_string(lockfile.spec)}",
15
+ "generated_by = #{format_string(lockfile.generated_by)}",
16
+ "manifest_hash = #{format_string(lockfile.manifest_hash)}"
17
+ ]
18
+ lines << "environment = #{format_string(lockfile.environment)}" if lockfile.environment
19
+ lines << ""
20
+
21
+ lockfile.source.each { |entry| append_section(lines, format_source(entry)) }
22
+ lockfile.package.each { |entry| append_section(lines, format_package(entry)) }
23
+ lockfile.target.each { |entry| append_section(lines, format_target(entry)) }
24
+ lockfile.managed_span.each { |entry| append_section(lines, format_managed_span(entry)) }
25
+
26
+ lines.pop while lines.last == ""
27
+ "#{lines.join("\n")}\n"
28
+ end
29
+
30
+ def append_section(lines, section_lines)
31
+ return if section_lines.empty?
32
+
33
+ lines.concat(section_lines, [""])
34
+ end
35
+
36
+ def format_source(entry)
37
+ lines = ["[[source]]"]
38
+ scalars = [
39
+ ["name", entry.name],
40
+ ["kind", entry.kind],
41
+ ["url", entry.url]
42
+ ]
43
+ scalars << ["revision", entry.revision] if entry.revision
44
+ scalars << ["host_key_fingerprint", entry.host_key_fingerprint] if entry.host_key_fingerprint
45
+ append_scalars(lines, scalars)
46
+ lines
47
+ end
48
+
49
+ def format_package(entry)
50
+ lines = ["[[package]]"]
51
+ scalars = [
52
+ ["name", entry.name],
53
+ ["version", entry.version]
54
+ ]
55
+ scalars << ["source", entry.source] unless entry.source.nil?
56
+ scalars.concat(
57
+ [
58
+ ["path", entry.path],
59
+ ["tree_hash", entry.tree_hash],
60
+ ["artifact_hash", entry.artifact_hash],
61
+ ["artifact", entry.artifact]
62
+ ]
63
+ )
64
+ append_scalars(lines, scalars)
65
+ lines << "exports = #{format_string_array(entry.exports)}"
66
+ lines << "dependencies = #{format_string_array(entry.dependencies)}"
67
+ lines << "signer_fingerprint = #{format_string(entry.signer_fingerprint)}" if entry.signer_fingerprint
68
+ lines
69
+ end
70
+
71
+ def format_target(entry)
72
+ lines = ["[[target]]"]
73
+ append_scalars(lines, [["name", entry.name]])
74
+ lines << "outputs = #{format_string_array(entry.outputs)}"
75
+ lines
76
+ end
77
+
78
+ def format_managed_span(entry)
79
+ lines = ["[[managed_span]]"]
80
+ append_scalars(
81
+ lines,
82
+ [
83
+ ["id", entry.id],
84
+ ["target", entry.target],
85
+ ["open_line", entry.open_line],
86
+ ["close_line", entry.close_line],
87
+ ["ideal_checksum", entry.ideal_checksum],
88
+ ["package", entry.package],
89
+ ["export", entry.export],
90
+ ["source_checksum", entry.source_checksum],
91
+ ["silenced", entry.silenced]
92
+ ]
93
+ )
94
+ lines
95
+ end
96
+
97
+ def append_scalars(lines, entries)
98
+ entries.each do |key, value|
99
+ lines << case value
100
+ when String
101
+ "#{key} = #{format_string(value)}"
102
+ when Integer
103
+ "#{key} = #{value}"
104
+ when TrueClass, FalseClass
105
+ "#{key} = #{value}"
106
+ else
107
+ raise ArgumentError, "unsupported lockfile scalar type for #{key}"
108
+ end
109
+ end
110
+ end
111
+
112
+ def format_string(value)
113
+ escaped = value.gsub("\\", "\\\\").gsub("\"", "\\\"")
114
+ "\"#{escaped}\""
115
+ end
116
+
117
+ def format_string_array(values)
118
+ return "[]" if values.empty?
119
+ return "[#{format_string(values.first)}]" if values.length == 1
120
+
121
+ items = values.map { |value| " #{format_string(value)}" }.join(",\n")
122
+ "[\n#{items},\n]"
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "manifest_json"
4
+ require_relative "manifest_formatter"
5
+ require_relative "manifest_parser_helpers"
6
+ require_relative "manifest_parser_blocks"
7
+ require_relative "manifest_parser"
8
+
9
+ module Pray
10
+ RenderPolicy = Struct.new(
11
+ :mode, :conflict, :churn, :header, :section_markers, :line_endings
12
+ ) do
13
+ def self.default
14
+ new(
15
+ mode: "managed",
16
+ conflict: "fail",
17
+ churn: "minimal",
18
+ header: true,
19
+ section_markers: true,
20
+ line_endings: "lf"
21
+ )
22
+ end
23
+ end
24
+
25
+ ManifestSource = Struct.new(:name, :kind, :url, :subdir, :rev, :tag)
26
+ ManifestTarget = Struct.new(
27
+ :name, :outputs, :skills, :commands, :rules, :max_bytes, :mode, :scoped, :entries
28
+ ) do
29
+ def initialize(
30
+ name:, outputs: [], skills: [], commands: [], rules: [], max_bytes: nil,
31
+ mode: "legacy", scoped: false, entries: []
32
+ )
33
+ super
34
+ end
35
+ end
36
+
37
+ ManifestPackage = Struct.new(
38
+ :name, :constraint, :source, :exports, :targets, :features, :groups, :optional,
39
+ :path, :git, :tag, :rev, :tarball, :oci, :file, :roles, :bound
40
+ ) do
41
+ def initialize(
42
+ name:, constraint: "*", source: nil, exports: [], targets: [], features: [], groups: [],
43
+ optional: false, path: nil, git: nil, tag: nil, rev: nil, tarball: nil, oci: nil,
44
+ file: nil, roles: [], bound: false
45
+ )
46
+ super
47
+ end
48
+ end
49
+
50
+ ManifestLocal = Struct.new(:path, :position, :optional, :bound) do
51
+ def initialize(path:, position: "after", optional: false, bound: false)
52
+ super
53
+ end
54
+ end
55
+
56
+ Manifest = Struct.new(
57
+ :prayfile_version, :sources, :targets, :packages, :local, :symbols, :render,
58
+ :deprecated_keywords,
59
+ keyword_init: true
60
+ ) do
61
+ def initialize(
62
+ prayfile_version: "",
63
+ sources: [],
64
+ targets: [],
65
+ packages: [],
66
+ local: [],
67
+ symbols: {},
68
+ render: RenderPolicy.default,
69
+ deprecated_keywords: []
70
+ )
71
+ super
72
+ end
73
+
74
+ def note_deprecated_keyword(keyword)
75
+ return unless %w[target output agent].include?(keyword)
76
+ self.deprecated_keywords ||= []
77
+ deprecated_keywords << keyword unless deprecated_keywords.include?(keyword)
78
+ end
79
+
80
+ def deprecation_warnings
81
+ replacements = {
82
+ "target" => "compose` / `tree",
83
+ "output" => "compose",
84
+ "agent" => "pray"
85
+ }
86
+ (deprecated_keywords || []).filter_map do |keyword|
87
+ replacement = replacements[keyword]
88
+ next unless replacement
89
+
90
+ "warning: `#{keyword}` is deprecated and will be removed in version 2; prefer `#{replacement}`"
91
+ end
92
+ end
93
+
94
+ def canonicalized
95
+ dup.tap do |copy|
96
+ copy.sources = sources.sort_by(&:name)
97
+ copy.targets = targets.sort_by(&:name)
98
+ copy.packages = packages.sort_by { |package| [package.name, package.source.to_s, package.constraint] }
99
+ copy.local = local.sort_by(&:path)
100
+ copy.deprecated_keywords = []
101
+ end
102
+ end
103
+
104
+ def manifest_hash
105
+ bytes = ManifestJson.encode_compact(canonicalized)
106
+ Hashing.sha256_prefixed(bytes)
107
+ end
108
+ end
109
+
110
+ module ManifestMethods
111
+ module_function
112
+
113
+ def read_manifest_text(manifest_path)
114
+ File.read(manifest_path)
115
+ rescue Errno::ENOENT
116
+ raise Error.manifest("missing #{manifest_path}; run pray init to create one")
117
+ end
118
+
119
+ def parse_manifest(text)
120
+ lines = Literal.prepare_parser_lines(text)
121
+ BlockParser.new(lines).parse_root
122
+ end
123
+ end
124
+
125
+ extend ManifestMethods
126
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ManifestMethods
5
+ module_function
6
+
7
+ def format_package_declaration(package)
8
+ parts = ["pray \"#{package.name}\""]
9
+ parts << "\"#{package.constraint}\"" unless package.constraint == "*"
10
+ parts << "path: \"#{package.path}\"" if package.path
11
+ parts << "source: \"#{package.source}\"" if package.source
12
+ parts << "git: \"#{package.git}\"" if package.git
13
+ parts << "tag: \"#{package.tag}\"" if package.tag
14
+ parts << "rev: \"#{package.rev}\"" if package.rev
15
+ parts << "tarball: \"#{package.tarball}\"" if package.tarball
16
+ parts << "oci: \"#{package.oci}\"" if package.oci
17
+ parts << "file: \"#{package.file}\"" if package.file
18
+ unless package.exports.empty?
19
+ parts << if package.exports.length == 1
20
+ "export: \"#{package.exports.first}\""
21
+ else
22
+ "exports: [#{format_string_keyword_list(package.exports)}]"
23
+ end
24
+ end
25
+ parts << "targets: [#{format_string_keyword_list(package.targets)}]" unless package.targets.empty?
26
+ parts << "features: [#{format_string_keyword_list(package.features)}]" unless package.features.empty?
27
+ parts << "optional: true" if package.optional
28
+ parts.join(", ")
29
+ end
30
+
31
+ def replace_package_declaration(text, package)
32
+ name = package.name
33
+ prefixes = [
34
+ "pray \"#{name}\"", "pray '#{name}'",
35
+ "use \"#{name}\"", "include \"#{name}\"",
36
+ "agent \"#{name}\"", "agent '#{name}'",
37
+ "package \"#{name}\"", "package '#{name}'"
38
+ ]
39
+ lines = text.lines.map(&:chomp)
40
+ index = lines.index { |line|
41
+ trimmed = line.lstrip
42
+ prefixes.any? { |prefix| trimmed.start_with?(prefix) }
43
+ }
44
+ raise Error.manifest("package #{name} not found in manifest") unless index
45
+
46
+ lines[index] = format_package_declaration(package)
47
+ output = lines.join("\n")
48
+ output += "\n" if text.end_with?("\n") && !output.end_with?("\n")
49
+ output
50
+ end
51
+
52
+ def format_string_keyword_list(values)
53
+ values.map { |value| "\"#{value}\"" }.join(", ")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ManifestJson
5
+ module_function
6
+
7
+ def encode_compact(manifest)
8
+ encode_object(manifest_fields(manifest))
9
+ end
10
+
11
+ def manifest_fields(manifest)
12
+ fields = {
13
+ "prayfile_version" => manifest.prayfile_version,
14
+ "sources" => manifest.sources.map { |source| source_fields(source) },
15
+ "targets" => manifest.targets.map { |target| target_fields(target) },
16
+ "packages" => manifest.packages.map { |package| package_fields(package) },
17
+ "local" => manifest.local.map { |entry| local_fields(entry) },
18
+ "render" => render_fields(manifest.render)
19
+ }
20
+ fields["symbols"] = manifest.symbols.sort.to_h unless manifest.symbols.empty?
21
+ fields
22
+ end
23
+
24
+ def source_fields(source)
25
+ fields = {
26
+ "name" => source.name,
27
+ "kind" => source.kind,
28
+ "url" => source.url
29
+ }
30
+ fields["subdir"] = source.subdir if source.subdir
31
+ fields["rev"] = source.rev if source.rev
32
+ fields["tag"] = source.tag if source.tag
33
+ fields
34
+ end
35
+
36
+ def target_fields(target)
37
+ {
38
+ "name" => target.name,
39
+ "outputs" => target.outputs,
40
+ "skills" => target.skills,
41
+ "commands" => target.commands,
42
+ "rules" => target.rules,
43
+ "max_bytes" => target.max_bytes,
44
+ "mode" => target.mode || "legacy",
45
+ "scoped" => target.scoped || false,
46
+ "entries" => (target.entries || []).map { |entry| entry_fields(entry) }
47
+ }
48
+ end
49
+
50
+ def entry_fields(entry)
51
+ if entry.kind == "package"
52
+ {"kind" => "package", "name" => entry.name}
53
+ else
54
+ {"kind" => "local", "path" => entry.path}
55
+ end
56
+ end
57
+
58
+ def package_fields(package)
59
+ fields = {
60
+ "name" => package.name,
61
+ "constraint" => package.constraint,
62
+ "source" => package.source,
63
+ "exports" => package.exports,
64
+ "targets" => package.targets,
65
+ "features" => package.features,
66
+ "groups" => package.groups,
67
+ "optional" => package.optional,
68
+ "path" => package.path,
69
+ "git" => package.git,
70
+ "tag" => package.tag,
71
+ "rev" => package.rev,
72
+ "tarball" => package.tarball,
73
+ "oci" => package.oci
74
+ }
75
+ fields["file"] = package.file if package.file
76
+ fields["roles"] = package.roles unless package.roles.nil? || package.roles.empty?
77
+ fields["bound"] = true if package.bound
78
+ fields
79
+ end
80
+
81
+ def local_fields(entry)
82
+ fields = {
83
+ "path" => entry.path,
84
+ "position" => entry.position,
85
+ "optional" => entry.optional
86
+ }
87
+ fields["bound"] = true if entry.bound
88
+ fields
89
+ end
90
+
91
+ def render_fields(render)
92
+ {
93
+ "mode" => render.mode,
94
+ "conflict" => render.conflict,
95
+ "churn" => render.churn,
96
+ "header" => render.header,
97
+ "section_markers" => render.section_markers,
98
+ "line_endings" => render.line_endings
99
+ }
100
+ end
101
+
102
+ def encode_object(hash)
103
+ entries = hash.map { |key, value| "#{encode_string(key)}:#{encode_value(value)}" }
104
+ "{#{entries.join(",")}}"
105
+ end
106
+
107
+ def encode_array(array)
108
+ "[#{array.map { |value| encode_value(value) }.join(",")}]"
109
+ end
110
+
111
+ def encode_value(value)
112
+ case value
113
+ when Hash then encode_object(value)
114
+ when Array then encode_array(value)
115
+ when String then encode_string(value)
116
+ when TrueClass, FalseClass then value.to_s
117
+ when NilClass then "null"
118
+ when Integer then value.to_s
119
+ else
120
+ raise Error.manifest("unsupported JSON value: #{value.inspect}")
121
+ end
122
+ end
123
+
124
+ def encode_string(text)
125
+ "\"#{text.gsub("\\", "\\\\").gsub("\"", "\\\"").gsub("\n", "\\n").gsub("\r", "\\r").gsub("\t", "\\t")}\""
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ManifestMethods
5
+ class BlockParser
6
+ include ParserHelpers
7
+ include ParserBlocks
8
+
9
+ def initialize(lines)
10
+ @lines = lines
11
+ @cursor = 0
12
+ @group_stack = []
13
+ @surface = StatementSurface::Reader.new
14
+ end
15
+
16
+ def parse_root
17
+ manifest = Manifest.new
18
+ while (statement = next_statement)
19
+ raise Error.parse("manifest", "unexpected 'end'") if statement == "end"
20
+
21
+ apply_statement(manifest, statement, false)
22
+ end
23
+ raise Error.manifest("missing prayfile version") if manifest.prayfile_version.empty?
24
+
25
+ manifest
26
+ end
27
+
28
+ def parse_nested(manifest, stop_on_end:)
29
+ while (statement = next_statement)
30
+ return if statement == "end" && stop_on_end
31
+ raise Error.parse("manifest", "unexpected 'end'") if statement == "end"
32
+
33
+ apply_statement(manifest, statement, true)
34
+ end
35
+ raise Error.parse("manifest", "missing 'end'") if stop_on_end
36
+ end
37
+
38
+ def apply_statement(manifest, statement, allow_target)
39
+ case statement
40
+ when /\Aprayfile (.+)\z/
41
+ manifest.prayfile_version = string_from_literal(Regexp.last_match(1))
42
+ when /\Asource (.+)\z/
43
+ manifest.sources << parse_source(Regexp.last_match(1))
44
+ when /\Atarget (.+)\z/
45
+ apply_legacy_target(manifest, statement, Regexp.last_match(1), allow_target)
46
+ when /\Agroup (.+)\z/
47
+ apply_group(manifest, Regexp.last_match(1))
48
+ when "pray do", "template do"
49
+ parse_symbols_block(manifest)
50
+ when /\A(?:compose|output) (.+)\z/
51
+ apply_compose_or_output(manifest, statement, Regexp.last_match(1))
52
+ when /\A(?:tree|folder|skills) (.+)\z/
53
+ apply_tree_or_folder(manifest, statement, Regexp.last_match(1))
54
+ when /\Afile (.+)\z/
55
+ parse_file_block(manifest, Regexp.last_match(1))
56
+ when /\Aagent (.+)\z/
57
+ apply_agent_package(manifest, Regexp.last_match(1))
58
+ when /\Apackage (.+)\z/
59
+ Destination.upsert_package(manifest, parse_package_with_groups(Regexp.last_match(1)))
60
+ when /\A(?:pray|use|include) (.+)\z/
61
+ apply_pray_statement(manifest, Regexp.last_match(1), nil)
62
+ when /\Alocal (.+)\z/
63
+ apply_unbound_local(manifest, Regexp.last_match(1))
64
+ when /\Arender (.+)\z/
65
+ manifest.render = parse_render_policy(Regexp.last_match(1))
66
+ else
67
+ raise Error.parse("manifest", "unrecognized statement: #{statement}")
68
+ end
69
+ end
70
+
71
+ def apply_legacy_target(manifest, statement, rest, allow_target)
72
+ raise Error.parse("manifest", "target must use a block") if !allow_target && !statement.end_with?(" do")
73
+
74
+ manifest.note_deprecated_keyword("target")
75
+ target, is_block = parse_target_header(rest)
76
+ manifest.targets << target
77
+ parse_target_block(manifest, manifest.targets.length - 1) if is_block
78
+ end
79
+
80
+ def apply_group(manifest, rest)
81
+ group_names, is_block = parse_group_header(rest)
82
+ raise Error.parse("manifest", "group must use a block") unless is_block
83
+ raise Error.parse("manifest", "nested group blocks are not supported") if @group_stack.any?
84
+
85
+ @group_stack.push(group_names)
86
+ parse_group_block(manifest)
87
+ @group_stack.pop
88
+ end
89
+
90
+ def apply_compose_or_output(manifest, statement, rest)
91
+ if statement.start_with?("output ")
92
+ unless statement.end_with?(" do")
93
+ raise Error.parse(
94
+ "manifest",
95
+ "top-level output must use a compose block (output \"path\" do ... end)"
96
+ )
97
+ end
98
+ manifest.note_deprecated_keyword("output")
99
+ end
100
+ parse_destination_block(manifest, rest, "compose")
101
+ end
102
+
103
+ def apply_tree_or_folder(manifest, statement, rest)
104
+ if statement.start_with?("folder ", "skills ") && !statement.end_with?(" do")
105
+ raise Error.parse("manifest", "top-level folder/skills must use a tree block")
106
+ end
107
+ parse_destination_block(manifest, rest, "tree")
108
+ end
109
+
110
+ def apply_agent_package(manifest, rest)
111
+ manifest.note_deprecated_keyword("agent")
112
+ Destination.upsert_package(manifest, parse_package_with_groups(rest))
113
+ end
114
+
115
+ def apply_unbound_local(manifest, rest)
116
+ local = parse_local_decl(rest)
117
+ local.bound = false
118
+ Destination.upsert_local(manifest, local)
119
+ end
120
+
121
+ def parse_package_with_groups(rest)
122
+ package = parse_package_decl(rest)
123
+ package.groups = @group_stack.last&.dup || []
124
+ package
125
+ end
126
+
127
+ def next_statement
128
+ pending = @surface.next
129
+ return pending if pending
130
+
131
+ while @cursor < @lines.length
132
+ statement = @lines[@cursor].strip
133
+ @cursor += 1
134
+ next if statement.empty?
135
+
136
+ while !statement.end_with?(" do") && statement != "end" && @cursor < @lines.length &&
137
+ (statement.rstrip.end_with?(",") || !Literal.is_balanced?(statement))
138
+ next_line = @lines[@cursor].strip
139
+ @cursor += 1
140
+ next if next_line.empty?
141
+
142
+ statement = "#{statement} #{next_line}"
143
+ end
144
+ @surface.push_raw(statement)
145
+ pending = @surface.next
146
+ return pending if pending
147
+ end
148
+ nil
149
+ end
150
+ end
151
+ end
152
+ end