ai_refactor 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +14 -5
- data/exe/ai_refactor +33 -16
- data/lib/ai_refactor/base_refactor.rb +70 -0
- data/lib/ai_refactor/file_processor.rb +10 -5
- data/lib/ai_refactor/refactors/generic.rb +96 -24
- data/lib/ai_refactor/refactors/minitest_to_rspec.rb +1 -1
- data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +10 -10
- data/lib/ai_refactor/version.rb +1 -1
- data/lib/ai_refactor.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa9421b9791dda3c02b930e63d0fa7bd3dc587d121a0b3748a0f9d8c649f98a3
|
4
|
+
data.tar.gz: a5cb385968939847ebb1ceb1d663b1926e410fce0571fa2ab7463440347c0f50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ff997f0533fe95da7f0361789d88b4a73ecd8fbd82a1f54179fbce3b6a3b466c40330ae615a00f850e549212fe2463ba60ca4467cc0d9f65bbd6679f326dff1
|
7
|
+
data.tar.gz: 5898b17ebaa96c24a89cae1ff741a53d3afbb7a6c4844f52a271ddfecc1ebc1923026324601cb98e56b5aed0d673bba5d583d388a7a936e4cc86b272e1858bd8
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,10 @@ This is based on the assumption that the LLM AIs are pretty good at identifying
|
|
9
9
|
|
10
10
|
## Available refactors
|
11
11
|
|
12
|
-
Currently
|
12
|
+
Currently available:
|
13
|
+
|
14
|
+
- `generic`
|
15
|
+
- `rspec_to_minitest_rails`
|
13
16
|
|
14
17
|
### `rspec_to_minitest_rails`
|
15
18
|
|
@@ -25,21 +28,27 @@ AI Refactor 1 files(s)/dir(s) '["spec/models/my_thing_spec.rb"]' with rspec_to_m
|
|
25
28
|
====================
|
26
29
|
Processing spec/models/my_thing_spec.rb...
|
27
30
|
[Run spec spec/models/my_thing_spec.rb... (bundle exec rspec spec/models/my_thing_spec.rb)]
|
28
|
-
Do you wish to overwrite test/models/
|
31
|
+
Do you wish to overwrite test/models/my_thing_test.rb? (y/n)
|
29
32
|
y
|
30
33
|
[Converting spec/models/my_thing_spec.rb...]
|
31
34
|
[Generate AI output. Generation attempts left: 3]
|
32
35
|
[OpenAI finished, with reason 'stop'...]
|
33
36
|
[Used tokens: 1869]
|
34
|
-
[Converted spec/models/my_thing_spec.rb to test/models/
|
35
|
-
[Run generated test file test/models/
|
36
|
-
[Done converting spec/models/my_thing_spec.rb to test/models/
|
37
|
+
[Converted spec/models/my_thing_spec.rb to test/models/my_thing_test.rb...]
|
38
|
+
[Run generated test file test/models/my_thing_test.rb (bundle exec rails test test/models/my_thing_test.rb)...]
|
39
|
+
[Done converting spec/models/my_thing_spec.rb to test/models/my_thing_test.rb...]
|
37
40
|
No differences found! Conversion worked!
|
38
41
|
Refactor succeeded on spec/models/my_thing_spec.rb
|
39
42
|
|
40
43
|
Done processing all files!
|
41
44
|
```
|
42
45
|
|
46
|
+
### `generic` (user supplied prompt)
|
47
|
+
|
48
|
+
Applies the refactor specified by prompting the AI with the user supplied prompt. You must supply a prompt file with the `-p` option.
|
49
|
+
|
50
|
+
The output is written to `stdout`.
|
51
|
+
|
43
52
|
## Installation
|
44
53
|
|
45
54
|
Install the gem and add to the application's Gemfile by executing:
|
data/exe/ai_refactor
CHANGED
@@ -14,7 +14,6 @@ supported_names = AIRefactor::Refactors.names
|
|
14
14
|
option_parser = OptionParser.new do |parser|
|
15
15
|
parser.banner = "Usage: ai_refactor REFACTOR_TYPE INPUT_FILE_OR_DIR [options]\n\nWhere REFACTOR_TYPE is one of: #{supported_names}\n\n"
|
16
16
|
|
17
|
-
# todo: support for a sort of generic process which uses a custom prompt file
|
18
17
|
parser.on("-p", "--prompt PROMPT_FILE", String, "Specify path to a text file that contains the ChatGPT 'system' prompt.") do |f|
|
19
18
|
options[:prompt_file_path] = f
|
20
19
|
end
|
@@ -27,11 +26,11 @@ option_parser = OptionParser.new do |parser|
|
|
27
26
|
options[:ai_model] = m
|
28
27
|
end
|
29
28
|
|
30
|
-
parser.on(
|
29
|
+
parser.on("--temperature TEMP", Float, "Specify the temperature parameter for ChatGPT (default 0.7).") do |p|
|
31
30
|
options[:ai_temperature] = p
|
32
31
|
end
|
33
32
|
|
34
|
-
parser.on(
|
33
|
+
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|
|
35
34
|
options[:ai_max_tokens] = m
|
36
35
|
end
|
37
36
|
|
@@ -47,18 +46,23 @@ option_parser = OptionParser.new do |parser|
|
|
47
46
|
options[:debug] = true
|
48
47
|
end
|
49
48
|
|
50
|
-
supported_refactors.each do |_name, refactorer|
|
51
|
-
refactorer.command_line_options.each do |option|
|
52
|
-
parser.on(option[:short], option[:long], option[:type], option[:help]) do |o|
|
53
|
-
options[option[:key]] = o
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
49
|
parser.on("-h", "--help", "Prints this help") do
|
59
50
|
puts parser
|
60
51
|
exit
|
61
52
|
end
|
53
|
+
|
54
|
+
parser.separator ""
|
55
|
+
|
56
|
+
supported_refactors.each do |name, refactorer|
|
57
|
+
parser.separator "For refactor type '#{name}':" if refactorer.command_line_options.size.positive?
|
58
|
+
refactorer.command_line_options.each do |option|
|
59
|
+
args = [option[:long], option[:type], option[:help]]
|
60
|
+
args.unshift(option[:short]) if option[:short]
|
61
|
+
parser.on(*args) do |o|
|
62
|
+
options[option[:key]] = o.nil? ? true : o
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
62
66
|
end
|
63
67
|
|
64
68
|
option_parser.parse!
|
@@ -88,15 +92,28 @@ end.flatten
|
|
88
92
|
logger.info "AI Refactor #{inputs.size} files(s)/dir(s) '#{input_file_path}' with #{refactorer.refactor_name} refactor\n"
|
89
93
|
logger.info "====================\n"
|
90
94
|
|
91
|
-
inputs.
|
95
|
+
return_values = inputs.map do |file|
|
92
96
|
logger.info "Processing #{file}..."
|
93
97
|
|
94
98
|
refactor = refactorer.new(file, options, logger)
|
95
|
-
|
96
|
-
|
97
|
-
|
99
|
+
refactor_returned = refactor.run
|
100
|
+
failed = refactor_returned == false
|
101
|
+
if failed
|
102
|
+
logger.warn "Refactor failed on #{file}\nFailed due to: #{refactor.failed_message}\n"
|
98
103
|
else
|
99
|
-
logger.
|
104
|
+
logger.success "Refactor succeeded on #{file}\n"
|
105
|
+
if refactor_returned.is_a?(String)
|
106
|
+
logger.info "Refactor #{file} output:\n\n#{refactor_returned}\n\n"
|
107
|
+
end
|
100
108
|
end
|
109
|
+
failed ? [file, refactor.failed_message] : true
|
110
|
+
end
|
111
|
+
|
112
|
+
if return_values.all?(true)
|
113
|
+
logger.success "All files processed successfully!"
|
114
|
+
else
|
115
|
+
files = return_values.select { |v| v != true }
|
116
|
+
logger.warn "Some files failed to process:\n#{files.map { |f| "#{f[0]} :\n > #{f[1]}" }.join("\n")}"
|
101
117
|
end
|
118
|
+
|
102
119
|
logger.info "Done processing all files!"
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
class BaseRefactor
|
5
|
+
attr_reader :input_file, :options, :logger
|
6
|
+
attr_writer :failed_message
|
7
|
+
|
8
|
+
def initialize(input_file, options, logger)
|
9
|
+
@input_file = input_file
|
10
|
+
@options = options
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def failed_message
|
19
|
+
@failed_message || "Reason not specified"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def can_overwrite_output_file?(output_path)
|
25
|
+
logger.info "Do you wish to overwrite #{output_path}? (y/n)"
|
26
|
+
answer = $stdin.gets.chomp
|
27
|
+
unless answer == "y" || answer == "Y"
|
28
|
+
logger.warn "Skipping #{input_file}..."
|
29
|
+
self.failed_message = "Skipped as output file already exists"
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def prompt_file_path
|
36
|
+
self.class.prompt_file_path
|
37
|
+
end
|
38
|
+
|
39
|
+
def ai_client
|
40
|
+
@ai_client ||= OpenAI::Client.new
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
def command_line_options
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
|
48
|
+
def refactor_name
|
49
|
+
name.split("::")
|
50
|
+
.last
|
51
|
+
.gsub(/::/, "/")
|
52
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
53
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
54
|
+
.tr("-", "_")
|
55
|
+
.downcase
|
56
|
+
end
|
57
|
+
|
58
|
+
def prompt_file_path
|
59
|
+
file = if options[:prompt_file_path]&.length&.positive?
|
60
|
+
options[:prompt_file_path]
|
61
|
+
else
|
62
|
+
File.join(File.dirname(File.expand_path(__FILE__)), "prompts", "#{refactor_name}.md")
|
63
|
+
end
|
64
|
+
file.tap do |prompt|
|
65
|
+
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -7,15 +7,16 @@ module AIRefactor
|
|
7
7
|
class FileProcessor
|
8
8
|
attr_reader :file_path, :output_path, :logger
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@file_path =
|
12
|
-
@output_path = output_path
|
10
|
+
def initialize(input_path:, prompt_file_path:, ai_client:, logger:, output_path: nil)
|
11
|
+
@file_path = input_path
|
13
12
|
@prompt_file_path = prompt_file_path
|
14
13
|
@ai_client = ai_client
|
15
14
|
@logger = logger
|
15
|
+
@output_path = output_path
|
16
16
|
end
|
17
17
|
|
18
18
|
def output_exists?
|
19
|
+
return false unless output_path
|
19
20
|
File.exist?(output_path)
|
20
21
|
end
|
21
22
|
|
@@ -29,9 +30,13 @@ module AIRefactor
|
|
29
30
|
]
|
30
31
|
content, finished_reason, usage = generate_next_message(messages, prompt, options, options[:ai_max_attempts] || 3)
|
31
32
|
|
32
|
-
if content && content.length > 0
|
33
|
+
content = if content && content.length > 0
|
33
34
|
processed = block_given? ? yield(content) : content
|
34
|
-
|
35
|
+
if output_path
|
36
|
+
File.write(output_path, processed)
|
37
|
+
logger.verbose "Wrote output to #{output_path}..."
|
38
|
+
end
|
39
|
+
processed
|
35
40
|
end
|
36
41
|
|
37
42
|
[content, finished_reason, usage]
|
@@ -2,42 +2,114 @@
|
|
2
2
|
|
3
3
|
module AIRefactor
|
4
4
|
module Refactors
|
5
|
-
class Generic
|
6
|
-
|
5
|
+
class Generic < BaseRefactor
|
6
|
+
def run
|
7
|
+
logger.verbose "Generic refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
8
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
processor = AIRefactor::FileProcessor.new(
|
11
|
+
input_path: input_file,
|
12
|
+
prompt_file_path: prompt_file_path,
|
13
|
+
ai_client: ai_client,
|
14
|
+
logger: logger,
|
15
|
+
output_path: output_file_path
|
16
|
+
)
|
13
17
|
|
14
|
-
|
15
|
-
|
18
|
+
if processor.output_exists?
|
19
|
+
return false unless can_overwrite_output_file?(output_file_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
logger.verbose "Converting #{input_file}..."
|
23
|
+
|
24
|
+
begin
|
25
|
+
output_content, finished_reason, usage = processor.process!(options)
|
26
|
+
rescue => e
|
27
|
+
logger.error "Request to OpenAI failed: #{e.message}"
|
28
|
+
logger.warn "Skipping #{input_file}..."
|
29
|
+
self.failed_message = "Request to OpenAI failed"
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
|
34
|
+
logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
|
35
|
+
|
36
|
+
if finished_reason == "length"
|
37
|
+
logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
|
38
|
+
end
|
39
|
+
|
40
|
+
if !output_content || output_content.length == 0
|
41
|
+
logger.warn "Skipping #{input_file}, no translated output..."
|
42
|
+
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
43
|
+
self.failed_message = "AI conversion failed, no output was generated"
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
output_file_path ? true : output_content
|
16
48
|
end
|
17
49
|
|
18
50
|
private
|
19
51
|
|
20
|
-
def
|
21
|
-
|
22
|
-
end
|
52
|
+
def output_file_path
|
53
|
+
return output_file_path_from_template if output_template_path
|
23
54
|
|
24
|
-
|
25
|
-
|
26
|
-
|
55
|
+
path = options[:output_file_path]
|
56
|
+
return unless path
|
57
|
+
|
58
|
+
if path == true
|
59
|
+
input_file
|
60
|
+
else
|
61
|
+
path
|
27
62
|
end
|
63
|
+
end
|
28
64
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
65
|
+
def output_template_path
|
66
|
+
options[:output_template_path]
|
67
|
+
end
|
68
|
+
|
69
|
+
def output_file_path_from_template
|
70
|
+
path = output_template_path.gsub("[FILE]", File.basename(input_file))
|
71
|
+
.gsub("[NAME]", File.basename(input_file, ".*"))
|
72
|
+
.gsub("[DIR]", File.dirname(input_file))
|
73
|
+
.gsub("[REFACTOR]", self.class.refactor_name)
|
74
|
+
.gsub("[EXT]", File.extname(input_file))
|
75
|
+
raise "Output template could not be used" unless path.length.positive?
|
76
|
+
path
|
77
|
+
end
|
78
|
+
|
79
|
+
def prompt_file_path
|
80
|
+
specified_prompt_path = options[:prompt_file_path]
|
81
|
+
if specified_prompt_path&.length&.positive?
|
82
|
+
if File.exist?(specified_prompt_path)
|
83
|
+
return specified_prompt_path
|
84
|
+
else
|
85
|
+
logger.error "No prompt file '#{specified_prompt_path}' found"
|
86
|
+
end
|
87
|
+
else
|
88
|
+
logger.error "No prompt file was specified!"
|
37
89
|
end
|
90
|
+
exit 1
|
91
|
+
end
|
38
92
|
|
93
|
+
class << self
|
39
94
|
def prompt_file_path
|
40
|
-
|
95
|
+
raise "Generic refactor requires prompt file to be user specified."
|
96
|
+
end
|
97
|
+
|
98
|
+
def command_line_options
|
99
|
+
[
|
100
|
+
{
|
101
|
+
key: :output_file_path,
|
102
|
+
long: "--output [FILE]",
|
103
|
+
type: String,
|
104
|
+
help: "Write output to file instead of stdout. If no path provided will overwrite input file (will prompt to overwrite existing files)"
|
105
|
+
},
|
106
|
+
{
|
107
|
+
key: :output_template_path,
|
108
|
+
long: "--output-template TEMPLATE",
|
109
|
+
type: String,
|
110
|
+
help: "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)"
|
111
|
+
}
|
112
|
+
]
|
41
113
|
end
|
42
114
|
end
|
43
115
|
end
|
@@ -7,7 +7,7 @@ require_relative "tests/test_run_diff_report"
|
|
7
7
|
|
8
8
|
module AIRefactor
|
9
9
|
module Refactors
|
10
|
-
class RspecToMinitestRails <
|
10
|
+
class RspecToMinitestRails < BaseRefactor
|
11
11
|
def run
|
12
12
|
spec_runner = AIRefactor::Tests::RSpecRunner.new(input_file)
|
13
13
|
logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
|
@@ -17,6 +17,7 @@ module AIRefactor
|
|
17
17
|
if spec_run.failed?
|
18
18
|
logger.warn "Skipping #{input_file}..."
|
19
19
|
logger.error "Failed to run #{input_file}, exited with status #{spec_run.exitstatus}. Stdout: #{spec_run.stdout}\n\nStderr: #{spec_run.stderr}\n\n"
|
20
|
+
self.failed_message = "Failed to run RSpec file, has errors"
|
20
21
|
return false
|
21
22
|
end
|
22
23
|
|
@@ -26,20 +27,15 @@ module AIRefactor
|
|
26
27
|
output_path = input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
|
27
28
|
|
28
29
|
processor = AIRefactor::FileProcessor.new(
|
29
|
-
input_file,
|
30
|
-
output_path,
|
31
|
-
prompt_file_path:
|
30
|
+
input_path: input_file,
|
31
|
+
output_path: output_path,
|
32
|
+
prompt_file_path: prompt_file_path,
|
32
33
|
ai_client: ai_client,
|
33
34
|
logger: logger
|
34
35
|
)
|
35
36
|
|
36
37
|
if processor.output_exists?
|
37
|
-
|
38
|
-
answer = $stdin.gets.chomp
|
39
|
-
unless answer == "y" || answer == "Y"
|
40
|
-
logger.warn "Skipping #{input_file}..."
|
41
|
-
return false
|
42
|
-
end
|
38
|
+
return false unless can_overwrite_output_file?(output_path)
|
43
39
|
end
|
44
40
|
|
45
41
|
logger.verbose "Converting #{input_file}..."
|
@@ -51,6 +47,7 @@ module AIRefactor
|
|
51
47
|
rescue => e
|
52
48
|
logger.error "Request to OpenAI failed: #{e.message}"
|
53
49
|
logger.warn "Skipping #{input_file}..."
|
50
|
+
self.failed_message = "Request to OpenAI failed"
|
54
51
|
return false
|
55
52
|
end
|
56
53
|
|
@@ -65,6 +62,7 @@ module AIRefactor
|
|
65
62
|
if !output_content || output_content.length == 0
|
66
63
|
logger.warn "Skipping #{input_file}, no translated output..."
|
67
64
|
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
65
|
+
self.failed_message = "AI conversion failed, no output was generated"
|
68
66
|
return false
|
69
67
|
end
|
70
68
|
|
@@ -79,6 +77,7 @@ module AIRefactor
|
|
79
77
|
logger.warn "Skipping #{input_file}..."
|
80
78
|
logger.error "Failed to run translated #{output_path}, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
|
81
79
|
logger.error "Conversion failed!", bold: true
|
80
|
+
self.failed_message = "Generated test file failed to run correctly"
|
82
81
|
return false
|
83
82
|
end
|
84
83
|
|
@@ -95,6 +94,7 @@ module AIRefactor
|
|
95
94
|
logger.warn report.diff.colorize(:yellow)
|
96
95
|
logger.verbose "Done converting #{input_file} to #{output_path}..."
|
97
96
|
logger.error "Differences found! Conversion failed!", bold: true
|
97
|
+
self.failed_message = "Generated test file run output did not match original RSpec spec run output"
|
98
98
|
false
|
99
99
|
end
|
100
100
|
end
|
data/lib/ai_refactor/version.rb
CHANGED
data/lib/ai_refactor.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative "ai_refactor/logger"
|
|
6
6
|
require_relative "ai_refactor/file_processor"
|
7
7
|
|
8
8
|
require_relative "ai_refactor/refactors"
|
9
|
+
require_relative "ai_refactor/base_refactor"
|
9
10
|
require_relative "ai_refactor/refactors/generic"
|
10
11
|
require_relative "ai_refactor/refactors/rspec_to_minitest_rails"
|
11
12
|
require_relative "ai_refactor/refactors/minitest_to_rspec"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ai_refactor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Ierodiaconou
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- ai_refactor.gemspec
|
78
78
|
- exe/ai_refactor
|
79
79
|
- lib/ai_refactor.rb
|
80
|
+
- lib/ai_refactor/base_refactor.rb
|
80
81
|
- lib/ai_refactor/file_processor.rb
|
81
82
|
- lib/ai_refactor/logger.rb
|
82
83
|
- lib/ai_refactor/refactors.rb
|