ai_refactor 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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
- descriptions = AIRefactor::Refactors.descriptions
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 REFACTOR_TYPE INPUT_FILE_OR_DIR [options]\n\nWhere REFACTOR_TYPE is one of: \n- #{descriptions.to_a.map { |refactor| refactor.join(": ") }.join("\n- ")}\n\n"
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
- options[:output_file_path] = f
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
- options[:output_template_path] = t
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
- options[:context_file_paths] = c
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
- options[:context_text] = c
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
- options[:review_prompt] = true
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
- options[:prompt_file_path] = f
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
- options[:diff] = true
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
- options[:ai_max_attempts] = c || 3
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
- options[:ai_model] = m
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
- options[:ai_temperature] = p
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
- options[:ai_max_tokens] = m
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
- options[:ai_timeout] = m
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
- options[:overwrite] = a
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
- options[:overwrite] = "n"
78
+ run_config.overwrite = "n"
74
79
  end
75
80
 
76
81
  parser.on("-v", "--verbose", "Show extra output and progress info") do
77
- options[:verbose] = true
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
- options[:debug] = true
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
- options[option[:key]] = o.nil? ? true : o
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
- # Load config from ~/.ai_refactor or .ai_refactor
120
- home_config_file_path = File.expand_path("~/.ai_refactor")
121
- local_config_file_path = File.join(Dir.pwd, ".ai_refactor")
122
-
123
- arguments = ARGV.dup
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
- config_file_path = if File.exist?(local_config_file_path)
126
- local_config_file_path
127
- elsif File.exist?(home_config_file_path)
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
- if config_file_path
131
- config_string = File.read(config_file_path)
132
- config_lines = config_string.split(/\n+/).reject { |s| s =~ /\A\s*#/ }
133
- arguments += config_lines.flat_map(&:shellsplit)
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
- option_parser.parse!(arguments)
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: options[:verbose], debug: options[:debug])
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
- if config_file_path
141
- logger.info "Loaded config from '#{config_file_path}'..."
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
- job = ::AIRefactor::Cli.new(refactoring_type: arguments.shift, inputs: arguments, options: options, logger: logger)
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
- puts option_parser.help
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 1
206
+ exit false
153
207
  end
@@ -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
- def initialize(refactoring_type:, inputs:, options:, logger:)
6
- @refactoring_type = refactoring_type
7
- @inputs = inputs
8
- @options = options
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 :refactoring_type, :inputs, :options, :logger
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 = options[:ai_timeout] || 240
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, options, logger)
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, options, logger)
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
@@ -14,7 +14,7 @@ module AIRefactor
14
14
  if @text.nil? || @text.empty?
15
15
  file_context
16
16
  else
17
- "Also note: #{@text}\n\n#{file_context}"
17
+ "\n#{file_context}\n\n#{@text}\n"
18
18
  end
19
19
  end
20
20
 
@@ -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 #{@prompt.prompt_file_path}")
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]
@@ -9,18 +9,18 @@ module AIRefactor
9
9
  CONTEXT_MARKER = "__{{context}}__"
10
10
  CONTENT_MARKER = "__{{content}}__"
11
11
 
12
- attr_reader :input_file_path, :prompt_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, prompt_file_path: nil, prompt_header: nil, prompt_footer: 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
- File.read(@prompt_file_path)
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, prompt_file_path: prompt_file_path, context: context, logger: logger, options: options)
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 prompt_file_path
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 = output_template_path.gsub("[FILE]", File.basename(input_file))
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 Generic < BaseRefactor
5
+ class Custom < BaseRefactor
6
6
  def run
7
- logger.verbose "Generic refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
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
- Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
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: "bundle exec ruby __FILE__")
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} (#{minitest_runner.command})..."
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,10 @@
1
+ __{{context}}__
2
+
3
+ __{{prompt_header}}__
4
+
5
+ The input file is: __{{input_file_path}}__
6
+ The output file path is: __{{output_file_path}}__
7
+
8
+ __{{content}}__
9
+
10
+ __{{prompt_footer}}__
@@ -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,7 @@
1
+ __{{context}}__
2
+
3
+ __{{prompt_header}}__
4
+
5
+ __{{content}}__
6
+
7
+ __{{prompt_footer}}__
@@ -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