ai_refactor 0.4.0 → 0.5.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/CHANGELOG.md +10 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +165 -1
- data/README.md +116 -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 +95 -42
- data/lib/ai_refactor/cli.rb +60 -8
- data/lib/ai_refactor/command_file_parser.rb +27 -0
- 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 -2
- data/lib/ai_refactor/refactors/{generic.rb → custom.rb} +2 -2
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +4 -0
- 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/test_runners/minitest_runner.rb +1 -1
- data/lib/ai_refactor/version.rb +1 -1
- metadata +15 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15f32bc208d6025b339dd08061747f790975c9163909a83dac8d3abc381b4fac
|
|
4
|
+
data.tar.gz: cb1e71b84e2aa8b77900a5a33f10090f3ee65d6d6f0fba606359c5a2cb1f2719
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b16ab1f05053cbe504cdda1f66326ec0ee27977e322df37ad5b00ffc319dd7c5692785f564a04ba4a6dc238015689f6ed7aca901ab4dc7bad84f418ad887c0e
|
|
7
|
+
data.tar.gz: a18db957cfd44fbf817b61c5b198f6bd122d6f8368f098925ff2b4de3f94c88f3f913133685424107e06b6c9338c6b70f1d6e5b0bb98b7865cecbee522b1cdb0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# AI Refactor Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2023-09-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Support for new command files, which are YAML files that can be used to define options for a refactor. This makes it
|
|
8
|
+
simpler to create configurations for refactors that will be used repeatedly. They can be committed to source control
|
|
9
|
+
of your project and shared with other developers.
|
|
10
|
+
- Support for configuring the run commands for the test runners
|
|
11
|
+
- Adding real life examples
|
|
12
|
+
|
|
3
13
|
## [0.4.0] - 2023-08-15
|
|
4
14
|
|
|
5
15
|
### Added
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
ai_refactor (0.
|
|
4
|
+
ai_refactor (0.5.0)
|
|
5
5
|
colorize (< 2.0)
|
|
6
6
|
open3 (< 2.0)
|
|
7
7
|
ruby-openai (>= 3.4.0, < 5.0)
|
|
@@ -10,28 +10,179 @@ PATH
|
|
|
10
10
|
GEM
|
|
11
11
|
remote: https://rubygems.org/
|
|
12
12
|
specs:
|
|
13
|
+
actioncable (7.0.8)
|
|
14
|
+
actionpack (= 7.0.8)
|
|
15
|
+
activesupport (= 7.0.8)
|
|
16
|
+
nio4r (~> 2.0)
|
|
17
|
+
websocket-driver (>= 0.6.1)
|
|
18
|
+
actionmailbox (7.0.8)
|
|
19
|
+
actionpack (= 7.0.8)
|
|
20
|
+
activejob (= 7.0.8)
|
|
21
|
+
activerecord (= 7.0.8)
|
|
22
|
+
activestorage (= 7.0.8)
|
|
23
|
+
activesupport (= 7.0.8)
|
|
24
|
+
mail (>= 2.7.1)
|
|
25
|
+
net-imap
|
|
26
|
+
net-pop
|
|
27
|
+
net-smtp
|
|
28
|
+
actionmailer (7.0.8)
|
|
29
|
+
actionpack (= 7.0.8)
|
|
30
|
+
actionview (= 7.0.8)
|
|
31
|
+
activejob (= 7.0.8)
|
|
32
|
+
activesupport (= 7.0.8)
|
|
33
|
+
mail (~> 2.5, >= 2.5.4)
|
|
34
|
+
net-imap
|
|
35
|
+
net-pop
|
|
36
|
+
net-smtp
|
|
37
|
+
rails-dom-testing (~> 2.0)
|
|
38
|
+
actionpack (7.0.8)
|
|
39
|
+
actionview (= 7.0.8)
|
|
40
|
+
activesupport (= 7.0.8)
|
|
41
|
+
rack (~> 2.0, >= 2.2.4)
|
|
42
|
+
rack-test (>= 0.6.3)
|
|
43
|
+
rails-dom-testing (~> 2.0)
|
|
44
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
|
45
|
+
actiontext (7.0.8)
|
|
46
|
+
actionpack (= 7.0.8)
|
|
47
|
+
activerecord (= 7.0.8)
|
|
48
|
+
activestorage (= 7.0.8)
|
|
49
|
+
activesupport (= 7.0.8)
|
|
50
|
+
globalid (>= 0.6.0)
|
|
51
|
+
nokogiri (>= 1.8.5)
|
|
52
|
+
actionview (7.0.8)
|
|
53
|
+
activesupport (= 7.0.8)
|
|
54
|
+
builder (~> 3.1)
|
|
55
|
+
erubi (~> 1.4)
|
|
56
|
+
rails-dom-testing (~> 2.0)
|
|
57
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
|
58
|
+
activejob (7.0.8)
|
|
59
|
+
activesupport (= 7.0.8)
|
|
60
|
+
globalid (>= 0.3.6)
|
|
61
|
+
activemodel (7.0.8)
|
|
62
|
+
activesupport (= 7.0.8)
|
|
63
|
+
activerecord (7.0.8)
|
|
64
|
+
activemodel (= 7.0.8)
|
|
65
|
+
activesupport (= 7.0.8)
|
|
66
|
+
activestorage (7.0.8)
|
|
67
|
+
actionpack (= 7.0.8)
|
|
68
|
+
activejob (= 7.0.8)
|
|
69
|
+
activerecord (= 7.0.8)
|
|
70
|
+
activesupport (= 7.0.8)
|
|
71
|
+
marcel (~> 1.0)
|
|
72
|
+
mini_mime (>= 1.1.0)
|
|
73
|
+
activesupport (7.0.8)
|
|
74
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
75
|
+
i18n (>= 1.6, < 2)
|
|
76
|
+
minitest (>= 5.1)
|
|
77
|
+
tzinfo (~> 2.0)
|
|
13
78
|
ast (2.4.2)
|
|
79
|
+
builder (3.2.4)
|
|
14
80
|
colorize (0.8.1)
|
|
81
|
+
concurrent-ruby (1.2.2)
|
|
82
|
+
crass (1.0.6)
|
|
83
|
+
date (3.3.3)
|
|
84
|
+
diff-lcs (1.5.0)
|
|
15
85
|
dotenv (2.8.1)
|
|
86
|
+
erubi (1.12.0)
|
|
16
87
|
faraday (2.7.4)
|
|
17
88
|
faraday-net_http (>= 2.0, < 3.1)
|
|
18
89
|
ruby2_keywords (>= 0.0.4)
|
|
19
90
|
faraday-multipart (1.0.4)
|
|
20
91
|
multipart-post (~> 2)
|
|
21
92
|
faraday-net_http (3.0.2)
|
|
93
|
+
globalid (1.2.1)
|
|
94
|
+
activesupport (>= 6.1)
|
|
95
|
+
i18n (1.14.1)
|
|
96
|
+
concurrent-ruby (~> 1.0)
|
|
22
97
|
json (2.6.3)
|
|
23
98
|
language_server-protocol (3.17.0.3)
|
|
24
99
|
lint_roller (1.0.0)
|
|
100
|
+
loofah (2.21.3)
|
|
101
|
+
crass (~> 1.0.2)
|
|
102
|
+
nokogiri (>= 1.12.0)
|
|
103
|
+
mail (2.8.1)
|
|
104
|
+
mini_mime (>= 0.1.1)
|
|
105
|
+
net-imap
|
|
106
|
+
net-pop
|
|
107
|
+
net-smtp
|
|
108
|
+
marcel (1.0.2)
|
|
109
|
+
method_source (1.0.0)
|
|
110
|
+
mini_mime (1.1.5)
|
|
25
111
|
minitest (5.18.0)
|
|
26
112
|
multipart-post (2.3.0)
|
|
113
|
+
net-imap (0.3.7)
|
|
114
|
+
date
|
|
115
|
+
net-protocol
|
|
116
|
+
net-pop (0.1.2)
|
|
117
|
+
net-protocol
|
|
118
|
+
net-protocol (0.2.1)
|
|
119
|
+
timeout
|
|
120
|
+
net-smtp (0.3.3)
|
|
121
|
+
net-protocol
|
|
122
|
+
nio4r (2.5.9)
|
|
123
|
+
nokogiri (1.15.4-arm64-darwin)
|
|
124
|
+
racc (~> 1.4)
|
|
27
125
|
open3 (0.1.2)
|
|
28
126
|
parallel (1.23.0)
|
|
29
127
|
parser (3.2.2.1)
|
|
30
128
|
ast (~> 2.4.1)
|
|
129
|
+
racc (1.7.1)
|
|
130
|
+
rack (2.2.8)
|
|
131
|
+
rack-test (2.1.0)
|
|
132
|
+
rack (>= 1.3)
|
|
133
|
+
rails (7.0.8)
|
|
134
|
+
actioncable (= 7.0.8)
|
|
135
|
+
actionmailbox (= 7.0.8)
|
|
136
|
+
actionmailer (= 7.0.8)
|
|
137
|
+
actionpack (= 7.0.8)
|
|
138
|
+
actiontext (= 7.0.8)
|
|
139
|
+
actionview (= 7.0.8)
|
|
140
|
+
activejob (= 7.0.8)
|
|
141
|
+
activemodel (= 7.0.8)
|
|
142
|
+
activerecord (= 7.0.8)
|
|
143
|
+
activestorage (= 7.0.8)
|
|
144
|
+
activesupport (= 7.0.8)
|
|
145
|
+
bundler (>= 1.15.0)
|
|
146
|
+
railties (= 7.0.8)
|
|
147
|
+
rails-dom-testing (2.2.0)
|
|
148
|
+
activesupport (>= 5.0.0)
|
|
149
|
+
minitest
|
|
150
|
+
nokogiri (>= 1.6)
|
|
151
|
+
rails-html-sanitizer (1.6.0)
|
|
152
|
+
loofah (~> 2.21)
|
|
153
|
+
nokogiri (~> 1.14)
|
|
154
|
+
railties (7.0.8)
|
|
155
|
+
actionpack (= 7.0.8)
|
|
156
|
+
activesupport (= 7.0.8)
|
|
157
|
+
method_source
|
|
158
|
+
rake (>= 12.2)
|
|
159
|
+
thor (~> 1.0)
|
|
160
|
+
zeitwerk (~> 2.5)
|
|
31
161
|
rainbow (3.1.1)
|
|
32
162
|
rake (13.0.6)
|
|
33
163
|
regexp_parser (2.8.0)
|
|
34
164
|
rexml (3.2.5)
|
|
165
|
+
rspec (3.12.0)
|
|
166
|
+
rspec-core (~> 3.12.0)
|
|
167
|
+
rspec-expectations (~> 3.12.0)
|
|
168
|
+
rspec-mocks (~> 3.12.0)
|
|
169
|
+
rspec-core (3.12.2)
|
|
170
|
+
rspec-support (~> 3.12.0)
|
|
171
|
+
rspec-expectations (3.12.3)
|
|
172
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
173
|
+
rspec-support (~> 3.12.0)
|
|
174
|
+
rspec-mocks (3.12.6)
|
|
175
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
176
|
+
rspec-support (~> 3.12.0)
|
|
177
|
+
rspec-rails (6.0.3)
|
|
178
|
+
actionpack (>= 6.1)
|
|
179
|
+
activesupport (>= 6.1)
|
|
180
|
+
railties (>= 6.1)
|
|
181
|
+
rspec-core (~> 3.12)
|
|
182
|
+
rspec-expectations (~> 3.12)
|
|
183
|
+
rspec-mocks (~> 3.12)
|
|
184
|
+
rspec-support (~> 3.12)
|
|
185
|
+
rspec-support (3.12.1)
|
|
35
186
|
rubocop (1.50.2)
|
|
36
187
|
json (~> 2.3)
|
|
37
188
|
parallel (~> 1.10)
|
|
@@ -52,6 +203,8 @@ GEM
|
|
|
52
203
|
faraday-multipart (>= 1)
|
|
53
204
|
ruby-progressbar (1.13.0)
|
|
54
205
|
ruby2_keywords (0.0.5)
|
|
206
|
+
shoulda-matchers (5.3.0)
|
|
207
|
+
activesupport (>= 5.2.0)
|
|
55
208
|
standard (1.28.2)
|
|
56
209
|
language_server-protocol (~> 3.17.0.2)
|
|
57
210
|
lint_roller (~> 1.0)
|
|
@@ -63,7 +216,14 @@ GEM
|
|
|
63
216
|
standard-performance (1.0.1)
|
|
64
217
|
lint_roller (~> 1.0)
|
|
65
218
|
rubocop-performance (~> 1.16.0)
|
|
219
|
+
thor (1.2.2)
|
|
220
|
+
timeout (0.4.0)
|
|
221
|
+
tzinfo (2.0.6)
|
|
222
|
+
concurrent-ruby (~> 1.0)
|
|
66
223
|
unicode-display_width (2.4.2)
|
|
224
|
+
websocket-driver (0.7.6)
|
|
225
|
+
websocket-extensions (>= 0.1.0)
|
|
226
|
+
websocket-extensions (0.1.5)
|
|
67
227
|
zeitwerk (2.6.8)
|
|
68
228
|
|
|
69
229
|
PLATFORMS
|
|
@@ -73,7 +233,11 @@ DEPENDENCIES
|
|
|
73
233
|
ai_refactor!
|
|
74
234
|
dotenv
|
|
75
235
|
minitest (~> 5.0)
|
|
236
|
+
rails
|
|
76
237
|
rake (~> 13.0)
|
|
238
|
+
rspec
|
|
239
|
+
rspec-rails
|
|
240
|
+
shoulda-matchers
|
|
77
241
|
standard (~> 1.3)
|
|
78
242
|
|
|
79
243
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AIRefactor for Ruby
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
__The goal for AIRefactor is to use LLMs to apply repetitive refactoring tasks to code.__
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
First the human decides what refactoring is needed and builds up a prompt to describe the task, or uses one of AIRefactors provided prompts.
|
|
6
|
+
|
|
7
|
+
AIRefactor then helps to apply the refactoring to one or more files.
|
|
8
|
+
|
|
9
|
+
In some cases, the tool can then check the generated code by running tests and comparing test outputs.
|
|
10
|
+
|
|
11
|
+
#### Notes
|
|
12
|
+
|
|
13
|
+
AI Refactor is an experimental tool and under active development as I explore the idea myself. It may not work as expected, or
|
|
14
|
+
change in ways that break existing functionality.
|
|
15
|
+
|
|
16
|
+
The focus of the tool is work with the Ruby programming language ecosystem, but it can be used with any language.
|
|
6
17
|
|
|
7
18
|
AI Refactor currently uses [OpenAI's ChatGPT](https://platform.openai.com/).
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
The tool then uses a LLM to apply the relevant refactor, and if appropriate, checks results by running tests and comparing output.
|
|
20
|
+
## Examples
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
See the [examples](examples/) directory for some examples of using the tool.
|
|
13
23
|
|
|
14
24
|
## Available refactors
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
Write your own prompt:
|
|
17
27
|
|
|
28
|
+
- `ruby/write_ruby`: provide your own prompt for the AI and expect to output Ruby code (no input files required)
|
|
29
|
+
- `ruby/refactor_ruby`: provide your own refactoring prompt for the AI and expect to output Ruby code
|
|
30
|
+
- `custom`: provide your own prompt for the AI and run against the input files. There is no expectation of the output.
|
|
31
|
+
|
|
32
|
+
Use a pre-built prompt:
|
|
33
|
+
|
|
34
|
+
- `minitest/write_test_for_class`: write a minitest test for a given class
|
|
18
35
|
- `rails/minitest/rspec_to_minitest`: convert RSpec specs to minitest tests in Rails apps
|
|
19
|
-
|
|
36
|
+
|
|
37
|
+
### User supplied prompts, eg `custom`, `ruby/write_ruby` and `ruby/refactor_ruby`
|
|
38
|
+
|
|
39
|
+
Applies the refactor specified by prompting the AI with the user supplied prompt. You must supply a prompt file with the `-p` option.
|
|
40
|
+
|
|
41
|
+
The output is written to `stdout`, or to a file with the `--output` option.
|
|
42
|
+
|
|
43
|
+
User supplied prompts are best configured using a command file, see below.
|
|
20
44
|
|
|
21
45
|
### `rails/minitest/rspec_to_minitest`
|
|
22
46
|
|
|
@@ -44,12 +68,6 @@ Refactor succeeded on spec/models/my_thing_spec.rb
|
|
|
44
68
|
Done processing all files!
|
|
45
69
|
```
|
|
46
70
|
|
|
47
|
-
### `generic` (user supplied prompt)
|
|
48
|
-
|
|
49
|
-
Applies the refactor specified by prompting the AI with the user supplied prompt. You must supply a prompt file with the `-p` option.
|
|
50
|
-
|
|
51
|
-
The output is written to `stdout`, or to a file with the `--output` option.
|
|
52
|
-
|
|
53
71
|
### `minitest/write_test_for_class`
|
|
54
72
|
|
|
55
73
|
Writes a minitest test for a given class. The output will, by default, be put into a directory named `test` in the current directory,
|
|
@@ -75,13 +93,14 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
75
93
|
See `ai_refactor --help` for more information.
|
|
76
94
|
|
|
77
95
|
```
|
|
78
|
-
Usage: ai_refactor
|
|
96
|
+
Usage: ai_refactor REFACTOR_TYPE_OR_COMMAND_FILE INPUT_FILE_OR_DIR [options]
|
|
79
97
|
|
|
80
|
-
Where
|
|
98
|
+
Where REFACTOR_TYPE_OR_COMMAND_FILE is either the path to a command YML file, or one of the refactor types: ["custom" ... (run ai_refactor --help for full list of refactor types)]
|
|
81
99
|
|
|
82
100
|
-o, --output [FILE] 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.
|
|
83
101
|
-O, --output-template TEMPLATE 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)
|
|
84
102
|
-c, --context CONTEXT_FILES 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.
|
|
103
|
+
-x, --extra CONTEXT_TEXT Specify some text to be prepended to the prompt sent to the AI as extra information of note.
|
|
85
104
|
-r, --review-prompt Show the prompt that will be sent to ChatGPT but do not actually call ChatGPT or make changes to files.
|
|
86
105
|
-p, --prompt PROMPT_FILE Specify path to a text file that contains the ChatGPT 'system' prompt.
|
|
87
106
|
-f, --diffs Request AI generate diffs of changes rather than writing out the whole file.
|
|
@@ -91,11 +110,81 @@ Where REFACTOR_TYPE is one of: ["generic" ... (run ai_refactor --help for full l
|
|
|
91
110
|
--max-tokens MAX_TOKENS Specify the max number of tokens of output ChatGPT can generate. Max will depend on the size of the prompt (default 1500)
|
|
92
111
|
-t, --timeout SECONDS Specify the max wait time for ChatGPT response.
|
|
93
112
|
--overwrite ANSWER Always overwrite existing output files, 'y' for yes, 'n' for no, or 'a' for ask. Default to ask.
|
|
113
|
+
-N, --no Never overwrite existing output files, same as --overwrite=n.
|
|
94
114
|
-v, --verbose Show extra output and progress info
|
|
95
115
|
-d, --debug Show debugging output to help diagnose issues
|
|
96
116
|
-h, --help Prints this help
|
|
97
117
|
```
|
|
98
118
|
|
|
119
|
+
### Interactive mode
|
|
120
|
+
|
|
121
|
+
A basic interactive mode exists too, where you are prompted for options.
|
|
122
|
+
|
|
123
|
+
Start interactive mode by not specifying anything for `REFACTOR_TYPE_OR_COMMAND_FILE` (ie no refactor type or command file)
|
|
124
|
+
|
|
125
|
+
### Command files and Custom prompts
|
|
126
|
+
|
|
127
|
+
Apart from invoking the tool with CLI options, the tool can also be invoked with a command file.
|
|
128
|
+
|
|
129
|
+
This makes it easier to build custom refactor prompts for projects, and run that custom refactor multiple times.
|
|
130
|
+
|
|
131
|
+
The command file is a YAML file that contains configuration options to pass to the tool.
|
|
132
|
+
|
|
133
|
+
The format of the YAML file is:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
# Required options:
|
|
137
|
+
refactor: refactor type name, eg 'ruby/write_ruby'
|
|
138
|
+
# Optional options:
|
|
139
|
+
input_file_paths:
|
|
140
|
+
- input files or directories
|
|
141
|
+
output_file_path: output file or directory
|
|
142
|
+
output_template_path: output file template (see docs)
|
|
143
|
+
prompt_file_path: path
|
|
144
|
+
prompt: |
|
|
145
|
+
A custom prompt to send to ChatGPT if the command needs it (otherwise read from file)
|
|
146
|
+
context_file_paths:
|
|
147
|
+
- file1.rb
|
|
148
|
+
- file2.rb
|
|
149
|
+
# Other configuration options:
|
|
150
|
+
context_text: |
|
|
151
|
+
Some extra info to prepend to the prompt
|
|
152
|
+
diff: true/false (default false)
|
|
153
|
+
ai_max_attempts: max times to generate more if AI does not complete generating (default 3)
|
|
154
|
+
ai_model: ChatGPT model name (default gpt-4)
|
|
155
|
+
ai_temperature: ChatGPT temperature (default 0.7)
|
|
156
|
+
ai_max_tokens: ChatGPT max tokens (default 1500)
|
|
157
|
+
ai_timeout: ChatGPT timeout (default 60)
|
|
158
|
+
overwrite: y/n/a (default a)
|
|
159
|
+
verbose: true/false (default false)
|
|
160
|
+
debug: true/false (default false)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The command file can be invoked by passing it as the first argument to the tool:
|
|
164
|
+
|
|
165
|
+
```shell
|
|
166
|
+
ai_refactor my_command_file.yml
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Other options can be passed on the command line and will override the options in the command file.
|
|
170
|
+
|
|
171
|
+
For example, if the command file contains:
|
|
172
|
+
|
|
173
|
+
```shell
|
|
174
|
+
ai_refactor my_command_file.yml my_input.rb -d --output foo.rb
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Prompt template substitutions
|
|
178
|
+
|
|
179
|
+
Prompt text can contain the following substitutions:
|
|
180
|
+
|
|
181
|
+
* `__{{input_file_path}}__`: the path to the input file
|
|
182
|
+
* `__{{output_file_path}}__`: the path to the output file
|
|
183
|
+
* `__{{prompt_header}}__`: the place the pre-build prompt will be injected, if used
|
|
184
|
+
* `__{{prompt_footer}}__`: prompt text that will be inserted after the prompt, eg the "make diffs" prompt if `--diffs` is used
|
|
185
|
+
* `__{{context}}__`: the contents of the context files, if any
|
|
186
|
+
* `__{{content}}__`: the contents of input file, if any
|
|
187
|
+
|
|
99
188
|
## Outputs
|
|
100
189
|
|
|
101
190
|
Some refactor tasks will write out to a new file by default, others to stdout.
|
|
@@ -117,6 +206,17 @@ eg for the input `my_dir/my_class.rb`
|
|
|
117
206
|
- `[REFACTOR]`: `generic`
|
|
118
207
|
- `[EXT]`: `.rb`
|
|
119
208
|
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
### `.ai_refactor` file
|
|
212
|
+
|
|
213
|
+
The tool can be configured using a `.ai_refactor` file in the current directory or in the user's home directory.
|
|
214
|
+
|
|
215
|
+
This file provides default CLI switches to add to any `ai_refactor` command.
|
|
216
|
+
|
|
217
|
+
## Command history
|
|
218
|
+
|
|
219
|
+
The tool keeps a history of commands run in the `.ai_refactor_history` file in the current working directory.
|
|
120
220
|
|
|
121
221
|
## Note on performance and ChatGPT version
|
|
122
222
|
|
data/examples/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ex1_input_test.rb
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
refactor: rails/minitest/rspec_to_minitest
|
|
2
|
+
input_file_paths:
|
|
3
|
+
- examples/ex1_input_spec.rb
|
|
4
|
+
# We need to add context here as otherwise to tell the AI to require our local test_helper.rb file so that we can run the tests after
|
|
5
|
+
context_text: "In the output test use `require_relative` to include 'test_helper'."
|
|
6
|
+
# By default, ai_refactor runs "bundle exec rails test" but this isn't going to work here as we are not actually in a Rails app context in the examples
|
|
7
|
+
minitest_run_command: ruby __FILE__
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe MyModel, type: :model do
|
|
4
|
+
subject(:model) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it { is_expected.to validate_presence_of(:name) }
|
|
7
|
+
|
|
8
|
+
it "should allow integer values for age" do
|
|
9
|
+
model.age = 1
|
|
10
|
+
expect(model.age).to eq 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should allow string values for name" do
|
|
14
|
+
model.name = "test"
|
|
15
|
+
expect(model.name).to eq "test"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should be invalid with invalid name" do
|
|
19
|
+
model.name = nil
|
|
20
|
+
expect(model).to be_invalid
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "should convert integer values for name" do
|
|
24
|
+
model.name = 1
|
|
25
|
+
expect(model.name).to eq "1"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should not allow string values for age" do
|
|
29
|
+
model.age = "test"
|
|
30
|
+
expect(model.age).to eq 0
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "rails/all"
|
|
2
|
+
require "shoulda-matchers"
|
|
3
|
+
|
|
4
|
+
Shoulda::Matchers.configure do |config|
|
|
5
|
+
config.integrate do |with|
|
|
6
|
+
with.test_framework :rspec
|
|
7
|
+
with.library :rails
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class MyModel
|
|
12
|
+
include ActiveModel::Model
|
|
13
|
+
include ActiveModel::Attributes
|
|
14
|
+
include ActiveModel::Validations
|
|
15
|
+
include ActiveModel::Validations::Callbacks
|
|
16
|
+
|
|
17
|
+
validates :name, presence: true
|
|
18
|
+
|
|
19
|
+
attribute :name, :string
|
|
20
|
+
attribute :age, :integer
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "rails/all"
|
|
2
|
+
require "active_support/testing/autorun"
|
|
3
|
+
|
|
4
|
+
class MyModel
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Attributes
|
|
7
|
+
include ActiveModel::Validations
|
|
8
|
+
include ActiveModel::Validations::Callbacks
|
|
9
|
+
|
|
10
|
+
validates :name, presence: true
|
|
11
|
+
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
attribute :age, :integer
|
|
14
|
+
end
|
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,93 @@ 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 "Loaded config from '#{options_from_config_file}'..." if options_from_config_file
|
|
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}'..."
|
|
142
194
|
end
|
|
143
195
|
|
|
144
|
-
|
|
196
|
+
run_config.input_file_paths = arguments
|
|
197
|
+
|
|
198
|
+
job = AIRefactor::Cli.new(run_config, logger: logger)
|
|
145
199
|
|
|
146
200
|
unless job.valid?
|
|
147
|
-
|
|
148
|
-
exit 1
|
|
201
|
+
exit_with_error("Refactor job failed or was not correctly configured. Did you specify the required inputs or options?.", logger)
|
|
149
202
|
end
|
|
150
203
|
|
|
151
204
|
unless job.run
|
|
152
|
-
exit
|
|
205
|
+
exit false
|
|
153
206
|
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
|
|
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
|
|
@@ -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
|
|
|
@@ -79,6 +79,7 @@ module AIRefactor
|
|
|
79
79
|
output_content
|
|
80
80
|
rescue => e
|
|
81
81
|
logger.error "Request to AI failed: #{e.message}"
|
|
82
|
+
puts e.backtrace
|
|
82
83
|
logger.warn "Skipping #{input_file}..."
|
|
83
84
|
self.failed_message = "Request to OpenAI failed"
|
|
84
85
|
raise e
|
|
@@ -102,7 +103,11 @@ module AIRefactor
|
|
|
102
103
|
false
|
|
103
104
|
end
|
|
104
105
|
|
|
105
|
-
def
|
|
106
|
+
def prompt_input
|
|
107
|
+
if options && options[:prompt]&.length&.positive?
|
|
108
|
+
return options[:prompt]
|
|
109
|
+
end
|
|
110
|
+
|
|
106
111
|
file = if options && options[:prompt_file_path]&.length&.positive?
|
|
107
112
|
options[:prompt_file_path]
|
|
108
113
|
else
|
|
@@ -112,6 +117,8 @@ module AIRefactor
|
|
|
112
117
|
file.tap do |prompt|
|
|
113
118
|
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
|
114
119
|
end
|
|
120
|
+
|
|
121
|
+
File.read(file)
|
|
115
122
|
end
|
|
116
123
|
|
|
117
124
|
def output_file_path
|
|
@@ -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
|
+
__{{prompt_header}}__
|
|
8
|
+
|
|
7
9
|
Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
|
|
8
10
|
|
|
9
11
|
__{{context}}__
|
|
10
12
|
|
|
13
|
+
__{{prompt_footer}}__
|
|
14
|
+
|
|
11
15
|
The class to test is:
|
|
@@ -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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AIRefactor
|
|
4
|
+
class RunConfiguration
|
|
5
|
+
def self.add_new_option(key)
|
|
6
|
+
self.class.define_method(key) { instance_variable_get("@#{key}") }
|
|
7
|
+
self.class.define_method("#{key}=") { |v| instance_variable_set("@#{key}", v) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :refactor,
|
|
11
|
+
:input_file_paths,
|
|
12
|
+
:output_file_path,
|
|
13
|
+
:output_template_path,
|
|
14
|
+
:context_file_paths,
|
|
15
|
+
:context_text,
|
|
16
|
+
:review_prompt,
|
|
17
|
+
:prompt,
|
|
18
|
+
:prompt_file_path,
|
|
19
|
+
:ai_max_attempts,
|
|
20
|
+
:ai_model,
|
|
21
|
+
:ai_temperature,
|
|
22
|
+
:ai_max_tokens,
|
|
23
|
+
:ai_timeout,
|
|
24
|
+
:overwrite,
|
|
25
|
+
:diff,
|
|
26
|
+
:verbose,
|
|
27
|
+
:debug
|
|
28
|
+
|
|
29
|
+
def set!(hash)
|
|
30
|
+
hash.each do |key, value|
|
|
31
|
+
raise StandardError, "Invalid option: #{key}" unless respond_to?("#{key}=")
|
|
32
|
+
send("#{key}=", value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_writer :refactor
|
|
37
|
+
|
|
38
|
+
# @deprecated
|
|
39
|
+
def [](key)
|
|
40
|
+
send(key)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def input_file_paths=(paths)
|
|
44
|
+
@input_file_paths ||= []
|
|
45
|
+
paths = [paths] unless paths.is_a?(Array)
|
|
46
|
+
@input_file_paths.concat(paths)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_writer :output_file_path
|
|
50
|
+
|
|
51
|
+
attr_writer :output_template_path
|
|
52
|
+
|
|
53
|
+
def context_file_paths=(paths)
|
|
54
|
+
@context_file_paths ||= []
|
|
55
|
+
paths = [paths] unless paths.is_a?(Array)
|
|
56
|
+
@context_file_paths.concat(paths)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def context_text=(text)
|
|
60
|
+
@context_text ||= ""
|
|
61
|
+
@context_text += text
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_writer :review_prompt
|
|
65
|
+
attr_writer :prompt
|
|
66
|
+
attr_writer :prompt_file_path
|
|
67
|
+
|
|
68
|
+
def rspec_run_command
|
|
69
|
+
@rspec_run_command || "bundle exec rspec __FILE__"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def minitest_run_command
|
|
73
|
+
@minitest_run_command || "ruby __FILE__"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attr_writer :rspec_run_command
|
|
77
|
+
attr_writer :minitest_run_command
|
|
78
|
+
|
|
79
|
+
def ai_max_attempts=(value)
|
|
80
|
+
@ai_max_attempts = value || 3
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ai_model=(value)
|
|
84
|
+
@ai_model = value || "gpt-4"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ai_temperature=(value)
|
|
88
|
+
@ai_temperature = value || 0.7
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ai_max_tokens=(value)
|
|
92
|
+
@ai_max_tokens = value || 1500
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ai_timeout=(value)
|
|
96
|
+
@ai_timeout = value || 60
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def overwrite=(value)
|
|
100
|
+
@overwrite = value || "a"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
attr_writer :diff
|
|
104
|
+
|
|
105
|
+
attr_writer :verbose
|
|
106
|
+
|
|
107
|
+
attr_writer :debug
|
|
108
|
+
|
|
109
|
+
def to_options
|
|
110
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
111
|
+
hash[var.to_s.delete("@").to_sym] = instance_variable_get(var)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -5,7 +5,7 @@ require "open3"
|
|
|
5
5
|
module AIRefactor
|
|
6
6
|
module TestRunners
|
|
7
7
|
class MinitestRunner
|
|
8
|
-
def initialize(file_path, command_template: "
|
|
8
|
+
def initialize(file_path, command_template: "ruby __FILE__")
|
|
9
9
|
@file_path = file_path
|
|
10
10
|
@command_template = command_template
|
|
11
11
|
end
|
data/lib/ai_refactor/version.rb
CHANGED
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.5.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-
|
|
11
|
+
date: 2023-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: colorize
|
|
@@ -89,9 +89,15 @@ files:
|
|
|
89
89
|
- README.md
|
|
90
90
|
- Rakefile
|
|
91
91
|
- ai_refactor.gemspec
|
|
92
|
+
- examples/.gitignore
|
|
93
|
+
- examples/ex1_convert_a_rspec_test_to_minitest.yml
|
|
94
|
+
- examples/ex1_input_spec.rb
|
|
95
|
+
- examples/rails_helper.rb
|
|
96
|
+
- examples/test_helper.rb
|
|
92
97
|
- exe/ai_refactor
|
|
93
98
|
- lib/ai_refactor.rb
|
|
94
99
|
- lib/ai_refactor/cli.rb
|
|
100
|
+
- lib/ai_refactor/command_file_parser.rb
|
|
95
101
|
- lib/ai_refactor/context.rb
|
|
96
102
|
- lib/ai_refactor/file_processor.rb
|
|
97
103
|
- lib/ai_refactor/logger.rb
|
|
@@ -100,7 +106,7 @@ files:
|
|
|
100
106
|
- lib/ai_refactor/prompts/input.md
|
|
101
107
|
- lib/ai_refactor/refactors.rb
|
|
102
108
|
- lib/ai_refactor/refactors/base_refactor.rb
|
|
103
|
-
- lib/ai_refactor/refactors/
|
|
109
|
+
- lib/ai_refactor/refactors/custom.rb
|
|
104
110
|
- lib/ai_refactor/refactors/minitest/write_test_for_class.md
|
|
105
111
|
- lib/ai_refactor/refactors/minitest/write_test_for_class.rb
|
|
106
112
|
- lib/ai_refactor/refactors/project/write_changelog_from_history.md
|
|
@@ -109,6 +115,11 @@ files:
|
|
|
109
115
|
- lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb
|
|
110
116
|
- lib/ai_refactor/refactors/rspec/minitest_to_rspec.md
|
|
111
117
|
- lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb
|
|
118
|
+
- lib/ai_refactor/refactors/ruby/refactor_ruby.md
|
|
119
|
+
- lib/ai_refactor/refactors/ruby/refactor_ruby.rb
|
|
120
|
+
- lib/ai_refactor/refactors/ruby/write_ruby.md
|
|
121
|
+
- lib/ai_refactor/refactors/ruby/write_ruby.rb
|
|
122
|
+
- lib/ai_refactor/run_configuration.rb
|
|
112
123
|
- lib/ai_refactor/test_runners/minitest_runner.rb
|
|
113
124
|
- lib/ai_refactor/test_runners/rspec_runner.rb
|
|
114
125
|
- lib/ai_refactor/test_runners/test_run_diff_report.rb
|
|
@@ -135,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
135
146
|
- !ruby/object:Gem::Version
|
|
136
147
|
version: '0'
|
|
137
148
|
requirements: []
|
|
138
|
-
rubygems_version: 3.4.
|
|
149
|
+
rubygems_version: 3.4.19
|
|
139
150
|
signing_key:
|
|
140
151
|
specification_version: 4
|
|
141
152
|
summary: Use AI to convert a Rails RSpec test suite to minitest.
|