roast-ai 0.4.10 → 0.5.1

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/docs/write-comments.md +36 -0
  3. data/.github/CODEOWNERS +1 -1
  4. data/.github/workflows/ci.yaml +10 -6
  5. data/.gitignore +0 -1
  6. data/.rubocop.yml +7 -1
  7. data/.ruby-version +1 -1
  8. data/CLAUDE.md +2 -2
  9. data/CONTRIBUTING.md +2 -0
  10. data/Gemfile +19 -18
  11. data/Gemfile.lock +35 -58
  12. data/README.md +118 -1432
  13. data/README_LEGACY.md +1464 -0
  14. data/Rakefile +39 -4
  15. data/dev.yml +29 -0
  16. data/dsl/agent_sessions.rb +20 -0
  17. data/dsl/async_cogs.rb +49 -0
  18. data/dsl/async_cogs_complex.rb +67 -0
  19. data/dsl/call.rb +44 -0
  20. data/dsl/collect_from.rb +72 -0
  21. data/dsl/json_output.rb +28 -0
  22. data/dsl/map.rb +55 -0
  23. data/dsl/map_reduce.rb +37 -0
  24. data/dsl/map_with_index.rb +49 -0
  25. data/dsl/next_break.rb +45 -0
  26. data/dsl/next_break_parallel.rb +44 -0
  27. data/dsl/outputs.rb +39 -0
  28. data/dsl/outputs_bang.rb +36 -0
  29. data/dsl/parallel_map.rb +37 -0
  30. data/dsl/prompts/simple_prompt.md.erb +3 -0
  31. data/dsl/prototype.rb +5 -7
  32. data/dsl/repeat_loop_results.rb +53 -0
  33. data/dsl/ruby_cog.rb +72 -0
  34. data/dsl/simple_agent.rb +18 -0
  35. data/dsl/simple_chat.rb +15 -1
  36. data/dsl/simple_repeat.rb +29 -0
  37. data/dsl/skip.rb +36 -0
  38. data/dsl/step_communication.rb +2 -3
  39. data/dsl/targets_and_params.rb +57 -0
  40. data/dsl/temperature.rb +17 -0
  41. data/dsl/temporary_directory.rb +22 -0
  42. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  43. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  44. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  45. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  46. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  47. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  48. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  49. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  50. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  51. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  52. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  53. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  54. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  55. data/dsl/tutorial/05_control_flow/README.md +156 -0
  56. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  57. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  58. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  59. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  60. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  61. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  62. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  63. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  64. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  65. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  66. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  67. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  68. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  69. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  70. data/dsl/tutorial/README.md +222 -0
  71. data/dsl/working_directory.rb +16 -0
  72. data/exe/roast +1 -1
  73. data/internal/documentation/architectural-notes.md +115 -0
  74. data/internal/documentation/doc-comments-external.md +686 -0
  75. data/internal/documentation/doc-comments-internal.md +342 -0
  76. data/internal/documentation/doc-comments.md +211 -0
  77. data/lib/roast/dsl/cog/config.rb +274 -3
  78. data/lib/roast/dsl/cog/input.rb +53 -10
  79. data/lib/roast/dsl/cog/output.rb +297 -8
  80. data/lib/roast/dsl/cog/registry.rb +35 -3
  81. data/lib/roast/dsl/cog/stack.rb +1 -1
  82. data/lib/roast/dsl/cog/store.rb +5 -5
  83. data/lib/roast/dsl/cog.rb +70 -14
  84. data/lib/roast/dsl/cog_input_context.rb +36 -1
  85. data/lib/roast/dsl/cog_input_manager.rb +116 -7
  86. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  87. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  88. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  89. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  90. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  91. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  92. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  93. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  94. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  95. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  96. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  97. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  98. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  99. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  100. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  103. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  104. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  105. data/lib/roast/dsl/cogs/agent.rb +75 -0
  106. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  107. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  108. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  109. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  110. data/lib/roast/dsl/cogs/chat.rb +59 -56
  111. data/lib/roast/dsl/cogs/cmd.rb +251 -61
  112. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  113. data/lib/roast/dsl/command_runner.rb +191 -0
  114. data/lib/roast/dsl/config_manager.rb +58 -11
  115. data/lib/roast/dsl/control_flow.rb +41 -0
  116. data/lib/roast/dsl/execution_manager.rb +162 -32
  117. data/lib/roast/dsl/nil_assertions.rb +23 -0
  118. data/lib/roast/dsl/system_cog/params.rb +32 -0
  119. data/lib/roast/dsl/system_cog.rb +36 -0
  120. data/lib/roast/dsl/system_cogs/call.rb +163 -0
  121. data/lib/roast/dsl/system_cogs/map.rb +454 -0
  122. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  123. data/lib/roast/dsl/workflow.rb +26 -16
  124. data/lib/roast/dsl/workflow_context.rb +20 -0
  125. data/lib/roast/dsl/workflow_params.rb +24 -0
  126. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  127. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  128. data/lib/roast/tools/apply_diff.rb +1 -3
  129. data/lib/roast/tools/cmd.rb +4 -3
  130. data/lib/roast/tools/read_file.rb +1 -1
  131. data/lib/roast/tools/update_files.rb +1 -1
  132. data/lib/roast/tools/write_file.rb +1 -1
  133. data/lib/roast/version.rb +1 -1
  134. data/lib/roast/workflow/base_workflow.rb +4 -0
  135. data/lib/roast/workflow/step_loader.rb +14 -2
  136. data/lib/roast-ai.rb +4 -0
  137. data/lib/roast.rb +58 -21
  138. data/{roast.gemspec → roast-ai.gemspec} +9 -13
  139. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  140. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  141. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  142. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  143. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  144. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  145. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  146. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  147. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  148. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  149. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
  150. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
  151. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
  152. data/sorbet/tapioca/config.yml +6 -0
  153. data/sorbet/tapioca/require.rb +2 -0
  154. metadata +157 -30
  155. data/dsl/less_simple.rb +0 -112
  156. data/dsl/scoped_executors.rb +0 -28
  157. data/dsl/simple.rb +0 -8
  158. data/lib/roast/dsl/cogs/execute.rb +0 -46
  159. data/lib/roast/dsl/cogs/graph.rb +0 -53
  160. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  161. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  162. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  163. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  164. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  165. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  166. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  167. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  168. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  169. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  170. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  171. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  172. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  173. data/sorbet/rbi/gems/rack@2.2.19.rbi +0 -5676
  174. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  175. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
