roast-ai 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +32 -0
- data/CLAUDE.md +52 -1
- data/Gemfile +3 -1
- data/Gemfile.lock +63 -16
- data/README.md +115 -5
- data/bin/roast +1 -1
- data/claude-swarm.yml +210 -0
- data/docs/AGENT_STEPS.md +264 -0
- data/examples/agent_workflow/README.md +75 -0
- data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
- data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
- data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
- data/examples/agent_workflow/workflow.yml +16 -0
- data/examples/available_tools_demo/README.md +42 -0
- data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
- data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
- data/examples/available_tools_demo/workflow.yml +32 -0
- data/examples/available_tools_demo/write_summary/prompt.md +6 -0
- data/examples/case_when/detect_language/prompt.md +2 -2
- data/examples/grading/README.md +71 -0
- data/examples/grading/run_coverage.rb +0 -2
- data/examples/iteration/analyze_complexity/prompt.md +2 -2
- data/examples/iteration/generate_recommendations/prompt.md +2 -2
- data/examples/iteration/implement_fix/prompt.md +2 -2
- data/examples/iteration/prioritize_issues/prompt.md +1 -1
- data/examples/iteration/prompts/analyze_file.md +2 -2
- data/examples/iteration/prompts/generate_summary.md +1 -1
- data/examples/iteration/prompts/update_report.md +3 -3
- data/examples/iteration/prompts/write_report.md +3 -3
- data/examples/iteration/read_file/prompt.md +2 -2
- data/examples/iteration/select_next_issue/prompt.md +2 -2
- data/examples/iteration/update_fix_count/prompt.md +4 -4
- data/examples/iteration/verify_fix/prompt.md +3 -3
- data/examples/mcp/README.md +3 -3
- data/examples/mcp/analyze_changes/prompt.md +1 -1
- data/examples/mcp/database_workflow.yml +1 -1
- data/examples/mcp/fetch_pr_context/prompt.md +1 -1
- data/examples/mcp/github_workflow.yml +1 -1
- data/examples/mcp/post_review/prompt.md +1 -1
- data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
- data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
- data/examples/user_input/README.md +90 -0
- data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
- data/examples/user_input/funny_name/workflow.yml +26 -0
- data/examples/user_input/generate_summary/prompt.md +11 -0
- data/examples/user_input/simple_input_demo/workflow.yml +35 -0
- data/examples/user_input/survey_workflow.yml +71 -0
- data/examples/user_input/welcome_message/prompt.md +3 -0
- data/examples/user_input/workflow.yml +73 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
- data/lib/roast/errors.rb +6 -4
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
- data/lib/roast/helpers/logger.rb +12 -35
- data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
- data/lib/roast/helpers/prompt_loader.rb +0 -2
- data/lib/roast/resources/api_resource.rb +0 -4
- data/lib/roast/resources/url_resource.rb +0 -3
- data/lib/roast/resources.rb +0 -8
- data/lib/roast/tools/ask_user.rb +0 -2
- data/lib/roast/tools/bash.rb +0 -3
- data/lib/roast/tools/cmd.rb +0 -3
- data/lib/roast/tools/coding_agent.rb +1 -8
- data/lib/roast/tools/grep.rb +0 -3
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
- data/lib/roast/tools/read_file.rb +0 -2
- data/lib/roast/tools/search_file.rb +0 -2
- data/lib/roast/tools/update_files.rb +0 -4
- data/lib/roast/tools/write_file.rb +0 -3
- data/lib/roast/tools.rb +0 -13
- data/lib/roast/value_objects/step_name.rb +14 -3
- data/lib/roast/value_objects/workflow_path.rb +0 -2
- data/lib/roast/value_objects.rb +4 -4
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +26 -0
- data/lib/roast/workflow/api_configuration.rb +0 -4
- data/lib/roast/workflow/base_iteration_step.rb +0 -4
- data/lib/roast/workflow/base_step.rb +54 -28
- data/lib/roast/workflow/base_workflow.rb +2 -21
- data/lib/roast/workflow/case_executor.rb +0 -1
- data/lib/roast/workflow/case_step.rb +0 -4
- data/lib/roast/workflow/command_executor.rb +0 -2
- data/lib/roast/workflow/conditional_executor.rb +0 -1
- data/lib/roast/workflow/conditional_step.rb +0 -4
- data/lib/roast/workflow/configuration.rb +3 -10
- data/lib/roast/workflow/configuration_loader.rb +0 -2
- data/lib/roast/workflow/configuration_parser.rb +1 -7
- data/lib/roast/workflow/dot_access_hash.rb +16 -1
- data/lib/roast/workflow/error_handler.rb +0 -3
- data/lib/roast/workflow/expression_evaluator.rb +0 -3
- data/lib/roast/workflow/file_state_repository.rb +0 -5
- data/lib/roast/workflow/input_executor.rb +41 -0
- data/lib/roast/workflow/input_step.rb +163 -0
- data/lib/roast/workflow/iteration_executor.rb +0 -2
- data/lib/roast/workflow/output_handler.rb +0 -2
- data/lib/roast/workflow/output_manager.rb +0 -2
- data/lib/roast/workflow/prompt_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +0 -3
- data/lib/roast/workflow/resource_resolver.rb +0 -3
- data/lib/roast/workflow/session_manager.rb +0 -3
- data/lib/roast/workflow/state_manager.rb +0 -2
- data/lib/roast/workflow/step_executor_coordinator.rb +34 -11
- data/lib/roast/workflow/step_executor_factory.rb +0 -5
- data/lib/roast/workflow/step_executor_registry.rb +1 -4
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
- data/lib/roast/workflow/step_factory.rb +56 -0
- data/lib/roast/workflow/step_loader.rb +30 -16
- data/lib/roast/workflow/step_orchestrator.rb +3 -2
- data/lib/roast/workflow/step_type_resolver.rb +28 -1
- data/lib/roast/workflow/validator.rb +0 -4
- data/lib/roast/workflow/workflow_executor.rb +0 -16
- data/lib/roast/workflow/workflow_initializer.rb +1 -8
- data/lib/roast/workflow/workflow_runner.rb +0 -7
- data/lib/roast/workflow.rb +0 -15
- data/lib/roast.rb +55 -10
- data/roast.gemspec +2 -1
- data/schema/workflow.json +46 -0
- metadata +44 -6
- data/lib/roast/helpers.rb +0 -12
@@ -1,37 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "erb"
|
4
|
-
require "forwardable"
|
5
|
-
require "roast/workflow/context_path_resolver"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
class BaseStep
|
10
|
-
|
11
|
-
|
12
|
-
attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to
|
6
|
+
attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to, :available_tools
|
13
7
|
attr_reader :workflow, :name, :context_path
|
14
8
|
|
15
|
-
|
16
|
-
|
17
|
-
def_delegator :workflow, :transcript
|
9
|
+
delegate :append_to_final_output, :transcript, to: :workflow
|
10
|
+
delegate_missing_to :workflow
|
18
11
|
|
19
12
|
# TODO: is this really the model we want to default to, and is this the right place to set it?
|
20
13
|
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
|
21
14
|
@workflow = workflow
|
22
15
|
@model = model
|
23
|
-
@name = name
|
16
|
+
@name = normalize_name(name)
|
24
17
|
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
25
18
|
@print_response = false
|
26
19
|
@json = false
|
27
20
|
@params = {}
|
28
21
|
@coerce_to = nil
|
22
|
+
@available_tools = nil
|
29
23
|
@resource = workflow.resource if workflow.respond_to?(:resource)
|
30
24
|
end
|
31
25
|
|
32
26
|
def call
|
33
27
|
prompt(read_sidecar_prompt)
|
34
|
-
result = chat_completion(print_response:, json:, params:)
|
28
|
+
result = chat_completion(print_response:, json:, params:, available_tools:)
|
35
29
|
|
36
30
|
# Apply coercion if configured
|
37
31
|
apply_coercion(result)
|
@@ -39,25 +33,17 @@ module Roast
|
|
39
33
|
|
40
34
|
protected
|
41
35
|
|
42
|
-
def chat_completion(print_response: nil, json: nil, params: nil)
|
36
|
+
def chat_completion(print_response: nil, json: nil, params: nil, available_tools: nil)
|
43
37
|
# Use instance variables as defaults if parameters are not provided
|
44
38
|
print_response = @print_response if print_response.nil?
|
45
39
|
json = @json if json.nil?
|
46
40
|
params = @params if params.nil?
|
41
|
+
available_tools = @available_tools if available_tools.nil?
|
47
42
|
|
48
|
-
workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:)
|
49
|
-
|
50
|
-
|
51
|
-
begin
|
52
|
-
if json
|
53
|
-
return nil if result.strip.empty? # Explicitly handle empty string
|
43
|
+
result = workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:, available_tools:)
|
44
|
+
process_output(result, print_response:)
|
54
45
|
|
55
|
-
|
56
|
-
end
|
57
|
-
rescue JSON::ParserError
|
58
|
-
# If JSON parsing fails, leave it as a string
|
59
|
-
end
|
60
|
-
end
|
46
|
+
result
|
61
47
|
end
|
62
48
|
|
63
49
|
def prompt(text)
|
@@ -79,8 +65,20 @@ module Roast
|
|
79
65
|
def process_output(response, print_response:)
|
80
66
|
output_path = File.join(context_path, "output.txt")
|
81
67
|
if File.exist?(output_path) && print_response
|
82
|
-
#
|
83
|
-
|
68
|
+
# Deep wrap the response for template access
|
69
|
+
template_response = deep_wrap_for_templates(response)
|
70
|
+
|
71
|
+
# Debug output
|
72
|
+
if template_response.is_a?(DotAccessHash) && template_response.recommendations&.is_a?(Array)
|
73
|
+
$stderr.puts "DEBUG: recommendations array has #{template_response.recommendations.size} items"
|
74
|
+
$stderr.puts "DEBUG: first item class: #{template_response.recommendations.first.class}" if template_response.recommendations.first
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a binding that includes the wrapped response
|
78
|
+
template_binding = binding
|
79
|
+
template_binding.local_variable_set(:response, template_response)
|
80
|
+
|
81
|
+
append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(template_binding))
|
84
82
|
elsif print_response
|
85
83
|
append_to_final_output(response)
|
86
84
|
end
|
@@ -88,6 +86,35 @@ module Roast
|
|
88
86
|
|
89
87
|
private
|
90
88
|
|
89
|
+
def normalize_name(name)
|
90
|
+
return name if name.is_a?(Roast::ValueObjects::StepName)
|
91
|
+
|
92
|
+
name_value = name || self.class.name.underscore.split("/").last
|
93
|
+
Roast::ValueObjects::StepName.new(name_value)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Deep wrap response for ERB templates
|
97
|
+
# This creates a new structure where:
|
98
|
+
# - Hashes are wrapped in DotAccessHash
|
99
|
+
# - Arrays are cloned with their Hash elements wrapped
|
100
|
+
def deep_wrap_for_templates(obj)
|
101
|
+
case obj
|
102
|
+
when Hash
|
103
|
+
# Convert the hash to a new hash with wrapped values
|
104
|
+
wrapped_hash = {}
|
105
|
+
obj.each do |key, value|
|
106
|
+
wrapped_hash[key] = deep_wrap_for_templates(value)
|
107
|
+
end
|
108
|
+
DotAccessHash.new(wrapped_hash)
|
109
|
+
when Array
|
110
|
+
# Create a new array with wrapped elements
|
111
|
+
# This allows the template to use dot notation on array elements
|
112
|
+
obj.map { |item| deep_wrap_for_templates(item) }
|
113
|
+
else
|
114
|
+
obj
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
91
118
|
def apply_coercion(result)
|
92
119
|
case @coerce_to
|
93
120
|
when :boolean
|
@@ -97,7 +124,6 @@ module Roast
|
|
97
124
|
!!result
|
98
125
|
when :llm_boolean
|
99
126
|
# Use LLM boolean coercer for natural language responses
|
100
|
-
require "roast/workflow/llm_boolean_coercer"
|
101
127
|
LlmBooleanCoercer.coerce(result)
|
102
128
|
when :iterable
|
103
129
|
# Ensure result is iterable
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "raix/chat_completion"
|
4
|
-
require "raix/function_dispatch"
|
5
|
-
|
6
|
-
require "roast/workflow/context_path_resolver"
|
7
|
-
require "roast/workflow/dot_access_hash"
|
8
|
-
require "roast/workflow/output_manager"
|
9
|
-
|
10
3
|
module Roast
|
11
4
|
module Workflow
|
12
5
|
class BaseWorkflow
|
@@ -29,6 +22,7 @@ module Roast
|
|
29
22
|
|
30
23
|
delegate :api_provider, :openai?, to: :workflow_configuration, allow_nil: true
|
31
24
|
delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
|
25
|
+
delegate_missing_to :output
|
32
26
|
|
33
27
|
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, workflow_configuration: nil, pre_processing_data: nil)
|
34
28
|
@file = file
|
@@ -81,7 +75,7 @@ module Roast
|
|
81
75
|
rescue Faraday::ResourceNotFound => e
|
82
76
|
execution_time = Time.now - start_time
|
83
77
|
message = e.response.dig(:body, "error", "message") || e.message
|
84
|
-
error = Roast::ResourceNotFoundError.new(message)
|
78
|
+
error = Roast::Errors::ResourceNotFoundError.new(message)
|
85
79
|
error.set_backtrace(e.backtrace)
|
86
80
|
log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
|
87
81
|
rescue => e
|
@@ -104,19 +98,6 @@ module Roast
|
|
104
98
|
# Expose output manager for state management
|
105
99
|
attr_reader :output_manager
|
106
100
|
|
107
|
-
# Allow direct access to output values without 'output.' prefix
|
108
|
-
def method_missing(method_name, *args, &block)
|
109
|
-
if output.respond_to?(method_name)
|
110
|
-
output.send(method_name, *args, &block)
|
111
|
-
else
|
112
|
-
super
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def respond_to_missing?(method_name, include_private = false)
|
117
|
-
output.respond_to?(method_name) || super
|
118
|
-
end
|
119
|
-
|
120
101
|
private
|
121
102
|
|
122
103
|
def log_and_raise_error(error, message, model, params, execution_time)
|
@@ -24,7 +24,6 @@ module Roast
|
|
24
24
|
raise WorkflowExecutor::ConfigurationError, "Missing 'when' clauses in case configuration" unless when_clauses
|
25
25
|
|
26
26
|
# Create and execute a CaseStep
|
27
|
-
require "roast/workflow/case_step" unless defined?(Roast::Workflow::CaseStep)
|
28
27
|
case_step = CaseStep.new(
|
29
28
|
@workflow,
|
30
29
|
config: case_config,
|
@@ -24,7 +24,6 @@ module Roast
|
|
24
24
|
raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
|
25
25
|
|
26
26
|
# Create and execute a ConditionalStep
|
27
|
-
require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
|
28
27
|
conditional_step = ConditionalStep.new(
|
29
28
|
@workflow,
|
30
29
|
config: conditional_config,
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/api_configuration"
|
4
|
-
require "roast/workflow/configuration_loader"
|
5
|
-
require "roast/workflow/resource_resolver"
|
6
|
-
require "roast/workflow/step_finder"
|
7
|
-
|
8
3
|
module Roast
|
9
4
|
module Workflow
|
10
5
|
# Encapsulates workflow configuration data and provides structured access
|
@@ -96,11 +91,9 @@ module Roast
|
|
96
91
|
attr_reader :api_configuration
|
97
92
|
|
98
93
|
def process_resource
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
@target = @resource.value if has_target?
|
103
|
-
end
|
94
|
+
@resource = ResourceResolver.resolve(@target, context_path)
|
95
|
+
# Update target with processed value for backward compatibility
|
96
|
+
@target = @resource.value if has_target?
|
104
97
|
end
|
105
98
|
end
|
106
99
|
end
|
@@ -1,17 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/configuration"
|
4
|
-
require "roast/workflow/workflow_initializer"
|
5
|
-
require "roast/workflow/workflow_runner"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
class ConfigurationParser
|
10
|
-
extend Forwardable
|
11
|
-
|
12
6
|
attr_reader :configuration, :options, :files, :current_workflow
|
13
7
|
|
14
|
-
|
8
|
+
delegate :output, to: :current_workflow
|
15
9
|
|
16
10
|
def initialize(workflow_path, files = [], options = {})
|
17
11
|
@configuration = Configuration.new(workflow_path, options)
|
@@ -9,7 +9,7 @@ module Roast
|
|
9
9
|
|
10
10
|
def [](key)
|
11
11
|
value = @hash[key.to_sym] || @hash[key.to_s]
|
12
|
-
|
12
|
+
wrap_value(value)
|
13
13
|
end
|
14
14
|
|
15
15
|
def []=(key, value)
|
@@ -193,6 +193,21 @@ module Roast
|
|
193
193
|
end
|
194
194
|
|
195
195
|
alias_method :member?, :has_key?
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def wrap_value(value)
|
200
|
+
case value
|
201
|
+
when Hash
|
202
|
+
DotAccessHash.new(value)
|
203
|
+
when Array
|
204
|
+
# Don't create a new array - return the original array
|
205
|
+
# Only wrap Hash elements within the array when needed
|
206
|
+
value
|
207
|
+
else
|
208
|
+
value
|
209
|
+
end
|
210
|
+
end
|
196
211
|
end
|
197
212
|
end
|
198
213
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Handles execution of input steps
|
6
|
+
class InputExecutor
|
7
|
+
def initialize(workflow, context_path, state_manager, workflow_executor = nil)
|
8
|
+
@workflow = workflow
|
9
|
+
@context_path = context_path
|
10
|
+
@state_manager = state_manager
|
11
|
+
@workflow_executor = workflow_executor
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute_input(input_config)
|
15
|
+
# Interpolate the prompt if workflow executor is available
|
16
|
+
if @workflow_executor && input_config["prompt"]
|
17
|
+
interpolated_config = input_config.dup
|
18
|
+
interpolated_config["prompt"] = @workflow_executor.interpolate(input_config["prompt"])
|
19
|
+
else
|
20
|
+
interpolated_config = input_config
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create and execute an InputStep
|
24
|
+
input_step = InputStep.new(
|
25
|
+
@workflow,
|
26
|
+
config: interpolated_config,
|
27
|
+
name: input_config["name"] || "input_#{Time.now.to_i}",
|
28
|
+
context_path: @context_path,
|
29
|
+
)
|
30
|
+
|
31
|
+
result = input_step.call
|
32
|
+
|
33
|
+
# Store in 'previous' for conditional checks
|
34
|
+
@workflow.output["previous"] = result
|
35
|
+
@state_manager.save_state("previous", result)
|
36
|
+
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Workflow
|
7
|
+
class InputStep < BaseStep
|
8
|
+
attr_reader :prompt_text, :type, :required, :default, :timeout, :options, :step_name
|
9
|
+
|
10
|
+
def initialize(workflow, config:, **kwargs)
|
11
|
+
super(workflow, **kwargs)
|
12
|
+
parse_config(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
# Get user input based on the configured type
|
17
|
+
result = case type
|
18
|
+
when "boolean"
|
19
|
+
prompt_boolean
|
20
|
+
when "choice"
|
21
|
+
prompt_choice
|
22
|
+
when "password"
|
23
|
+
prompt_password
|
24
|
+
else
|
25
|
+
prompt_text_input
|
26
|
+
end
|
27
|
+
|
28
|
+
# Store the result in workflow state if a name was provided
|
29
|
+
store_in_state(result) if step_name
|
30
|
+
|
31
|
+
result
|
32
|
+
rescue Timeout::Error
|
33
|
+
handle_timeout
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def parse_config(config)
|
39
|
+
@prompt_text = config["prompt"] || raise_config_error("Missing 'prompt' in input configuration")
|
40
|
+
@step_name = config["name"]
|
41
|
+
@type = config["type"] || "text"
|
42
|
+
@required = config.fetch("required", false)
|
43
|
+
@default = config["default"]
|
44
|
+
@timeout = config["timeout"]
|
45
|
+
@options = config["options"]
|
46
|
+
|
47
|
+
validate_config
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_config
|
51
|
+
if type == "choice" && options.nil?
|
52
|
+
raise_config_error("Missing 'options' for choice type input")
|
53
|
+
end
|
54
|
+
|
55
|
+
if type == "boolean" && default && ![true, false, "true", "false", "yes", "no"].include?(default)
|
56
|
+
raise_config_error("Invalid default value for boolean type: #{default}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def prompt_text_input
|
61
|
+
loop do
|
62
|
+
result = if timeout
|
63
|
+
with_timeout { ::CLI::UI.ask(prompt_text, default: default) }
|
64
|
+
else
|
65
|
+
::CLI::UI.ask(prompt_text, default: default)
|
66
|
+
end
|
67
|
+
|
68
|
+
if required && result.to_s.strip.empty?
|
69
|
+
puts ::CLI::UI.fmt("{{red:This field is required. Please provide a value.}}")
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
return result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def prompt_boolean
|
78
|
+
if timeout
|
79
|
+
with_timeout { ::CLI::UI.confirm(prompt_text, default: boolean_default) }
|
80
|
+
else
|
81
|
+
::CLI::UI.confirm(prompt_text, default: boolean_default)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def prompt_choice
|
86
|
+
if timeout
|
87
|
+
with_timeout { ::CLI::UI.ask(prompt_text, options: options, default: default) }
|
88
|
+
else
|
89
|
+
::CLI::UI.ask(prompt_text, options: options, default: default)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def prompt_password
|
94
|
+
require "io/console"
|
95
|
+
|
96
|
+
loop do
|
97
|
+
result = if timeout
|
98
|
+
with_timeout { prompt_password_with_echo_off }
|
99
|
+
else
|
100
|
+
prompt_password_with_echo_off
|
101
|
+
end
|
102
|
+
|
103
|
+
if required && result.to_s.strip.empty?
|
104
|
+
puts ::CLI::UI.fmt("{{red:This field is required. Please provide a value.}}")
|
105
|
+
next
|
106
|
+
end
|
107
|
+
|
108
|
+
return result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def prompt_password_with_echo_off
|
113
|
+
::CLI::UI.with_frame_color(:blue) do
|
114
|
+
print("🔒 #{prompt_text} ")
|
115
|
+
|
116
|
+
password = if $stdin.tty?
|
117
|
+
# Use noecho for TTY environments
|
118
|
+
$stdin.noecho { $stdin.gets }.chomp
|
119
|
+
else
|
120
|
+
# Fall back to regular input for non-TTY environments
|
121
|
+
warn("[WARNING] Password will be visible (not running in TTY)")
|
122
|
+
$stdin.gets.chomp
|
123
|
+
end
|
124
|
+
|
125
|
+
puts # Add newline after password input
|
126
|
+
password
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def boolean_default
|
131
|
+
case default
|
132
|
+
when true, "true", "yes"
|
133
|
+
true
|
134
|
+
when false, "false", "no"
|
135
|
+
false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def with_timeout(&block)
|
140
|
+
Timeout.timeout(timeout, &block)
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_timeout
|
144
|
+
puts ::CLI::UI.fmt("{{yellow:Input timed out after #{timeout} seconds}}")
|
145
|
+
|
146
|
+
if default
|
147
|
+
puts ::CLI::UI.fmt("{{yellow:Using default value: #{default}}}")
|
148
|
+
default
|
149
|
+
elsif required
|
150
|
+
raise_config_error("Required input timed out with no default value")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def store_in_state(value)
|
155
|
+
workflow.output[step_name] = value
|
156
|
+
end
|
157
|
+
|
158
|
+
def raise_config_error(message)
|
159
|
+
raise WorkflowExecutor::ConfigurationError, message
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -24,7 +24,6 @@ module Roast
|
|
24
24
|
raise WorkflowExecutor::ConfigurationError, "Missing 'until' condition in repeat configuration" unless until_condition
|
25
25
|
|
26
26
|
# Create and execute a RepeatStep
|
27
|
-
require "roast/workflow/repeat_step" unless defined?(RepeatStep)
|
28
27
|
repeat_step = RepeatStep.new(
|
29
28
|
@workflow,
|
30
29
|
steps: steps,
|
@@ -64,7 +63,6 @@ module Roast
|
|
64
63
|
raise WorkflowExecutor::ConfigurationError, "Missing 'steps' in each configuration" unless steps
|
65
64
|
|
66
65
|
# Create and execute an EachStep
|
67
|
-
require "roast/workflow/each_step" unless defined?(EachStep)
|
68
66
|
each_step = EachStep.new(
|
69
67
|
@workflow,
|
70
68
|
collection_expr: collection_expr,
|