ai_refactor 0.4.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +165 -1
- data/README.md +145 -16
- data/examples/.gitignore +1 -0
- data/examples/ex1_convert_a_rspec_test_to_minitest.yml +7 -0
- data/examples/ex1_input_spec.rb +32 -0
- data/examples/rails_helper.rb +21 -0
- data/examples/test_helper.rb +14 -0
- data/exe/ai_refactor +96 -42
- data/lib/ai_refactor/cli.rb +60 -8
- data/lib/ai_refactor/command_file_parser.rb +27 -0
- data/lib/ai_refactor/context.rb +1 -1
- data/lib/ai_refactor/file_processor.rb +1 -1
- data/lib/ai_refactor/prompt.rb +4 -4
- data/lib/ai_refactor/refactors/base_refactor.rb +9 -7
- data/lib/ai_refactor/refactors/{generic.rb → custom.rb} +2 -2
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +5 -1
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +2 -2
- data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +2 -2
- data/lib/ai_refactor/refactors/ruby/refactor_ruby.md +10 -0
- data/lib/ai_refactor/refactors/ruby/refactor_ruby.rb +29 -0
- data/lib/ai_refactor/refactors/ruby/write_ruby.md +7 -0
- data/lib/ai_refactor/refactors/ruby/write_ruby.rb +33 -0
- data/lib/ai_refactor/run_configuration.rb +115 -0
- data/lib/ai_refactor/templated_path.rb +48 -0
- data/lib/ai_refactor/test_runners/minitest_runner.rb +1 -1
- data/lib/ai_refactor/version.rb +1 -1
- metadata +16 -4
data/exe/ai_refactor
CHANGED
|
@@ -8,77 +8,82 @@ require_relative "../lib/ai_refactor"
|
|
|
8
8
|
|
|
9
9
|
require "dotenv/load"
|
|
10
10
|
|
|
11
|
-
options = {}
|
|
12
|
-
|
|
13
11
|
supported_refactors = AIRefactor::Refactors.all
|
|
14
|
-
|
|
12
|
+
refactors_descriptions = AIRefactor::Refactors.descriptions
|
|
13
|
+
|
|
14
|
+
arguments = ARGV.dup
|
|
15
|
+
|
|
16
|
+
options_from_config_file = AIRefactor::Cli.load_options_from_config_file
|
|
17
|
+
arguments += options_from_config_file if options_from_config_file
|
|
18
|
+
|
|
19
|
+
run_config = AIRefactor::RunConfiguration.new
|
|
15
20
|
|
|
16
21
|
# General options for all refactor types
|
|
17
22
|
option_parser = OptionParser.new do |parser|
|
|
18
|
-
parser.banner = "Usage: ai_refactor
|
|
23
|
+
parser.banner = "Usage: ai_refactor REFACTOR_TYPE_OR_COMMAND_FILE INPUT_FILE_OR_DIR [options]\n\nWhere REFACTOR_TYPE_OR_COMMAND_FILE is either the path to a command YML file, or one of the refactor types to run: \n- #{refactors_descriptions.to_a.map { |refactor| refactor.join(": ") }.join("\n- ")}\n\n"
|
|
19
24
|
|
|
20
25
|
parser.on("-o", "--output [FILE]", String, "Write output to given file instead of stdout. If no path provided will overwrite input file (will prompt to overwrite existing files). Some refactor tasks will write out to a new file by default. This option will override the tasks default behaviour.") do |f|
|
|
21
|
-
|
|
26
|
+
run_config.output_file_path = f
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
parser.on("-O", "--output-template TEMPLATE", String, "Write outputs to files instead of stdout. The template is used to create the output name, where the it can have substitutions, '[FILE]', '[NAME]', '[DIR]', '[REFACTOR]' & '[EXT]'. Eg `[DIR]/[NAME]_[REFACTOR][EXT]` (will prompt to overwrite existing files)") do |t|
|
|
25
|
-
|
|
30
|
+
run_config.output_template_path = t
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
parser.on("-c", "--context CONTEXT_FILES", Array, "Specify one or more files to use as context for the AI. The contents of these files will be prepended to the prompt sent to the AI.") do |c|
|
|
29
|
-
|
|
34
|
+
run_config.context_file_paths = c
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
parser.on("-x", "--extra CONTEXT_TEXT", String, "Specify some text to be prepended to the prompt sent to the AI as extra information of note.") do |c|
|
|
33
|
-
|
|
38
|
+
run_config.context_text = c
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
parser.on("-r", "--review-prompt", "Show the prompt that will be sent to ChatGPT but do not actually call ChatGPT or make changes to files.") do
|
|
37
|
-
|
|
42
|
+
run_config.review_prompt = true
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
parser.on("-p", "--prompt PROMPT_FILE", String, "Specify path to a text file that contains the ChatGPT 'system' prompt.") do |f|
|
|
41
|
-
|
|
46
|
+
run_config.prompt_file_path = f
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
parser.on("-f", "--diffs", "Request AI generate diffs of changes rather than writing out the whole file.") do
|
|
45
|
-
|
|
50
|
+
run_config.diff = true
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
parser.on("-C", "--continue [MAX_MESSAGES]", Integer, "If ChatGPT stops generating due to the maximum token count being reached, continue to generate more messages, until a stop condition or MAX_MESSAGES. MAX_MESSAGES defaults to 3") do |c|
|
|
49
|
-
|
|
54
|
+
run_config.ai_max_attempts = c
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
parser.on("-m", "--model MODEL_NAME", String, "Specify a ChatGPT model to use (default gpt-4).") do |m|
|
|
53
|
-
|
|
58
|
+
run_config.ai_model = m
|
|
54
59
|
end
|
|
55
60
|
|
|
56
61
|
parser.on("--temperature TEMP", Float, "Specify the temperature parameter for ChatGPT (default 0.7).") do |p|
|
|
57
|
-
|
|
62
|
+
run_config.ai_temperature = p
|
|
58
63
|
end
|
|
59
64
|
|
|
60
65
|
parser.on("--max-tokens MAX_TOKENS", Integer, "Specify the max number of tokens of output ChatGPT can generate. Max will depend on the size of the prompt (default 1500)") do |m|
|
|
61
|
-
|
|
66
|
+
run_config.ai_max_tokens = m
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
parser.on("-t", "--timeout SECONDS", Integer, "Specify the max wait time for ChatGPT response.") do |m|
|
|
65
|
-
|
|
70
|
+
run_config.ai_timeout = m
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
parser.on("--overwrite ANSWER", "Always overwrite existing output files, 'y' for yes, 'n' for no, or 'a' for ask. Default to ask.") do |a|
|
|
69
|
-
|
|
74
|
+
run_config.overwrite = a
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
parser.on("-N", "--no", "Never overwrite existing output files, same as --overwrite=n.") do |a|
|
|
73
|
-
|
|
78
|
+
run_config.overwrite = "n"
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
parser.on("-v", "--verbose", "Show extra output and progress info") do
|
|
77
|
-
|
|
82
|
+
run_config.verbose = true
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
parser.on("-d", "--debug", "Show debugging output to help diagnose issues") do
|
|
81
|
-
|
|
86
|
+
run_config.debug = true
|
|
82
87
|
end
|
|
83
88
|
|
|
84
89
|
parser.on("-h", "--help", "Prints this help") do
|
|
@@ -109,45 +114,94 @@ option_parser = OptionParser.new do |parser|
|
|
|
109
114
|
refactorer.command_line_options.each do |option|
|
|
110
115
|
args = [option[:long], option[:type], option[:help]]
|
|
111
116
|
args.unshift(option[:short]) if option[:short]
|
|
117
|
+
AIRefactor::RunConfiguration.add_new_option(option[:key])
|
|
112
118
|
parser.on(*args) do |o|
|
|
113
|
-
|
|
119
|
+
run_config.send("#{option[:key]}=", o.nil? ? true : o)
|
|
114
120
|
end
|
|
115
121
|
end
|
|
116
122
|
end
|
|
117
123
|
end
|
|
118
124
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
def exit_with_option_error(message, option_parser = nil, logger = nil)
|
|
126
|
+
logger ? logger.error(message, bold: true) : puts(message)
|
|
127
|
+
puts option_parser if option_parser
|
|
128
|
+
exit false
|
|
129
|
+
end
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
home_config_file_path
|
|
131
|
+
def exit_with_error(message, logger = nil)
|
|
132
|
+
logger ? logger.error(message, bold: true) : puts(message)
|
|
133
|
+
exit false
|
|
129
134
|
end
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
|
|
136
|
+
# If no command was provided, prompt for one in interactive mode
|
|
137
|
+
if arguments.empty? || arguments.all? { |arg| arg.start_with?("-") && !(arg == "-h" || arg == "--help") }
|
|
138
|
+
interactive_log = AIRefactor::Logger.new
|
|
139
|
+
# For each option that is required but not provided, prompt for it
|
|
140
|
+
# Put the option in arguments to parse with option_parser
|
|
141
|
+
interactive_log.info "Interactive mode started. You can use tab to autocomplete:"
|
|
142
|
+
predefined_commands = AIRefactor::Refactors.names
|
|
143
|
+
|
|
144
|
+
interactive_log.info "Available refactors: #{predefined_commands.join(", ")}\n"
|
|
145
|
+
command = AIRefactor::Cli.request_input_with_autocomplete("Enter refactor name: ", predefined_commands)
|
|
146
|
+
exit_with_option_error("No refactor name provided.", option_parser) if command.nil? || command.empty?
|
|
147
|
+
initial = [command]
|
|
148
|
+
|
|
149
|
+
input_path = AIRefactor::Cli.request_file_inputs("Enter input file path: ", multiple: false)
|
|
150
|
+
exit_with_option_error("No input file path provided.", option_parser) if input_path.nil? || input_path.empty?
|
|
151
|
+
initial << input_path
|
|
152
|
+
|
|
153
|
+
arguments.prepend(*initial)
|
|
154
|
+
|
|
155
|
+
# Ask if template should be used - then prompt for it
|
|
156
|
+
|
|
157
|
+
output = AIRefactor::Cli.request_file_inputs("Enter output file path (blank for refactor default): ", multiple: false)
|
|
158
|
+
arguments.concat(["-o", " #{output}"]) unless output.nil? || output.empty?
|
|
159
|
+
|
|
160
|
+
context_text = AIRefactor::Cli.request_text_input("Enter extra text to add to prompt (blank for none): ")
|
|
161
|
+
arguments.concat(["-x", context_text]) unless context_text.nil? || context_text.empty?
|
|
162
|
+
|
|
163
|
+
context_files = AIRefactor::Cli.request_file_inputs("Enter extra context file path(s) (blank for none): ")
|
|
164
|
+
arguments.concat(["-c", context_files]) unless context_files.nil? || context_files.empty?
|
|
165
|
+
|
|
166
|
+
prompt_file = AIRefactor::Cli.request_file_inputs("Enter Prompt file path (blank for refactor default): ", multiple: false)
|
|
167
|
+
arguments.concat(["-p", prompt_file]) unless prompt_file.nil? || prompt_file.empty?
|
|
168
|
+
|
|
169
|
+
review = AIRefactor::Cli.request_switch("Dry-run (review prompt only)? (y/N) (blank for 'N'): ")
|
|
170
|
+
arguments << "-r" if review
|
|
134
171
|
end
|
|
135
172
|
|
|
136
|
-
|
|
173
|
+
File.write(".ai_refactor_history", arguments.join(" ") + "\n", mode: "a")
|
|
174
|
+
|
|
175
|
+
begin
|
|
176
|
+
option_parser.parse!(arguments)
|
|
177
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
|
178
|
+
exit_with_option_error($!, option_parser)
|
|
179
|
+
end
|
|
137
180
|
|
|
138
|
-
logger = AIRefactor::Logger.new(verbose:
|
|
181
|
+
logger = AIRefactor::Logger.new(verbose: run_config.verbose, debug: run_config.debug)
|
|
182
|
+
logger.info "Also loaded options from '.ai_refactor' file..." if options_from_config_file&.size&.positive?
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
184
|
+
command_or_file = arguments.shift
|
|
185
|
+
if AIRefactor::CommandFileParser.command_file?(command_or_file)
|
|
186
|
+
logger.info "Loading refactor command file '#{command_or_file}'..."
|
|
187
|
+
begin
|
|
188
|
+
run_config.set!(AIRefactor::CommandFileParser.new(command_or_file).parse)
|
|
189
|
+
rescue => e
|
|
190
|
+
exit_with_option_error(e.message, option_parser, logger)
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
logger.info "Requested to run refactor '#{command_or_file}'..."
|
|
194
|
+
run_config.refactor = command_or_file
|
|
142
195
|
end
|
|
143
196
|
|
|
144
|
-
|
|
197
|
+
run_config.input_file_paths = arguments
|
|
198
|
+
|
|
199
|
+
job = AIRefactor::Cli.new(run_config, logger: logger)
|
|
145
200
|
|
|
146
201
|
unless job.valid?
|
|
147
|
-
|
|
148
|
-
exit 1
|
|
202
|
+
exit_with_error("Refactor job failed or was not correctly configured. Did you specify the required inputs or options?.", logger)
|
|
149
203
|
end
|
|
150
204
|
|
|
151
205
|
unless job.run
|
|
152
|
-
exit
|
|
206
|
+
exit false
|
|
153
207
|
end
|
data/lib/ai_refactor/cli.rb
CHANGED
|
@@ -1,15 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "readline"
|
|
4
|
+
|
|
3
5
|
module AIRefactor
|
|
4
6
|
class Cli
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
class << self
|
|
8
|
+
def load_options_from_config_file
|
|
9
|
+
# Load config from ~/.ai_refactor or .ai_refactor
|
|
10
|
+
home_config_file_path = File.expand_path("~/.ai_refactor")
|
|
11
|
+
local_config_file_path = File.join(Dir.pwd, ".ai_refactor")
|
|
12
|
+
|
|
13
|
+
config_file_path = if File.exist?(local_config_file_path)
|
|
14
|
+
local_config_file_path
|
|
15
|
+
elsif File.exist?(home_config_file_path)
|
|
16
|
+
home_config_file_path
|
|
17
|
+
end
|
|
18
|
+
return unless config_file_path
|
|
19
|
+
|
|
20
|
+
config_string = File.read(config_file_path)
|
|
21
|
+
config_lines = config_string.split(/\n+/).reject { |s| s =~ /\A\s*#/ }.map(&:strip)
|
|
22
|
+
config_lines.flat_map(&:shellsplit)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def request_text_input(prompt)
|
|
26
|
+
puts prompt
|
|
27
|
+
gets.chomp
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def request_input_with_autocomplete(prompt, completion_list)
|
|
31
|
+
Readline.completion_append_character = nil
|
|
32
|
+
Readline.completion_proc = proc do |str|
|
|
33
|
+
completion_list.grep(/^#{Regexp.escape(str)}/)
|
|
34
|
+
end
|
|
35
|
+
Readline.readline(prompt, true)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def request_file_inputs(prompt, multiple: true)
|
|
39
|
+
Readline.completion_append_character = multiple ? " " : nil
|
|
40
|
+
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
|
41
|
+
|
|
42
|
+
paths = Readline.readline(prompt, true)
|
|
43
|
+
multiple ? paths.gsub(/[^\\] /, ",") : paths
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def request_switch(prompt)
|
|
47
|
+
(Readline.readline(prompt, true) =~ /^y/i) ? true : false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(configuration, logger:)
|
|
52
|
+
@configuration = configuration
|
|
9
53
|
@logger = logger
|
|
10
54
|
end
|
|
11
55
|
|
|
12
|
-
attr_reader :
|
|
56
|
+
attr_reader :configuration, :logger
|
|
57
|
+
|
|
58
|
+
def refactoring_type
|
|
59
|
+
configuration.refactor || raise(StandardError, "No refactor provided")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inputs
|
|
63
|
+
configuration.input_file_paths
|
|
64
|
+
end
|
|
13
65
|
|
|
14
66
|
def valid?
|
|
15
67
|
return false unless refactorer
|
|
@@ -23,7 +75,7 @@ module AIRefactor
|
|
|
23
75
|
OpenAI.configure do |config|
|
|
24
76
|
config.access_token = ENV.fetch("OPENAI_API_KEY")
|
|
25
77
|
config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID", nil)
|
|
26
|
-
config.request_timeout =
|
|
78
|
+
config.request_timeout = configuration.ai_timeout || 240
|
|
27
79
|
end
|
|
28
80
|
|
|
29
81
|
if refactorer.takes_input_files?
|
|
@@ -37,7 +89,7 @@ module AIRefactor
|
|
|
37
89
|
return_values = expanded_inputs.map do |file|
|
|
38
90
|
logger.info "Processing #{file}..."
|
|
39
91
|
|
|
40
|
-
refactor = refactorer.new(file,
|
|
92
|
+
refactor = refactorer.new(file, configuration, logger)
|
|
41
93
|
refactor_returned = refactor.run
|
|
42
94
|
failed = refactor_returned == false
|
|
43
95
|
if failed
|
|
@@ -63,7 +115,7 @@ module AIRefactor
|
|
|
63
115
|
name = refactorer.refactor_name
|
|
64
116
|
logger.info "AI Refactor - #{name} refactor\n"
|
|
65
117
|
logger.info "====================\n"
|
|
66
|
-
refactor = refactorer.new(nil,
|
|
118
|
+
refactor = refactorer.new(nil, configuration, logger)
|
|
67
119
|
refactor_returned = refactor.run
|
|
68
120
|
failed = refactor_returned == false
|
|
69
121
|
if failed
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module AIRefactor
|
|
6
|
+
class CommandFileParser
|
|
7
|
+
def self.command_file?(name)
|
|
8
|
+
name.match?(/\.ya?ml$/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse
|
|
16
|
+
raise StandardError, "Invalid command file: file does not exist" unless File.exist?(@path)
|
|
17
|
+
|
|
18
|
+
options = YAML.safe_load_file(@path, permitted_classes: [Symbol], symbolize_names: true, aliases: true)
|
|
19
|
+
|
|
20
|
+
unless options && options[:refactor]
|
|
21
|
+
raise StandardError, "Invalid command file format, a 'refactor' key is required"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
options
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ai_refactor/context.rb
CHANGED
|
@@ -22,7 +22,7 @@ module AIRefactor
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def process!
|
|
25
|
-
logger.debug("Processing #{@prompt.input_file_path} with prompt in #{
|
|
25
|
+
logger.debug("Processing #{@prompt.input_file_path} with prompt in #{options.prompt_file_path}")
|
|
26
26
|
logger.debug("Options: #{options.inspect}")
|
|
27
27
|
messages = @prompt.chat_messages
|
|
28
28
|
if options[:review_prompt]
|
data/lib/ai_refactor/prompt.rb
CHANGED
|
@@ -9,18 +9,18 @@ module AIRefactor
|
|
|
9
9
|
CONTEXT_MARKER = "__{{context}}__"
|
|
10
10
|
CONTENT_MARKER = "__{{content}}__"
|
|
11
11
|
|
|
12
|
-
attr_reader :input_file_path
|
|
12
|
+
attr_reader :input_file_path
|
|
13
13
|
|
|
14
|
-
def initialize(options:, logger:, context: nil, input_content: nil, input_path: nil, output_file_path: nil,
|
|
14
|
+
def initialize(options:, logger:, context: nil, input_content: nil, input_path: nil, output_file_path: nil, prompt: nil, prompt_header: nil, prompt_footer: nil)
|
|
15
15
|
@input_content = input_content
|
|
16
16
|
@input_file_path = input_path
|
|
17
17
|
@output_file_path = output_file_path
|
|
18
|
-
@prompt_file_path = prompt_file_path
|
|
19
18
|
@logger = logger
|
|
20
19
|
@header = prompt_header
|
|
21
20
|
@footer = prompt_footer
|
|
22
21
|
@diff = options[:diff]
|
|
23
22
|
@context = context
|
|
23
|
+
@prompt = prompt || raise(StandardError, "Prompt not provided")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def chat_messages
|
|
@@ -41,7 +41,7 @@ module AIRefactor
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def system_prompt_template
|
|
44
|
-
|
|
44
|
+
@prompt
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def system_prompt_footer
|
|
@@ -39,7 +39,7 @@ module AIRefactor
|
|
|
39
39
|
|
|
40
40
|
def file_processor
|
|
41
41
|
context = ::AIRefactor::Context.new(files: options[:context_file_paths], text: options[:context_text], logger: logger)
|
|
42
|
-
prompt = ::AIRefactor::Prompt.new(input_content: input_content, input_path: input_file, output_file_path: output_file_path,
|
|
42
|
+
prompt = ::AIRefactor::Prompt.new(input_content: input_content, input_path: input_file, output_file_path: output_file_path, prompt: prompt_input, context: context, logger: logger, options: options)
|
|
43
43
|
AIRefactor::FileProcessor.new(prompt: prompt, ai_client: ai_client, output_path: output_file_path, logger: logger, options: options)
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -102,7 +102,11 @@ module AIRefactor
|
|
|
102
102
|
false
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
def
|
|
105
|
+
def prompt_input
|
|
106
|
+
if options && options[:prompt]&.length&.positive?
|
|
107
|
+
return options[:prompt]
|
|
108
|
+
end
|
|
109
|
+
|
|
106
110
|
file = if options && options[:prompt_file_path]&.length&.positive?
|
|
107
111
|
options[:prompt_file_path]
|
|
108
112
|
else
|
|
@@ -112,6 +116,8 @@ module AIRefactor
|
|
|
112
116
|
file.tap do |prompt|
|
|
113
117
|
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
|
114
118
|
end
|
|
119
|
+
|
|
120
|
+
File.read(file)
|
|
115
121
|
end
|
|
116
122
|
|
|
117
123
|
def output_file_path
|
|
@@ -140,11 +146,7 @@ module AIRefactor
|
|
|
140
146
|
end
|
|
141
147
|
|
|
142
148
|
def output_file_path_from_template
|
|
143
|
-
path =
|
|
144
|
-
.gsub("[NAME]", File.basename(input_file, ".*"))
|
|
145
|
-
.gsub("[DIR]", File.dirname(input_file))
|
|
146
|
-
.gsub("[REFACTOR]", self.class.refactor_name)
|
|
147
|
-
.gsub("[EXT]", File.extname(input_file))
|
|
149
|
+
path = AIRefactor::TemplatedPath.new(input_file, refactor_name, output_template_path).generate
|
|
148
150
|
raise "Output template could not be used" unless path.length.positive?
|
|
149
151
|
path
|
|
150
152
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module AIRefactor
|
|
4
4
|
module Refactors
|
|
5
|
-
class
|
|
5
|
+
class Custom < BaseRefactor
|
|
6
6
|
def run
|
|
7
|
-
logger.verbose "
|
|
7
|
+
logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
|
8
8
|
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
|
9
9
|
|
|
10
10
|
begin
|
|
@@ -4,8 +4,12 @@ Test 100% of the code.
|
|
|
4
4
|
The path to the file to test is: __{{input_file_path}}__
|
|
5
5
|
The output file path is: __{{output_file_path}}__
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
__{{prompt_header}}__
|
|
8
8
|
|
|
9
9
|
__{{context}}__
|
|
10
10
|
|
|
11
|
+
Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
|
|
12
|
+
|
|
13
|
+
__{{prompt_footer}}__
|
|
14
|
+
|
|
11
15
|
The class to test is:
|
|
@@ -19,9 +19,9 @@ module AIRefactor
|
|
|
19
19
|
|
|
20
20
|
logger.verbose "Generated #{output_file_path} from #{input_file} ..." if output_content
|
|
21
21
|
|
|
22
|
-
minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template:
|
|
22
|
+
minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: options.minitest_run_command)
|
|
23
23
|
|
|
24
|
-
logger.verbose "Run generated test file #{output_file_path} (
|
|
24
|
+
logger.verbose "Run generated test file #{output_file_path} (`#{minitest_runner.command}`)..."
|
|
25
25
|
test_run = minitest_runner.run
|
|
26
26
|
|
|
27
27
|
if test_run.failed?
|
|
@@ -6,7 +6,7 @@ module AIRefactor
|
|
|
6
6
|
module Minitest
|
|
7
7
|
class RspecToMinitest < BaseRefactor
|
|
8
8
|
def run
|
|
9
|
-
spec_runner = AIRefactor::TestRunners::RSpecRunner.new(input_file)
|
|
9
|
+
spec_runner = AIRefactor::TestRunners::RSpecRunner.new(input_file, command_template: options.rspec_run_command)
|
|
10
10
|
logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
|
|
11
11
|
|
|
12
12
|
spec_run = spec_runner.run
|
|
@@ -32,7 +32,7 @@ module AIRefactor
|
|
|
32
32
|
|
|
33
33
|
logger.verbose "Converted #{input_file} to #{output_file_path}..." if result
|
|
34
34
|
|
|
35
|
-
minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path)
|
|
35
|
+
minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: options.minitest_run_command)
|
|
36
36
|
|
|
37
37
|
logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
|
|
38
38
|
test_run = minitest_runner.run
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AIRefactor
|
|
4
|
+
module Refactors
|
|
5
|
+
module Ruby
|
|
6
|
+
class RefactorRuby < Custom
|
|
7
|
+
def run
|
|
8
|
+
logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
|
9
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
output_content = process!(strip_ticks: true)
|
|
13
|
+
rescue => e
|
|
14
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
|
15
|
+
return false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
return false unless output_content
|
|
19
|
+
|
|
20
|
+
output_file_path ? true : output_content
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.description
|
|
24
|
+
"Generic refactor using user supplied prompt (assumes Ruby code generation)"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AIRefactor
|
|
4
|
+
module Refactors
|
|
5
|
+
module Ruby
|
|
6
|
+
class WriteRuby < Custom
|
|
7
|
+
def run
|
|
8
|
+
logger.verbose "Write some ruby code... (using user supplied prompt #{prompt_file_path})"
|
|
9
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
output_content = process!(strip_ticks: true)
|
|
13
|
+
rescue => e
|
|
14
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
|
15
|
+
return false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
return false unless output_content
|
|
19
|
+
|
|
20
|
+
output_file_path ? true : output_content
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.takes_input_files?
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.description
|
|
28
|
+
"User supplied prompt to write Ruby code"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|