roast-ai 0.4.9 → 0.5.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 (194) 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/CLAUDE.md +2 -2
  8. data/CONTRIBUTING.md +2 -0
  9. data/Gemfile +18 -18
  10. data/Gemfile.lock +46 -57
  11. data/README.md +118 -1432
  12. data/README_LEGACY.md +1464 -0
  13. data/Rakefile +39 -4
  14. data/dev.yml +29 -0
  15. data/dsl/agent_sessions.rb +20 -0
  16. data/dsl/async_cogs.rb +49 -0
  17. data/dsl/async_cogs_complex.rb +67 -0
  18. data/dsl/call.rb +44 -0
  19. data/dsl/collect_from.rb +72 -0
  20. data/dsl/demo/Gemfile +4 -0
  21. data/dsl/demo/Gemfile.lock +120 -0
  22. data/dsl/demo/cogs/local.rb +15 -0
  23. data/dsl/demo/simple_external_cog.rb +17 -0
  24. data/dsl/json_output.rb +28 -0
  25. data/dsl/map.rb +55 -0
  26. data/dsl/map_reduce.rb +37 -0
  27. data/dsl/map_with_index.rb +49 -0
  28. data/dsl/next_break.rb +40 -0
  29. data/dsl/next_break_parallel.rb +44 -0
  30. data/dsl/outputs.rb +39 -0
  31. data/dsl/outputs_bang.rb +36 -0
  32. data/dsl/parallel_map.rb +37 -0
  33. data/dsl/plugin-gem-example/.gitignore +8 -0
  34. data/dsl/plugin-gem-example/Gemfile +13 -0
  35. data/dsl/plugin-gem-example/Gemfile.lock +178 -0
  36. data/dsl/plugin-gem-example/lib/other.rb +17 -0
  37. data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
  38. data/dsl/plugin-gem-example/lib/simple.rb +15 -0
  39. data/dsl/plugin-gem-example/lib/version.rb +10 -0
  40. data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
  41. data/dsl/prompts/simple_prompt.md.erb +3 -0
  42. data/dsl/prototype.rb +10 -4
  43. data/dsl/repeat_loop_results.rb +53 -0
  44. data/dsl/ruby_cog.rb +72 -0
  45. data/dsl/simple_agent.rb +18 -0
  46. data/dsl/simple_chat.rb +26 -0
  47. data/dsl/simple_repeat.rb +29 -0
  48. data/dsl/skip.rb +36 -0
  49. data/dsl/step_communication.rb +10 -5
  50. data/dsl/targets_and_params.rb +57 -0
  51. data/dsl/temperature.rb +17 -0
  52. data/dsl/temporary_directory.rb +22 -0
  53. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  54. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  55. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  56. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  57. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  58. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  59. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  60. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  61. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  62. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  63. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  64. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  65. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  66. data/dsl/tutorial/05_control_flow/README.md +156 -0
  67. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  68. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  69. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  70. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  71. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  72. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  73. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  74. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  75. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  76. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  77. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  78. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  79. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  80. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  81. data/dsl/tutorial/README.md +222 -0
  82. data/dsl/working_directory.rb +16 -0
  83. data/exe/roast +1 -1
  84. data/internal/documentation/architectural-notes.md +115 -0
  85. data/internal/documentation/doc-comments-external.md +686 -0
  86. data/internal/documentation/doc-comments-internal.md +342 -0
  87. data/internal/documentation/doc-comments.md +211 -0
  88. data/lib/roast/dsl/cog/config.rb +280 -4
  89. data/lib/roast/dsl/cog/input.rb +73 -0
  90. data/lib/roast/dsl/cog/output.rb +313 -0
  91. data/lib/roast/dsl/cog/registry.rb +71 -0
  92. data/lib/roast/dsl/cog/stack.rb +3 -2
  93. data/lib/roast/dsl/cog/store.rb +11 -8
  94. data/lib/roast/dsl/cog.rb +108 -31
  95. data/lib/roast/dsl/cog_input_context.rb +44 -0
  96. data/lib/roast/dsl/cog_input_manager.rb +156 -0
  97. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  98. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  99. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  100. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  103. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  104. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  105. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  106. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  107. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  108. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  109. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  110. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  111. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  112. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  113. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  114. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  115. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  116. data/lib/roast/dsl/cogs/agent.rb +75 -0
  117. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  118. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  119. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  120. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  121. data/lib/roast/dsl/cogs/chat.rb +81 -0
  122. data/lib/roast/dsl/cogs/cmd.rb +291 -27
  123. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  124. data/lib/roast/dsl/command_runner.rb +191 -0
  125. data/lib/roast/dsl/config_context.rb +2 -47
  126. data/lib/roast/dsl/config_manager.rb +143 -0
  127. data/lib/roast/dsl/control_flow.rb +41 -0
  128. data/lib/roast/dsl/execution_context.rb +9 -0
  129. data/lib/roast/dsl/execution_manager.rb +267 -0
  130. data/lib/roast/dsl/nil_assertions.rb +23 -0
  131. data/lib/roast/dsl/system_cog/params.rb +32 -0
  132. data/lib/roast/dsl/system_cog.rb +36 -0
  133. data/lib/roast/dsl/system_cogs/call.rb +162 -0
  134. data/lib/roast/dsl/system_cogs/map.rb +448 -0
  135. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  136. data/lib/roast/dsl/workflow.rb +123 -0
  137. data/lib/roast/dsl/workflow_context.rb +20 -0
  138. data/lib/roast/dsl/workflow_params.rb +24 -0
  139. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  140. data/lib/roast/tools/apply_diff.rb +1 -3
  141. data/lib/roast/tools/cmd.rb +4 -3
  142. data/lib/roast/tools/read_file.rb +1 -1
  143. data/lib/roast/tools/update_files.rb +1 -1
  144. data/lib/roast/tools/write_file.rb +1 -1
  145. data/lib/roast/version.rb +1 -1
  146. data/lib/roast/workflow/base_workflow.rb +4 -0
  147. data/lib/roast/workflow/step_loader.rb +14 -2
  148. data/lib/roast-ai.rb +4 -0
  149. data/lib/roast.rb +60 -22
  150. data/{roast.gemspec → roast-ai.gemspec} +10 -13
  151. data/sorbet/config +1 -0
  152. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  153. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  154. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  155. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  156. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  157. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  158. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  159. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  160. data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
  161. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  162. data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
  163. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  164. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1197 -0
  165. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +314 -2
  166. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +498 -0
  167. data/sorbet/tapioca/config.yml +6 -0
  168. data/sorbet/tapioca/require.rb +2 -0
  169. metadata +198 -34
  170. data/dsl/less_simple.rb +0 -112
  171. data/dsl/simple.rb +0 -8
  172. data/lib/roast/dsl/cog_execution_context.rb +0 -29
  173. data/lib/roast/dsl/cogs/graph.rb +0 -53
  174. data/lib/roast/dsl/cogs.rb +0 -65
  175. data/lib/roast/dsl/executor.rb +0 -82
  176. data/lib/roast/dsl/workflow_execution_context.rb +0 -47
  177. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  178. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  179. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  180. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  181. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  182. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  183. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  184. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  185. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  186. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  187. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  188. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  189. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  190. data/sorbet/rbi/gems/rack@2.2.18.rbi +0 -5659
  191. data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +0 -2170
  192. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  193. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
  194. data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +0 -11
