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.
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