@@ -4,73 +4,76 @@
4
4
  module Roast
5
5
  module DSL
6
6
  module Cogs
7
+ # Chat cog for pure LLM interaction
8
+ #
9
+ # The chat cog provides pure LLM interaction without local system access. While it
10
+ # cannot access local files or run local tools, it can still perform complex reasoning and
11
+ # access any cloud-based tools and MCP servers according to the capabilities of the model and
12
+ # the capabilities that may be provided to it by the LLM provider.
13
+ #
14
+ # Key characteristics:
15
+ # - No access to local filesystem (cannot read or write local files)
16
+ # - Cannot run local tools or commands
17
+ # - Can access cloud-based tools and MCP servers provided by the LLM provider
18
+ # - Performs request-response interactions
19
+ # - Does not currently maintain conversation state across invocations (not yet implemented)
20
+ # - Does not currently support automatic session resumption (not yet implemented)
21
+ #
22
+ # For tasks requiring local filesystem access or locally-configured tools, use the `agent` cog instead.
7
23
  class Chat < Cog
8
- class Input < Cog::Input
9
- #: String?
10
- attr_accessor :prompt
24
+ # The configuration object for this chat cog instance
25
+ #
26
+ #: Roast::DSL::Cogs::Chat::Config
27
+ attr_reader :config
11
28
 
12
- #: () -> void
13
- def initialize
14
- super
15
- @prompt = nil #: String?
16
- end
17
-
18
- #: () -> void
19
- def validate!
20
- raise Cog::Input::InvalidInputError, "'prompt' is required" unless prompt.present?
21
- end
29
+ # Execute the chat completion with the given input and return the output
30
+ #
31
+ #: (Input) -> Output
32
+ def execute(input)
33
+ chat = ruby_llm_context.chat(
34
+ model: config.valid_model,
35
+ provider: config.valid_provider!,
36
+ assume_model_exists: !config.verify_model_exists?,
37
+ )
38
+ input.valid_session&.apply!(chat)
39
+ chat = chat.with_temperature(config.valid_temperature) if config.valid_temperature
40
+ num_existing_messages = chat.messages.length
22
41
 