@@ -0,0 +1,81 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
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.
23
+ class Chat < Cog
24
+ # The configuration object for this chat cog instance
25
+ #
26
+ #: Roast::DSL::Cogs::Chat::Config
27
+ attr_reader :config
28
+
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
41
+
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?
53
+ end
54
+ end
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}"
62
+ end
63
+
64
+ Output.new(Session.from_chat(chat), response.content)
65
+ end
66
+
67
+ private
68
+
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
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -5,49 +5,313 @@ module Roast
5
5
  module DSL
6
6
  module Cogs
7
7
  class Cmd < Cog
8
- class Output
9
- #: String?
10
- attr_reader :command_output
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
11
28
 
12
- #: String?
13
- attr_reader :err
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
14
44
 
15
- #: Process::Status?
16
- attr_reader :status
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_")
17
68
 
18
- #: (
19
- #| String? output,
20
- #| String? error,
21
- #| Process::Status? status
22
- #| ) -> void
23
- def initialize(output, error, status)
24
- @command_output = output
25
- @err = error
26
- @status = status
69
+ @values[:show_stdout] = true
27
70
  end
28
- end
29
71
 
30
- class Config < Cog::Config
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
+ #
31
79
  #: () -> void
32
- def print_all!
33
- @values[:print_all] = true
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
34
84
  end
35
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
+ #
36
92
  #: () -> bool
37
- def print_all?
38
- !!@values[:print_all]
93
+ def show_stdout?
94
+ !!@values[:show_stdout]
39
95
  end
40
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
41
150
  def display!
42
- print_all!
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
43
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!)
44
197
  end
45
198
 
46
- #: (String) -> Output
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.
203
+ class Input < Cog::Input
204
+ # The command to execute
205
+ #
206
+ #: String?
207
+ attr_accessor :command
208
+
209
+ # Arguments to pass to the command
210
+ #
211
+ #: Array[String]
212
+ attr_accessor :args
213
+
214
+ # Data to pass to command's standard input
215
+ #
216
+ #: String?
217
+ attr_accessor :stdin
218
+
219
+ #: () -> void
220
+ def initialize
221
+ super
222
+ @args = []
223
+ end
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
+ #
232
+ #: () -> void
233
+ def validate!
234
+ raise Cog::Input::InvalidInputError, "'command' is required" unless command.present?
235
+ end
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
+ #
246
+ #: (String | Array[untyped]) -> void
247
+ def coerce(input_return_value)
248
+ case input_return_value
249
+ when String
250
+ self.command = input_return_value
251
+ when Array
252
+ input_return_value.map!(&:to_s)
253
+ self.command = input_return_value.shift
254
+ self.args = input_return_value
255
+ end
256
+ end
257
+ end
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.
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
+ #
270
+ #: String
271
+ attr_reader :out
272
+
273
+ # The standard error (STDERR) from the command
274
+ #
275
+ #: String
276
+ attr_reader :err
277
+
278
+ # The exit status of the command process
279
+ #
280
+ #: Process::Status
281
+ attr_reader :status
282
+
283
+ #: ( String, String, Process::Status) -> void
284
+ def initialize(out, err, status)
285
+ super()
286
+ @out = out #: String
287
+ @err = err #: String
288
+ @status = status #: Process::Status
289
+ end
290
+
291
+ private
292
+
293
+ def raw_text
294
+ out
295
+ end
296
+ end
297
+
298
+ #: (Input) -> Output
47
299
  def execute(input)
48
- result = Output.new(*Roast::Helpers::CmdRunner.capture3(input))
49
- puts result.command_output if @config.print_all?
50
- result
300
+ config = @config #: as Config
301
+
302
+ stdout_handler = config.show_stdout? ? ->(line) { $stdout.print(line) } : nil
303
+ stderr_handler = config.show_stderr? ? ->(line) { $stderr.print(line) } : nil
304
+
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
+
314
+ Output.new(stdout, stderr, status)
51
315
  end
52
316
  end
53
317
  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