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