23
- #: (untyped) -> void
24
- def coerce!(input_return_value)
25
- if input_return_value.is_a?(String)
26
- self.prompt = input_return_value
42
+ response = chat.ask(input.valid_prompt!)
43
+ chat.messages[num_existing_messages..].each do |message|
44
+ case message.role
45
+ when :user
46
+ puts "[USER PROMPT] #{message.content}" if config.show_prompt?
47
+ when :assistant
48
+ puts "[LLM RESPONSE] #{message.content}" if config.show_response?
49
+ else
50
+ # No other message types are expected, but let's show them if they do appear
51
+ # but only the user has requested some form of output
52
+ puts "[UNKNOWN] #{message.content}" if config.show_prompt? || config.show_response?
27
53
  end
28
54
  end
29
- end
30
-
31
- class Output < Cog::Output
32
- #: String
33
- attr_reader :response
34
-
35
- #: (String response) -> void
36
- def initialize(response)
37
- super()
38
- @response = response
55
+ if config.show_stats?
56
+ temperature = chat.instance_variable_get(:@temperature)
57
+ puts "[LLM STATS]"
58
+ puts "\tModel: #{response.model_id}"
59
+ puts "\tTemperature: #{format("%0.2f", temperature)}" if temperature
60
+ puts "\tInput Tokens: #{response.input_tokens}"
61
+ puts "\tOutput Tokens: #{response.output_tokens}"
39
62
  end
40
- end
41
63
 
42
- class Config < Cog::Config
43
- #: () -> String?
44
- def openai_api_key
45
- @values[:openai_api_key] ||= ENV["OPENAI_API_KEY"]
46
- end
47
-
48
- #: () -> String?
49
- def openai_api_base_url
50
- @values[:openai_api_base_url] ||= ENV["OPENAI_API_BASE_URL"]
51
- end
64
+ Output.new(Session.from_chat(chat), response.content)
52
65
  end
53
66
 
54
- #: (Input) -> Output
55
- def execute(input)
56
- config = @config #: as Config
57
- RubyLLM.configure do |ruby_llm_config|
58
- ruby_llm_config.openai_api_key = config.openai_api_key
59
- ruby_llm_config.openai_api_base = config.openai_api_base_url
60
- end
61
-
62
- chat = RubyLLM.chat
63
- resp = chat.ask(input.prompt)
64
- puts "Model: #{resp.model_id}"
65
- puts "Role: #{resp.role}"
66
- puts "Input Tokens: #{resp.input_tokens}"
67
- puts "Output Tokens: #{resp.output_tokens}"
67
+ private
68
68
 
69
- chat.messages.each do |message|
70
- puts "[#{message.role.to_s.upcase}] #{message.content}"
69
+ # Get a RubyLLM context configured for this chat cog
70
+ #
71
+ #: () -> RubyLLM::Context
72
+ def ruby_llm_context
73
+ @ruby_llm_context ||= RubyLLM.context do |context|
74
+ context.openai_api_key = config.valid_api_key!
75
+ context.openai_api_base = config.valid_base_url
71
76
  end
72
-
73
- Output.new(resp.content)
74
77
  end
75
78
  end
76
79
  end
@@ -5,25 +5,244 @@ module Roast
5
5
  module DSL
6
6
  module Cogs
7
7
  class Cmd < Cog
