ai_refactor 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725fb1da5fc311d3740687d97fa3b6c3c4292a3b531f467ea1d8ff9e734608bc
4
- data.tar.gz: 4c16d310c643ae1158442816a9f863961ae151bb4ae486c965e96340342e2d44
3
+ metadata.gz: 15f32bc208d6025b339dd08061747f790975c9163909a83dac8d3abc381b4fac
4
+ data.tar.gz: cb1e71b84e2aa8b77900a5a33f10090f3ee65d6d6f0fba606359c5a2cb1f2719
5
5
  SHA512:
6
- metadata.gz: d0f380ef54a29be017b23c2caaa4491ee6304dc410e52ab6e11038b9f30c30a09a04597f3342332f464007e0e932f5b81112f712d46c584b2728d51bee61c036
7
- data.tar.gz: 9ffa29857815cb73daef7d6902bf7301dc80e36361b0451d3d3df1b7921b1c0978ef16ea7963c544b893db1bd0d6db34120378aa6a7ae4ac5add0831e4b61dbf
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
@@ -12,3 +12,10 @@ gem "minitest", "~> 5.0"
12
12
  gem "standard", "~> 1.3"
13
13
 
14
14
  gem "dotenv"
15
+
16
+ # for the examples
17
+
18
+ gem "rails"
19
+ gem "rspec"
20
+ gem "rspec-rails"
21
+ gem "shoulda-matchers"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ai_refactor (0.4.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
- # AI Refactor for Ruby
1
+ # AIRefactor for Ruby
2
2
 
3
- AI Refactor is an experimental tool to use AI to help apply refactoring to code.
3
+ __The goal for AIRefactor is to use LLMs to apply repetitive refactoring tasks to code.__
4
4
 
5
- __The goal for AI Refactor is to help apply repetitive refactoring tasks, not to replace human mind that decides what refactoring is needed.__
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
- The tool lets the human user prompt the AI with explicit refactoring tasks, and can be run on one or more files at a time.
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
- The focus of the tool is work with the Ruby programming language ecosystem, but it can be used with any language.
22
+ See the [examples](examples/) directory for some examples of using the tool.
13
23
 
14
24
  ## Available refactors
15
25
 
16
- Currently available:
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
- - `generic`: provide your own prompt for the AI and run against the input files
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 REFACTOR_TYPE INPUT_FILE_OR_DIR [options]
96
+ Usage: ai_refactor REFACTOR_TYPE_OR_COMMAND_FILE INPUT_FILE_OR_DIR [options]
79
97
 
80
- Where REFACTOR_TYPE is one of: ["generic" ... (run ai_refactor --help for full list of refactor types)]
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
 
@@ -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
- 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,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
- 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 "Loaded config from '#{options_from_config_file}'..." if options_from_config_file
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}'..."
142
194
  end
143
195
 
144
- job = ::AIRefactor::Cli.new(refactoring_type: arguments.shift, inputs: arguments, options: options, logger: logger)
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
- puts option_parser.help
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 1
205
+ exit false
153
206
  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
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
@@ -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
 
@@ -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 prompt_file_path
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 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
+ __{{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,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
@@ -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: "bundle exec rails test __FILE__")
8
+ def initialize(file_path, command_template: "ruby __FILE__")
9
9
  @file_path = file_path
10
10
  @command_template = command_template
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIRefactor
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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-08-15 00:00:00.000000000 Z
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/generic.rb
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.10
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.