roast-ai 0.1.0 → 0.1.2
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/.github/workflows/cla.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +3 -4
- data/README.md +419 -5
- data/Rakefile +1 -6
- data/docs/INSTRUMENTATION.md +202 -0
- data/examples/api_workflow/README.md +85 -0
- data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
- data/examples/api_workflow/generate_report/prompt.md +10 -0
- data/examples/api_workflow/prompt.md +10 -0
- data/examples/api_workflow/transform_data/prompt.md +10 -0
- data/examples/api_workflow/workflow.yml +30 -0
- data/examples/grading/format_result.rb +25 -9
- data/examples/grading/js_test_runner +31 -0
- data/examples/grading/rb_test_runner +19 -0
- data/examples/grading/read_dependencies/prompt.md +14 -0
- data/examples/grading/run_coverage.rb +2 -2
- data/examples/grading/workflow.yml +3 -12
- data/examples/instrumentation.rb +76 -0
- data/examples/rspec_to_minitest/README.md +68 -0
- data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
- data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
- data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
- data/examples/rspec_to_minitest/workflow.md +10 -0
- data/examples/rspec_to_minitest/workflow.yml +40 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
- data/lib/roast/helpers/prompt_loader.rb +2 -0
- data/lib/roast/resources/api_resource.rb +137 -0
- data/lib/roast/resources/base_resource.rb +47 -0
- data/lib/roast/resources/directory_resource.rb +40 -0
- data/lib/roast/resources/file_resource.rb +33 -0
- data/lib/roast/resources/none_resource.rb +29 -0
- data/lib/roast/resources/url_resource.rb +63 -0
- data/lib/roast/resources.rb +100 -0
- data/lib/roast/tools/coding_agent.rb +69 -0
- data/lib/roast/tools.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_step.rb +21 -17
- data/lib/roast/workflow/base_workflow.rb +69 -17
- data/lib/roast/workflow/configuration.rb +83 -8
- data/lib/roast/workflow/configuration_parser.rb +218 -3
- data/lib/roast/workflow/file_state_repository.rb +156 -0
- data/lib/roast/workflow/prompt_step.rb +16 -0
- data/lib/roast/workflow/session_manager.rb +82 -0
- data/lib/roast/workflow/state_repository.rb +21 -0
- data/lib/roast/workflow/workflow_executor.rb +99 -9
- data/lib/roast/workflow.rb +4 -0
- data/lib/roast.rb +2 -5
- data/roast.gemspec +1 -1
- data/schema/workflow.json +12 -0
- metadata +34 -6
- data/.rspec +0 -1
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "resources/base_resource"
|
4
|
+
require_relative "resources/file_resource"
|
5
|
+
require_relative "resources/directory_resource"
|
6
|
+
require_relative "resources/url_resource"
|
7
|
+
require_relative "resources/api_resource"
|
8
|
+
require_relative "resources/none_resource"
|
9
|
+
require "uri"
|
10
|
+
|
11
|
+
module Roast
|
12
|
+
# The Resources module contains classes for handling different types of resources
|
13
|
+
# that workflows can operate on. Each resource type implements a common interface.
|
14
|
+
module Resources
|
15
|
+
extend self
|
16
|
+
|
17
|
+
# Create the appropriate resource based on the target
|
18
|
+
# @param target [String] The target specified in the workflow
|
19
|
+
# @return [BaseResource] A resource object of the appropriate type
|
20
|
+
def for(target)
|
21
|
+
type = detect_type(target)
|
22
|
+
|
23
|
+
case type
|
24
|
+
when :file
|
25
|
+
FileResource.new(target)
|
26
|
+
when :directory
|
27
|
+
DirectoryResource.new(target)
|
28
|
+
when :url
|
29
|
+
UrlResource.new(target)
|
30
|
+
when :api
|
31
|
+
ApiResource.new(target)
|
32
|
+
when :command
|
33
|
+
CommandResource.new(target)
|
34
|
+
when :none
|
35
|
+
NoneResource.new(target)
|
36
|
+
else
|
37
|
+
BaseResource.new(target) # Default to base resource
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Determines the resource type from the target
|
42
|
+
# @param target [String] The target specified in the workflow
|
43
|
+
# @return [Symbol] :file, :directory, :url, :api, or :none
|
44
|
+
def detect_type(target)
|
45
|
+
return :none if target.nil? || target.strip.empty?
|
46
|
+
|
47
|
+
# Check for command syntax $(...)
|
48
|
+
if target.match?(/^\$\(.*\)$/)
|
49
|
+
return :command
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check for URLs
|
53
|
+
if target.start_with?("http://", "https://", "ftp://")
|
54
|
+
return :url
|
55
|
+
end
|
56
|
+
|
57
|
+
# Try to parse as URI to detect other URL schemes
|
58
|
+
begin
|
59
|
+
uri = URI.parse(target)
|
60
|
+
return :url if uri.scheme && uri.host
|
61
|
+
rescue URI::InvalidURIError
|
62
|
+
# Not a URL, continue with other checks
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check for directory
|
66
|
+
if Dir.exist?(target)
|
67
|
+
return :directory
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check for glob patterns (containing * or ?)
|
71
|
+
if target.include?("*") || target.include?("?")
|
72
|
+
matches = Dir.glob(target)
|
73
|
+
return :none if matches.empty?
|
74
|
+
# If the glob matches only directories, treat as directory type
|
75
|
+
return :directory if matches.all? { |path| Dir.exist?(path) }
|
76
|
+
|
77
|
+
# Otherwise treat as file type (could be mixed or all files)
|
78
|
+
return :file
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check for file
|
82
|
+
if File.exist?(target)
|
83
|
+
return :file
|
84
|
+
end
|
85
|
+
|
86
|
+
# Check for API configuration in Fetch API style format
|
87
|
+
begin
|
88
|
+
potential_config = JSON.parse(target)
|
89
|
+
if potential_config.is_a?(Hash) && potential_config.key?("url") && potential_config.key?("options")
|
90
|
+
return :api
|
91
|
+
end
|
92
|
+
rescue JSON::ParserError
|
93
|
+
# Not a JSON string, continue with other checks
|
94
|
+
end
|
95
|
+
|
96
|
+
# Default to file for anything else
|
97
|
+
:file
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/helpers/logger"
|
4
|
+
require "open3"
|
5
|
+
require "tempfile"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module Roast
|
9
|
+
module Tools
|
10
|
+
module CodingAgent
|
11
|
+
extend self
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def included(base)
|
15
|
+
base.class_eval do
|
16
|
+
function(
|
17
|
+
:coding_agent,
|
18
|
+
"AI-powered coding agent that runs Claude Code CLI with the given prompt",
|
19
|
+
prompt: { type: "string", description: "The prompt to send to Claude Code" },
|
20
|
+
) do |params|
|
21
|
+
Roast::Tools::CodingAgent.call(params[:prompt])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(prompt)
|
28
|
+
Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
|
29
|
+
run_claude_code(prompt)
|
30
|
+
rescue StandardError => e
|
31
|
+
"Error running CodingAgent: #{e.message}".tap do |error_message|
|
32
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
33
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def run_claude_code(prompt)
|
40
|
+
Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
|
41
|
+
|
42
|
+
# Create a temporary file with a unique name
|
43
|
+
timestamp = Time.now.to_i
|
44
|
+
random_id = SecureRandom.hex(8)
|
45
|
+
pid = Process.pid
|
46
|
+
temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
|
47
|
+
|
48
|
+
begin
|
49
|
+
# Write the prompt to the file
|
50
|
+
temp_file.write(prompt)
|
51
|
+
temp_file.close
|
52
|
+
|
53
|
+
# Run Claude Code CLI using the temp file as input
|
54
|
+
claude_code_command = ENV.fetch("CLAUDE_CODE_COMMAND", "claude -p")
|
55
|
+
stdout, stderr, status = Open3.capture3("cat #{temp_file.path} | #{claude_code_command}")
|
56
|
+
|
57
|
+
if status.success?
|
58
|
+
stdout
|
59
|
+
else
|
60
|
+
"Error running ClaudeCode: #{stderr}"
|
61
|
+
end
|
62
|
+
ensure
|
63
|
+
# Always clean up the temp file
|
64
|
+
temp_file.unlink
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/roast/tools.rb
CHANGED
data/lib/roast/version.rb
CHANGED
@@ -8,35 +8,34 @@ module Roast
|
|
8
8
|
class BaseStep
|
9
9
|
extend Forwardable
|
10
10
|
|
11
|
-
attr_accessor :model, :print_response, :
|
11
|
+
attr_accessor :model, :print_response, :auto_loop, :json, :params, :resource
|
12
12
|
attr_reader :workflow, :name, :context_path
|
13
13
|
|
14
14
|
def_delegator :workflow, :append_to_final_output
|
15
15
|
def_delegator :workflow, :chat_completion
|
16
16
|
def_delegator :workflow, :transcript
|
17
17
|
|
18
|
-
def initialize(workflow, model: "anthropic:claude-3-7-sonnet", name: nil, context_path: nil)
|
18
|
+
def initialize(workflow, model: "anthropic:claude-3-7-sonnet", name: nil, context_path: nil, auto_loop: true)
|
19
19
|
@workflow = workflow
|
20
20
|
@model = model
|
21
21
|
@name = name || self.class.name.underscore.split("/").last
|
22
22
|
@context_path = context_path || determine_context_path
|
23
23
|
@print_response = false
|
24
|
-
@
|
24
|
+
@auto_loop = auto_loop
|
25
25
|
@json = false
|
26
26
|
@params = {}
|
27
|
+
@resource = workflow.resource if workflow.respond_to?(:resource)
|
27
28
|
end
|
28
29
|
|
29
30
|
def call
|
30
31
|
prompt(read_sidecar_prompt)
|
31
|
-
chat_completion(print_response:,
|
32
|
+
chat_completion(print_response:, auto_loop:, json:, params:)
|
32
33
|
end
|
33
34
|
|
34
35
|
protected
|
35
36
|
|
36
|
-
def chat_completion(print_response: false,
|
37
|
-
workflow.chat_completion(openai: model, loop
|
38
|
-
append_to_final_output(response) if print_response
|
39
|
-
end.then do |response|
|
37
|
+
def chat_completion(print_response: false, auto_loop: true, json: false, params: {})
|
38
|
+
workflow.chat_completion(openai: model, loop: auto_loop, json:, params:).then do |response|
|
40
39
|
case response
|
41
40
|
in Array
|
42
41
|
response.map(&:presence).compact.join("\n")
|
@@ -44,7 +43,7 @@ module Roast
|
|
44
43
|
response
|
45
44
|
end
|
46
45
|
end.tap do |response|
|
47
|
-
|
46
|
+
process_output(response, print_response:)
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
@@ -74,19 +73,24 @@ module Roast
|
|
74
73
|
end
|
75
74
|
|
76
75
|
def read_sidecar_prompt
|
77
|
-
|
76
|
+
# For file resources, use the target path for prompt selection
|
77
|
+
# For other resource types, fall back to workflow.file
|
78
|
+
target_path = if resource&.type == :file
|
79
|
+
resource.target
|
80
|
+
else
|
81
|
+
workflow.file
|
82
|
+
end
|
83
|
+
|
84
|
+
Roast::Helpers::PromptLoader.load_prompt(self, target_path)
|
78
85
|
end
|
79
86
|
|
80
|
-
def
|
81
|
-
# look for a file named output.txt.erb in the context path
|
82
|
-
# if found, render it with the response
|
83
|
-
# if not found, just return the response
|
84
|
-
# TODO: this can be a lot more sophisticated
|
85
|
-
# incorporating different file types, etc.
|
87
|
+
def process_output(response, print_response:)
|
86
88
|
output_path = File.join(context_path, "output.txt")
|
87
|
-
if File.exist?(output_path)
|
89
|
+
if File.exist?(output_path) && print_response
|
88
90
|
# TODO: use the workflow binding or the step?
|
89
91
|
append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(binding))
|
92
|
+
elsif print_response
|
93
|
+
append_to_final_output(response)
|
90
94
|
end
|
91
95
|
end
|
92
96
|
end
|
@@ -2,42 +2,102 @@
|
|
2
2
|
|
3
3
|
require "raix/chat_completion"
|
4
4
|
require "raix/function_dispatch"
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/isolated_execution_state"
|
7
|
+
require "active_support/notifications"
|
8
|
+
require "active_support/core_ext/hash/indifferent_access"
|
5
9
|
|
6
10
|
module Roast
|
7
11
|
module Workflow
|
8
12
|
class BaseWorkflow
|
9
13
|
include Raix::ChatCompletion
|
10
14
|
|
15
|
+
attr_reader :output
|
11
16
|
attr_accessor :file,
|
12
17
|
:concise,
|
13
18
|
:output_file,
|
14
|
-
:subject_file,
|
15
19
|
:verbose,
|
16
20
|
:name,
|
17
21
|
:context_path,
|
18
|
-
:
|
22
|
+
:resource,
|
23
|
+
:session_name,
|
24
|
+
:session_timestamp,
|
25
|
+
:configuration
|
19
26
|
|
20
|
-
def initialize(file
|
27
|
+
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
|
21
28
|
@file = file
|
22
|
-
@subject_file = subject_file
|
23
29
|
@name = name || self.class.name.underscore.split("/").last
|
24
30
|
@context_path = context_path || determine_context_path
|
25
31
|
@final_output = []
|
26
|
-
@output =
|
32
|
+
@output = ActiveSupport::HashWithIndifferentAccess.new
|
33
|
+
@resource = resource || Roast::Resources.for(file)
|
34
|
+
@session_name = session_name || @name
|
35
|
+
@session_timestamp = nil
|
36
|
+
@configuration = configuration
|
27
37
|
transcript << { system: read_sidecar_prompt }
|
28
|
-
unless subject_file.blank?
|
29
|
-
transcript << { user: read_subject_file }
|
30
|
-
end
|
31
38
|
Roast::Tools.setup_interrupt_handler(transcript)
|
32
39
|
Roast::Tools.setup_exit_handler(self)
|
33
40
|
end
|
34
41
|
|
42
|
+
# Custom writer for output to ensure it's always a HashWithIndifferentAccess
|
43
|
+
def output=(value)
|
44
|
+
@output = if value.is_a?(ActiveSupport::HashWithIndifferentAccess)
|
45
|
+
value
|
46
|
+
else
|
47
|
+
ActiveSupport::HashWithIndifferentAccess.new(value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
35
51
|
def append_to_final_output(message)
|
36
52
|
@final_output << message
|
37
53
|
end
|
38
54
|
|
39
55
|
def final_output
|
40
|
-
@final_output.
|
56
|
+
return @final_output if @final_output.is_a?(String)
|
57
|
+
return "" if @final_output.nil?
|
58
|
+
|
59
|
+
# Handle array case (expected normal case)
|
60
|
+
if @final_output.respond_to?(:join)
|
61
|
+
@final_output.join("\n\n")
|
62
|
+
else
|
63
|
+
# Handle any other unexpected type by converting to string
|
64
|
+
@final_output.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Override chat_completion to add instrumentation
|
69
|
+
def chat_completion(**kwargs)
|
70
|
+
start_time = Time.now
|
71
|
+
model = kwargs[:openai] || "default"
|
72
|
+
|
73
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
|
74
|
+
model: model,
|
75
|
+
parameters: kwargs.except(:openai),
|
76
|
+
})
|
77
|
+
|
78
|
+
result = super(**kwargs)
|
79
|
+
execution_time = Time.now - start_time
|
80
|
+
|
81
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
|
82
|
+
success: true,
|
83
|
+
model: model,
|
84
|
+
parameters: kwargs.except(:openai),
|
85
|
+
execution_time: execution_time,
|
86
|
+
response_size: result.to_s.length,
|
87
|
+
})
|
88
|
+
|
89
|
+
result
|
90
|
+
rescue => e
|
91
|
+
execution_time = Time.now - start_time
|
92
|
+
|
93
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.error", {
|
94
|
+
error: e.class.name,
|
95
|
+
message: e.message,
|
96
|
+
model: model,
|
97
|
+
parameters: kwargs.except(:openai),
|
98
|
+
execution_time: execution_time,
|
99
|
+
})
|
100
|
+
raise
|
41
101
|
end
|
42
102
|
|
43
103
|
private
|
@@ -66,14 +126,6 @@ module Roast
|
|
66
126
|
def read_sidecar_prompt
|
67
127
|
Roast::Helpers::PromptLoader.load_prompt(self, file)
|
68
128
|
end
|
69
|
-
|
70
|
-
def read_subject_file
|
71
|
-
[
|
72
|
-
"# SUT (Subject Under Test)",
|
73
|
-
"# #{subject_file}",
|
74
|
-
File.read(subject_file),
|
75
|
-
].join("\n")
|
76
|
-
end
|
77
129
|
end
|
78
130
|
end
|
79
131
|
end
|
@@ -8,7 +8,7 @@ module Roast
|
|
8
8
|
# Encapsulates workflow configuration data and provides structured access
|
9
9
|
# to the configuration settings
|
10
10
|
class Configuration
|
11
|
-
attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs
|
11
|
+
attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :model, :resource
|
12
12
|
attr_accessor :target
|
13
13
|
|
14
14
|
def initialize(workflow_path, options = {})
|
@@ -30,6 +30,23 @@ module Roast
|
|
30
30
|
|
31
31
|
# Process the target command if it's a shell command
|
32
32
|
@target = process_target(@target) if has_target?
|
33
|
+
|
34
|
+
# Create the appropriate resource object for the target
|
35
|
+
if defined?(Roast::Resources)
|
36
|
+
@resource = if has_target?
|
37
|
+
Roast::Resources.for(@target)
|
38
|
+
else
|
39
|
+
Roast::Resources::NoneResource.new(nil)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Process API token if provided
|
44
|
+
if @config_hash["api_token"]
|
45
|
+
@api_token = process_shell_command(@config_hash["api_token"])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Extract default model if provided
|
49
|
+
@model = @config_hash["model"]
|
33
50
|
end
|
34
51
|
|
35
52
|
def context_path
|
@@ -48,8 +65,49 @@ module Roast
|
|
48
65
|
@config_hash[step_name] || {}
|
49
66
|
end
|
50
67
|
|
51
|
-
|
52
|
-
|
68
|
+
# Find the index of a step in the workflow steps array
|
69
|
+
# @param [Array] steps Optional - The steps array to search (defaults to self.steps)
|
70
|
+
# @param [String] target_step The name of the step to find
|
71
|
+
# @return [Integer, nil] The index of the step, or nil if not found
|
72
|
+
def find_step_index(steps_array = nil, target_step = nil)
|
73
|
+
# Handle different call patterns for backward compatibility
|
74
|
+
if steps_array.is_a?(String) && target_step.nil?
|
75
|
+
target_step = steps_array
|
76
|
+
steps_array = steps
|
77
|
+
elsif steps_array.is_a?(Array) && target_step.is_a?(String)
|
78
|
+
# This is the normal case - steps_array and target_step are provided
|
79
|
+
else
|
80
|
+
# Default to self.steps if just the target_step is provided
|
81
|
+
steps_array = steps
|
82
|
+
end
|
83
|
+
|
84
|
+
# First, try using the new more detailed search
|
85
|
+
steps_array.each_with_index do |step, index|
|
86
|
+
case step
|
87
|
+
when Hash
|
88
|
+
# Could be {name: command} or {name: {substeps}}
|
89
|
+
step_key = step.keys.first
|
90
|
+
return index if step_key == target_step
|
91
|
+
when Array
|
92
|
+
# This is a parallel step container, search inside it
|
93
|
+
found = step.any? do |substep|
|
94
|
+
case substep
|
95
|
+
when Hash
|
96
|
+
substep.keys.first == target_step
|
97
|
+
when String
|
98
|
+
substep == target_step
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
return index if found
|
104
|
+
when String
|
105
|
+
return index if step == target_step
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Fall back to the original method using extract_step_name
|
110
|
+
steps_array.each_with_index do |step, index|
|
53
111
|
step_name = extract_step_name(step)
|
54
112
|
if step_name.is_a?(Array)
|
55
113
|
# For arrays (parallel steps), check if target is in the array
|
@@ -58,6 +116,7 @@ module Roast
|
|
58
116
|
return index
|
59
117
|
end
|
60
118
|
end
|
119
|
+
|
61
120
|
nil
|
62
121
|
end
|
63
122
|
|
@@ -81,8 +140,8 @@ module Roast
|
|
81
140
|
|
82
141
|
private
|
83
142
|
|
84
|
-
def
|
85
|
-
# If it's a bash command with the
|
143
|
+
def process_shell_command(command)
|
144
|
+
# If it's a bash command with the $(command) syntax
|
86
145
|
if command =~ /^\$\((.*)\)$/
|
87
146
|
return Open3.capture2e({}, ::Regexp.last_match(1)).first.strip
|
88
147
|
end
|
@@ -92,13 +151,29 @@ module Roast
|
|
92
151
|
return Open3.capture2e({}, *command.split(" ")[1..-1]).first.strip
|
93
152
|
end
|
94
153
|
|
154
|
+
# Not a shell command, return as is
|
155
|
+
command
|
156
|
+
end
|
157
|
+
|
158
|
+
def process_target(command)
|
159
|
+
# Process shell command first
|
160
|
+
processed = process_shell_command(command)
|
161
|
+
|
95
162
|
# If it's a glob pattern, return the full paths of the files it matches
|
96
|
-
if
|
97
|
-
|
163
|
+
if processed.include?("*")
|
164
|
+
matched_files = Dir.glob(processed)
|
165
|
+
# If no files match, return the pattern itself
|
166
|
+
return processed if matched_files.empty?
|
167
|
+
|
168
|
+
return matched_files.map { |file| File.expand_path(file) }.join("\n")
|
98
169
|
end
|
99
170
|
|
171
|
+
# For tests, if the command was already processed as a shell command and is simple,
|
172
|
+
# don't expand the path to avoid breaking existing tests
|
173
|
+
return processed if command != processed && !processed.include?("/")
|
174
|
+
|
100
175
|
# assumed to be a direct file path(s)
|
101
|
-
File.expand_path(
|
176
|
+
File.expand_path(processed)
|
102
177
|
end
|
103
178
|
|
104
179
|
def extract_step_name(step)
|