8
+ # Configure the `cmd` cog
9
+ #
10
+ # See sorbet/rbi/shims/lib/roast/dsl/config_context.rbi for full class documentation.
11
+ class Config < Cog::Config
12
+ # Configure the cog to consider itself failed if the command returns a non-zero exit status
13
+ #
14
+ # Enabled by default. When enabled, a non-zero exit status will mark the cog as failed,
15
+ # which may also abort the workflow depending on the cog's `abort_on_failure` configuration.
16
+ #
17
+ # #### Inverse Methods
18
+ # - `no_fail_on_error!`
19
+ #
20
+ # #### See Also
21
+ # - `fail_on_error?`
22
+ # - `abort_on_failure!`
23
+ #
24
+ #: () -> void
25
+ def fail_on_error!
26
+ @values[:fail_on_error] = true
27
+ end
28
+
29
+ # Configure the cog __not__ to consider itself failed if the command returns a non-zero exit status
30
+ #
31
+ # When disabled, the cog will complete successfully regardless of the command's exit status.
32
+ # The exit status will still be available in the output for inspection.
33
+ #
34
+ # #### Inverse Methods
35
+ # - `fail_on_error!`
36
+ #
37
+ # #### See Also
38
+ # - `fail_on_error?`
39
+ #
40
+ #: () -> void
41
+ def no_fail_on_error!
42
+ @values[:fail_on_error] = false
43
+ end
44
+
45
+ # Check if the cog is configured to fail when the command returns a non-zero exit status
46
+ #
47
+ # #### See Also
48
+ # - `fail_on_error!`
49
+ # - `no_fail_on_error!`
50
+ #
51
+ #: () -> bool
52
+ def fail_on_error?
53
+ @values[:fail_on_error] != false
54
+ end
55
+
56
+ # Configure the cog to write STDOUT to the console
57
+ #
58
+ # Disabled by default.
59
+ #
60
+ # #### See Also
61
+ # - `no_show_stdout!`
62
+ # - `show_stdout?`
63
+ # - `display!`
64
+ #
65
+ #: () -> void
66
+ def show_stdout!
67
+ raise "⚠️ DEPRECATION: use #{__callee__.to_s.sub("print_", "show_")} instead of #{__callee__}" if __callee__.to_s.include?("print_")
68
+
69
+ @values[:show_stdout] = true
70
+ end
71
+
72
+ # Configure the cog __not__ to write STDOUT to the console
73
+ #
74
+ # #### See Also
75
+ # - `show_stdout!`
76
+ # - `show_stdout?`
77
+ # - `no_display!`
78
+ #
79
+ #: () -> void
80
+ def no_show_stdout!
81
+ raise "⚠️ DEPRECATION: use #{__callee__.to_s.sub("print_", "show_")} instead of #{__callee__}" if __callee__.to_s.include?("print_")
82
+
83
+ @values[:show_stdout] = false
84
+ end
85
+
86
+ # Check if the cog is configured to write STDOUT to the console
87
+ #
88
+ # #### See Also
89
+ # - `show_stdout!`
90
+ # - `no_show_stdout!`
91
+ #
92
+ #: () -> bool
93
+ def show_stdout?
94
+ !!@values[:show_stdout]
95
+ end
96
+
97
+ # Configure the cog to write STDERR to the console
98
+ #
99
+ # Disabled by default.
100
+ #
101
+ # #### See Also
102
+ # - `no_show_stderr!`
103
+ # - `show_stderr?`
104
+ # - `display!`
105
+ #
106
+ #: () -> void
107
+ def show_stderr!
108
+ raise "⚠️ DEPRECATION: use #{__callee__.to_s.sub("print_", "show_")} instead of #{__callee__}" if __callee__.to_s.include?("print_")
109
+
110
+ @values[:show_stderr] = true
111
+ end
112
+
113
+ # Configure the cog __not__ to write STDERR to the console
114
+ #
115
+ # #### See Also
116
+ # - `show_stderr!`
117
+ # - `show_stderr?`
118
+ # - `no_display!`
119
+ #
120
+ #: () -> void
121
+ def no_show_stderr!
122
+ raise "⚠️ DEPRECATION: use #{__callee__.to_s.sub("print_", "show_")} instead of #{__callee__}" if __callee__.to_s.include?("print_")
123
+
124
+ @values[:show_stderr] = false
125
+ end
126
+
127
+ # Check if the cog is configured to write STDERR to the console
128
+ #
129
+ # #### See Also
130
+ # - `show_stderr!`
131
+ # - `no_show_stderr!`
132
+ #
133
+ #: () -> bool
134
+ def show_stderr?
135
+ !!@values[:show_stderr]
136
+ end
137
+
138
+ # Configure the cog to write both STDOUT and STDERR to the console
139
+ #
140
+ # #### Alias Methods
141
+ # - `display!`
142
+ # - `print_all!`
143
+ #
144
+ # #### See Also
145
+ # - `no_display!`
146
+ # - `show_stdout!`
147
+ # - `show_stderr!`
148
+ #
149
+ #: () -> void
150
+ def display!
151
+ raise "⚠️ DEPRECATION: use display! instead of #{__callee__}" if __callee__.to_s.include?("print_")
152
+
153
+ @values[:show_stdout] = true
154
+ @values[:show_stderr] = true
155
+ end
156
+
157
+ # Configure the cog to write __no output__ to the console, neither STDOUT nor STDERR
158
+ #
159
+ # #### Alias Methods
160
+ # - `no_display!`
161
+ # - `print_none!`
162
+ # - `quiet!`
163
+ #
164
+ # #### See Also
165
+ # - `display!`
166
+ # - `no_show_stdout!`
167
+ # - `no_show_stderr!`
168
+ #
169
+ #: () -> void
170
+ def no_display!
171
+ raise "⚠️ DEPRECATION: use no_display! instead of #{__callee__}" if __callee__.to_s.include?("print_")
172
+
173
+ @values[:show_stdout] = false
174
+ @values[:show_stderr] = false
175
+ end
176
+
177
+ # Check if the cog is configured to display any output while running
178
+ #
179
+ # #### See Also
180
+ # - `display!`
181
+ # - `no_display!`
182
+ # - `show_stdout?`
183
+ # - `show_stderr?`
184
+ #
185
+ #: () -> bool
186
+ def display?
187
+ show_stdout? || show_stderr?
188
+ end
189
+
190
+ alias_method(:quiet!, :no_display!)
191
+ alias_method(:print_all!, :display!)
192
+ alias_method(:print_none!, :no_display!)
193
+ alias_method(:print_stdout!, :show_stdout!)
194
+ alias_method(:no_print_stdout!, :no_show_stdout!)
195
+ alias_method(:print_stderr!, :show_stderr!)
196
+ alias_method(:no_print_stderr!, :no_show_stderr!)
197
+ end
198
+
199
+ # Input specification for the cmd cog
200
+ #
201
+ # The cmd cog requires a command to execute, optionally with arguments.
202
+ # The command will be executed in the configured working directory.
8
203
  class Input < Cog::Input
204
+ # The command to execute
205
+ #
9
206
  #: String?
10
207
  attr_accessor :command
11
208
 
209
+ # Arguments to pass to the command
210
+ #
12
211
  #: Array[String]
13
212
  attr_accessor :args
14
213
 
214
+ # Data to pass to command's standard input
215
+ #
216
+ #: String?
217
+ attr_accessor :stdin
218
+
15
219
  #: () -> void
16
220
  def initialize
17
221
  super
18
- @command = nil
19
222
  @args = []
20
223
  end
21
224
 
225
+ # Validate that the input has all required parameters
226
+ #
227
+ # This method ensures that a command has been provided before the cmd cog executes.
228
+ #
229
+ # #### See Also
230
+ # - `coerce`
231
+ #
22
232
  #: () -> void
23
233
  def validate!
24
234
  raise Cog::Input::InvalidInputError, "'command' is required" unless command.present?
25
235
  end
26
236
 
237
+ # Coerce the input from the return value of the input block
238
+ #
239
+ # If the input block returns a String, it will be used as the command value.
240
+ # If the input block returns an Array, the first element will be used as the command
241
+ # and the remaining elements will be used as arguments.
242
+ #
243
+ # #### See Also
244
+ # - `validate!`
245
+ #
27
246
  #: (String | Array[untyped]) -> void
28
247
  def coerce(input_return_value)
29
248
  case input_return_value
@@ -37,13 +256,27 @@ module Roast
37
256
  end
38
257
  end
39
258
 
259
+ # Output from running the cmd cog
260
+ #
261
+ # Contains the standard output, standard error, and exit status from the executed command.
262
+ # Includes JSON and text parsing capabilities via `WithJson` and `WithText` modules.
40
263
  class Output < Cog::Output
264
+ include Cog::Output::WithJson
265
+ include Cog::Output::WithNumber
266
+ include Cog::Output::WithText
267
+
268
+ # The standard output (STDOUT) from the command
269
+ #
41
270
  #: String
42
271
  attr_reader :out
43
272
 
273
+ # The standard error (STDERR) from the command
274
+ #
44
275
  #: String
45
276
  attr_reader :err
46
277
 
278
+ # The exit status of the command process
279
+ #
47
280
  #: Process::Status
48
281
  attr_reader :status
49
282
 
@@ -54,77 +287,34 @@ module Roast
54
287
  @err = err #: String
55
288
  @status = status #: Process::Status
56
289
  end
57
- end
58
290
 
59
- class Config < Cog::Config
60
- #: () -> void
61
- def print_all!
62
- @values[:print_stdout] = true
63
- @values[:print_stderr] = true
64
- end
291
+ private
65
292
 
66
- #: () -> void
67
- def print_stdout!
68
- @values[:print_stdout] = true
69
- end
70
-
71
- #: () -> void
72
- def print_stderr!
73
- @values[:print_stderr] = true
74
- end
75
-
76
- #: () -> bool
77
- def print_stdout?
78
- !!@values[:print_stdout]
79
- end
80
-
81
- #: () -> bool
82
- def print_stderr?
83
- !!@values[:print_stderr]
84
- end
85
-
86
- #: () -> void
87
- def display!
88
- print_all!
293
+ def raw_text
294
+ out
89
295
  end
90
296
  end
91
297
 
92
298
  #: (Input) -> Output
93
299
  def execute(input)
94
300
  config = @config #: as Config
95
- result = T.unsafe(Roast::Helpers::CmdRunner).popen3(input.command, *input.args) do |stdin, stdout, stderr, wait_thread|
96
- stdin.close
97
- command_output = ""
98
- command_error = ""
99
-
100
- # Thread to read and accumulate stdout
101
- stdout_thread = Thread.new do
102
- stdout.each_line do |line|
103
- command_output += line
104
- $stdout.puts(line) if config.print_stdout?
105
- end
106
- rescue IOError => e
107
- Roast::Helpers::Logger.debug("IOError while reading stdout: #{e.message}")
108
- end
109
-
110
- # Thread to read and accumulate stderr
111
- stderr_thread = Thread.new do
112
- stderr.each_line do |line|
113
- command_error += line
114
- $stderr.puts(line) if config.print_stderr?
115
- end
116
- rescue IOError => e
117
- Roast::Helpers::Logger.debug("IOError while reading stderr: #{e.message}")
118
- end
119
301
 
120
- # Wait for threads to finish
121
- stdout_thread.join
122
- stderr_thread.join
302
+ stdout_handler = config.show_stdout? ? ->(line) { $stdout.print(line) } : nil
303
+ stderr_handler = config.show_stderr? ? ->(line) { $stderr.print(line) } : nil
123
304
 
124
- [command_output, command_error, wait_thread.value]
125
- end #: as [String, String, Process::Status]
305
+ stdout, stderr, status = CommandRunner #: as untyped
306
+ .execute(
307
+ [input.command] + input.args,
308
+ working_directory: config.valid_working_directory,
309
+ stdin_content: input.stdin,
310
+ stdout_handler: stdout_handler,
311
+ stderr_handler: stderr_handler,
312
+ )
313
+ if !status.success? && config.fail_on_error?
314
+ raise ControlFlow::FailCog, "Process exited with status code #{status.exitstatus}"
315
+ end
126
316
 
127
- Output.new(*result)
317
+ Output.new(stdout, stderr, status)
128
318
  end
129
319
  end
130
320
  end
@@ -0,0 +1,171 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module Cogs
7
+ class Ruby < Cog
8
+ class Config < Cog::Config; end
9
+
10
+ # Input specification for the ruby cog
11
+ #
12
+ # The ruby cog accepts any Ruby value from the input block, which will be directly
13
+ # passed through to the output without modification.
14
+ class Input < Cog::Input
15
+ # The value to pass through to the output
16
+ #
17
+ # This value will be directly returned as the `value` attribute on the cog's output object.
18
+ #
19
+ #: untyped
20
+ attr_accessor :value
21
+
22
+ # Validate that the input has all required parameters
23
+ #
24
+ # This method ensures that a value has been provided, either directly via the `value`
25
+ # attribute or through the input block (via `coerce`).
26
+ #
27
+ # #### See Also
28
+ # - `coerce`
29
+ #
30
+ #: () -> void
31
+ def validate!
32
+ raise Cog::Input::InvalidInputError if value.nil? && !coerce_ran?
33
+ end
34
+
35
+ # Coerce the input from the return value of the input block
36
+ #
37
+ # The return value from the input block will be used directly as the `value` attribute.
38
+ # This allows any Ruby object to be passed through the ruby cog.
39
+ #
40
+ # #### See Also
41
+ # - `validate!`
42
+ #
43
+ #: (untyped) -> void
44
+ def coerce(input_return_value)
45
+ super
46
+ @value = input_return_value
47
+ end
48
+ end
49
+
50
+ # Output from running the ruby cog
51
+ #
52
+ # Contains the value that was provided to the input block, passed through unchanged.
53
+ # This allows Ruby values to be used directly in workflow steps.
54
+ #
55
+ # The output provides convenient dynamic method dispatch with the following priority:
56
+ # 1. If the value object responds to a method, it delegates to that method
57
+ # 2. If the value is a Hash, methods correspond to hash keys
58
+ # 3. Hash values that are Procs can be called directly as methods
59
+ #
60
+ # Additional conveniences:
61
+ # - Use `[]` for direct hash key access when the value is a Hash
62
+ # - Use `call()` to invoke the value if it's a Proc, or to call a Proc stored in a Hash
63
+ class Output < Cog::Output
64
+ # The value passed through from the input
65
+ #
66
+ #: untyped
67
+ attr_reader :value
68
+
69
+ #: (untyped) -> void
70
+ def initialize(value)
71
+ super()
72
+ @value = value
73
+ end
74
+
75
+ # Access a hash key directly when the value is a Hash
76
+ #
77
+ # Provides direct bracket notation access to hash keys without going through
78
+ # method dispatch. This is useful when you need explicit hash key access.
79
+ #
80
+ # #### See Also
81
+ # - `call`
82
+ # - `method_missing`
83
+ #
84
+ #: (Symbol) -> untyped
85
+ def [](key)
86
+ value[key]
87
+ end
88
+
89
+ # Call the value as a Proc, or call a Proc stored in a Hash
90
+ #
91
+ # This method provides two calling patterns:
92
+ # - If the value is a Proc, calls it directly with the provided arguments
93
+ # - If the value is a Hash, expects the first argument to be a Symbol key, retrieves
94
+ # the Proc at that key, and calls it with the remaining arguments
95
+ #
96
+ # Raises `ArgumentError` if called on a Hash without a Symbol key.
97
+ # Raises `NoMethodError` if the Hash key doesn't contain a Proc.
98
+ #
99
+ # #### See Also
100
+ # - `[]`
101
+ # - `method_missing`
102
+ #
103
+ #: (*untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped
104
+ def call(*args, **kwargs, &blk)
105
+ return value.call(*args, **kwargs, &blk) if value.is_a?(Proc)
106
+
107
+ key = args.first
108
+ raise ArgumentError unless key.is_a?(Symbol)
109
+
110
+ proc = value[key]
111
+ raise NoMethodError, key unless proc.is_a?(Proc)
112
+
113
+ proc = proc #: as untyped
114
+ proc.call(*args, **kwargs, &blk)
115
+ end
116
+
117
+ # Handle dynamic method calls with intelligent dispatch
118
+ #
119
+ # This method implements a multi-level dispatch strategy:
120
+ #
121
+ # 1. **Value delegation**: If the value object responds to the method, delegates directly to it.
122
+ # This allows calling methods like `lines`, `length`, `upcase` on String values, or any
123
+ # method on the underlying object.
124
+ #
125
+ # 2. **Hash key access**: If the value is a Hash and contains the method name as a key,
126
+ # returns the value at that key. If the value is a Proc, calls it with the provided arguments.
127
+ #
128
+ # 3. **Fallback**: If neither condition is met, calls `super` to trigger standard Ruby behavior.
129
+ #
130
+ # #### See Also
131
+ # - `respond_to_missing?`
132
+ # - `[]`
133
+ # - `call`
134
+ #
135
+ #: (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped
136
+ def method_missing(name, *args, **kwargs, &blk)
137
+ return value.public_send(name, *args, **kwargs, &blk) if value.respond_to?(name, false)
138
+ return super unless value.is_a?(Hash) && value.key?(name)
139
+
140
+ if value[name].is_a?(Proc)
141
+ proc = value[name] #: as untyped
142
+ proc.call(*args, **kwargs, &blk)
143
+ else
144
+ value[name]
145
+ end
146
+ end
147
+
148
+ # Check if a dynamic method should respond
149
+ #
150
+ # Returns `true` if any of the following conditions are met:
151
+ # 1. The value object responds to the method
152
+ # 2. The value is a Hash and contains the method name as a key
153
+ # 3. The parent class would respond to the method
154
+ #
155
+ # #### See Also
156
+ # - `method_missing`
157
+ #
158
+ #: (Symbol | String, ?bool) -> bool
159
+ def respond_to_missing?(name, include_private = false)
160
+ value.respond_to?(name, false) || value.is_a?(Hash) && value.key?(name) || super
161
+ end
162
+ end
163
+
164
+ #: (Input) -> Output
165
+ def execute(input)
166
+ Output.new(input.value)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end