rubyn 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/rubyn/commands/refactor.rb +6 -140
- data/lib/rubyn/context/file_applier.rb +114 -0
- data/lib/rubyn/context/response_parser.rb +56 -0
- data/lib/rubyn/engine/app/assets/javascripts/rubyn/application.js +6 -7
- data/lib/rubyn/version.rb +1 -1
- data/lib/rubyn.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db58e485a2422294e1c127b5a6347ac42304534013835d28c2fc67d59aea06df
|
|
4
|
+
data.tar.gz: 8e5ff68de409481dd622746ccb4331465db048df15743a357f18ac80a5a239c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb4224d7a928da8a211c3dc9e10e1c3753f8f524b55a79dbb3bfb89905a887ff5c1762a5a49c625d4a8b892826557d39aa498a8ba3f483ccb43da29212b734b6
|
|
7
|
+
data.tar.gz: a7807763294cc0defae24600c534736426d25653bd868c67c0c21b6fd161c4dbc32a3e4c33926a88b779b54f06b16db8e5874efd384ad46f79c72d56752a05cb
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
3
|
module Rubyn
|
|
6
4
|
module Commands
|
|
7
5
|
class Refactor < Base
|
|
@@ -35,150 +33,18 @@ module Rubyn
|
|
|
35
33
|
formatter.newline
|
|
36
34
|
formatter.credit_usage(result["credits_used"], nil) if result["credits_used"]
|
|
37
35
|
formatter.newline
|
|
38
|
-
|
|
36
|
+
|
|
37
|
+
file_blocks = Context::ResponseParser.extract_file_blocks(full_response)
|
|
38
|
+
if file_blocks.any?
|
|
39
|
+
applier = Context::FileApplier.new(original_file: file, formatter: formatter)
|
|
40
|
+
applier.apply(file_blocks)
|
|
41
|
+
end
|
|
39
42
|
rescue Rubyn::AuthenticationError => e
|
|
40
43
|
formatter.error("Authentication failed: #{e.message}")
|
|
41
44
|
rescue Rubyn::APIError => e
|
|
42
45
|
formatter.error(e.message)
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def prompt_apply(file, original, response, formatter)
|
|
49
|
-
file_blocks = extract_file_blocks(response)
|
|
50
|
-
return if file_blocks.empty?
|
|
51
|
-
|
|
52
|
-
if file_blocks.length == 1 && file_blocks.first[:tag].nil?
|
|
53
|
-
prompt_apply_single(file, original, file_blocks.first[:code], formatter)
|
|
54
|
-
else
|
|
55
|
-
prompt_apply_multi(file, original, file_blocks, formatter)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def extract_file_blocks(response)
|
|
60
|
-
blocks = []
|
|
61
|
-
|
|
62
|
-
# Split response around code blocks, keeping surrounding text for path detection
|
|
63
|
-
parts = response.split(/(```ruby\n.*?```)/m)
|
|
64
|
-
|
|
65
|
-
parts.each_with_index do |part, i|
|
|
66
|
-
next unless part.start_with?("```ruby\n")
|
|
67
|
-
|
|
68
|
-
code = part.sub(/\A```ruby\n/, "").sub(/```\z/, "")
|
|
69
|
-
preceding_text = i > 0 ? parts[i - 1] : ""
|
|
70
|
-
|
|
71
|
-
# Strategy 1: Path in bold header above code block
|
|
72
|
-
# **New file: app/services/foo.rb** or **app/services/foo.rb**
|
|
73
|
-
path = preceding_text.match(/\*\*(?:New file:\s*|Modified:\s*)?([a-zA-Z0-9_\/\.\-]+\.rb)\*\*/i)&.dig(1)
|
|
74
|
-
|
|
75
|
-
# Strategy 2: Path as backtick-wrapped line above code block
|
|
76
|
-
path ||= preceding_text.match(/`([a-zA-Z0-9_\/\.\-]+\.rb)`\s*\z/)&.dig(1)
|
|
77
|
-
|
|
78
|
-
# Strategy 3: Path as comment on first line inside code block
|
|
79
|
-
path ||= code.lines.first&.strip&.match(/^#\s*([a-zA-Z0-9_\/\.\-]+\.rb)/)&.dig(1)
|
|
80
|
-
|
|
81
|
-
blocks << { path: path, code: code }
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
blocks
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def prompt_apply_single(file, original, new_content, formatter)
|
|
88
|
-
print "\nApply changes? (y/n/diff) "
|
|
89
|
-
choice = $stdin.gets&.strip&.downcase
|
|
90
|
-
|
|
91
|
-
case choice
|
|
92
|
-
when "y"
|
|
93
|
-
File.write(file, new_content)
|
|
94
|
-
formatter.success("Changes applied to #{file}")
|
|
95
|
-
when "diff"
|
|
96
|
-
Rubyn::Output::DiffRenderer.render(original: original, modified: new_content)
|
|
97
|
-
print "\nApply changes? (y/n) "
|
|
98
|
-
if $stdin.gets&.strip&.downcase == "y"
|
|
99
|
-
File.write(file, new_content)
|
|
100
|
-
formatter.success("Changes applied to #{file}")
|
|
101
|
-
else
|
|
102
|
-
formatter.info("Changes discarded.")
|
|
103
|
-
end
|
|
104
|
-
else
|
|
105
|
-
formatter.info("Changes discarded.")
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def prompt_apply_multi(file, original, file_blocks, formatter)
|
|
110
|
-
formatter.newline
|
|
111
|
-
formatter.info("This refactor produces #{file_blocks.length} file(s):")
|
|
112
|
-
file_blocks.each do |block|
|
|
113
|
-
path = block[:path] || file
|
|
114
|
-
is_new = path != file && !path.end_with?("/#{file}") && !file.end_with?("/#{path}")
|
|
115
|
-
label = is_new ? "NEW" : "MODIFIED"
|
|
116
|
-
formatter.info(" [#{label}] #{path}")
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
print "\nApply all changes? (y/n/select) "
|
|
120
|
-
choice = $stdin.gets&.strip&.downcase
|
|
121
|
-
|
|
122
|
-
case choice
|
|
123
|
-
when "y"
|
|
124
|
-
apply_all_blocks(file, original, file_blocks, formatter)
|
|
125
|
-
when "select"
|
|
126
|
-
apply_selected_blocks(file, original, file_blocks, formatter)
|
|
127
|
-
else
|
|
128
|
-
formatter.info("All changes discarded.")
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def apply_all_blocks(file, original, file_blocks, formatter)
|
|
133
|
-
file_blocks.each do |block|
|
|
134
|
-
write_block(file, block, formatter)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def apply_selected_blocks(file, original, file_blocks, formatter)
|
|
139
|
-
file_blocks.each do |block|
|
|
140
|
-
path = block[:path] || file
|
|
141
|
-
is_new = path != file && !path.end_with?("/#{file}") && !file.end_with?("/#{path}")
|
|
142
|
-
label = is_new ? "NEW" : "MODIFIED"
|
|
143
|
-
print " Apply [#{label}] #{path}? (y/n/diff) "
|
|
144
|
-
choice = $stdin.gets&.strip&.downcase
|
|
145
|
-
|
|
146
|
-
case choice
|
|
147
|
-
when "y"
|
|
148
|
-
write_block(file, block, formatter)
|
|
149
|
-
when "diff"
|
|
150
|
-
existing = is_new ? "" : File.read(resolve_path(file, block))
|
|
151
|
-
Rubyn::Output::DiffRenderer.render(original: existing, modified: block[:code])
|
|
152
|
-
print " Apply? (y/n) "
|
|
153
|
-
if $stdin.gets&.strip&.downcase == "y"
|
|
154
|
-
write_block(file, block, formatter)
|
|
155
|
-
else
|
|
156
|
-
formatter.info(" Skipped #{path}")
|
|
157
|
-
end
|
|
158
|
-
else
|
|
159
|
-
formatter.info(" Skipped #{path}")
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def write_block(original_file, block, formatter)
|
|
165
|
-
target = resolve_path(original_file, block)
|
|
166
|
-
dir = File.dirname(target)
|
|
167
|
-
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
168
|
-
File.write(target, block[:code])
|
|
169
|
-
label = block[:tag] == "NEW" ? "Created" : "Updated"
|
|
170
|
-
formatter.success("#{label} #{target}")
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def resolve_path(original_file, block)
|
|
174
|
-
return original_file unless block[:path]
|
|
175
|
-
|
|
176
|
-
if block[:path].start_with?("/")
|
|
177
|
-
block[:path]
|
|
178
|
-
else
|
|
179
|
-
File.join(Dir.pwd, block[:path])
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
48
|
end
|
|
183
49
|
end
|
|
184
50
|
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rubyn
|
|
6
|
+
module Context
|
|
7
|
+
class FileApplier
|
|
8
|
+
def initialize(original_file:, formatter:)
|
|
9
|
+
@original_file = original_file
|
|
10
|
+
@formatter = formatter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def apply(file_blocks)
|
|
14
|
+
if file_blocks.length == 1 && file_blocks.first[:path].nil?
|
|
15
|
+
apply_single(file_blocks.first[:code])
|
|
16
|
+
else
|
|
17
|
+
apply_multi(file_blocks)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def new_file?(path)
|
|
22
|
+
return false unless path
|
|
23
|
+
|
|
24
|
+
path != @original_file &&
|
|
25
|
+
!path.end_with?("/#{@original_file}") &&
|
|
26
|
+
!@original_file.end_with?("/#{path}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def apply_single(code)
|
|
32
|
+
print "\nApply changes? (y/n/diff) "
|
|
33
|
+
choice = $stdin.gets&.strip&.downcase
|
|
34
|
+
|
|
35
|
+
case choice
|
|
36
|
+
when "y"
|
|
37
|
+
write_file(@original_file, code)
|
|
38
|
+
when "diff"
|
|
39
|
+
original_content = File.read(@original_file)
|
|
40
|
+
Rubyn::Output::DiffRenderer.render(original: original_content, modified: code)
|
|
41
|
+
print "\nApply changes? (y/n) "
|
|
42
|
+
write_file(@original_file, code) if $stdin.gets&.strip&.downcase == "y"
|
|
43
|
+
else
|
|
44
|
+
@formatter.info("Changes discarded.")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_multi(file_blocks)
|
|
49
|
+
@formatter.newline
|
|
50
|
+
@formatter.info("This refactor produces #{file_blocks.length} file(s):")
|
|
51
|
+
file_blocks.each do |block|
|
|
52
|
+
path = block[:path] || @original_file
|
|
53
|
+
label = new_file?(path) ? "NEW" : "MODIFIED"
|
|
54
|
+
@formatter.info(" [#{label}] #{path}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
print "\nApply all changes? (y/n/select) "
|
|
58
|
+
choice = $stdin.gets&.strip&.downcase
|
|
59
|
+
|
|
60
|
+
case choice
|
|
61
|
+
when "y"
|
|
62
|
+
file_blocks.each { |block| write_block(block) }
|
|
63
|
+
when "select"
|
|
64
|
+
file_blocks.each { |block| apply_selected(block) }
|
|
65
|
+
else
|
|
66
|
+
@formatter.info("All changes discarded.")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply_selected(block)
|
|
71
|
+
path = block[:path] || @original_file
|
|
72
|
+
label = new_file?(path) ? "NEW" : "MODIFIED"
|
|
73
|
+
print " Apply [#{label}] #{path}? (y/n/diff) "
|
|
74
|
+
choice = $stdin.gets&.strip&.downcase
|
|
75
|
+
|
|
76
|
+
case choice
|
|
77
|
+
when "y"
|
|
78
|
+
write_block(block)
|
|
79
|
+
when "diff"
|
|
80
|
+
existing = new_file?(path) ? "" : File.read(resolve_path(block))
|
|
81
|
+
Rubyn::Output::DiffRenderer.render(original: existing, modified: block[:code])
|
|
82
|
+
print " Apply? (y/n) "
|
|
83
|
+
write_block(block) if $stdin.gets&.strip&.downcase == "y"
|
|
84
|
+
else
|
|
85
|
+
@formatter.info(" Skipped #{path}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def write_block(block)
|
|
90
|
+
target = resolve_path(block)
|
|
91
|
+
write_file(target, block[:code])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def write_file(path, code)
|
|
95
|
+
dir = File.dirname(path)
|
|
96
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
97
|
+
created = !File.exist?(path)
|
|
98
|
+
File.write(path, code)
|
|
99
|
+
label = created ? "Created" : "Updated"
|
|
100
|
+
@formatter.success("#{label} #{path}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_path(block)
|
|
104
|
+
return @original_file unless block[:path]
|
|
105
|
+
|
|
106
|
+
if block[:path].start_with?("/")
|
|
107
|
+
block[:path]
|
|
108
|
+
else
|
|
109
|
+
File.join(Dir.pwd, block[:path])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyn
|
|
4
|
+
module Context
|
|
5
|
+
class ResponseParser
|
|
6
|
+
BOLD_HEADER = /\*\*(?:(?:New|Updated|Modified)\s*(?:file)?:\s*)?([a-zA-Z0-9_\/\.\-]+\.rb)\*\*/i
|
|
7
|
+
BACKTICK_PATH = /`([a-zA-Z0-9_\/\.\-]+\.rb)`\s*\z/
|
|
8
|
+
INLINE_COMMENT = /^#\s*([a-zA-Z0-9_\/\.\-]+\.rb)/
|
|
9
|
+
|
|
10
|
+
def self.extract_file_blocks(response)
|
|
11
|
+
new(response).extract
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(response)
|
|
15
|
+
@response = response
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract
|
|
19
|
+
blocks = []
|
|
20
|
+
parts = @response.split(/(```ruby\n.*?```)/m)
|
|
21
|
+
|
|
22
|
+
parts.each_with_index do |part, i|
|
|
23
|
+
next unless part.start_with?("```ruby\n")
|
|
24
|
+
|
|
25
|
+
code = part.sub(/\A```ruby\n/, "").sub(/```\z/, "")
|
|
26
|
+
preceding = i > 0 ? parts[i - 1] : ""
|
|
27
|
+
path = detect_path(preceding, code)
|
|
28
|
+
|
|
29
|
+
blocks << { path: path, code: code }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
blocks
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def detect_path(preceding_text, code)
|
|
38
|
+
match_bold_header(preceding_text) ||
|
|
39
|
+
match_backtick_path(preceding_text) ||
|
|
40
|
+
match_inline_comment(code)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def match_bold_header(text)
|
|
44
|
+
text.match(BOLD_HEADER)&.[](1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def match_backtick_path(text)
|
|
48
|
+
text.match(BACKTICK_PATH)&.[](1)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def match_inline_comment(code)
|
|
52
|
+
code.lines.first&.strip&.match(INLINE_COMMENT)&.[](1)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -296,8 +296,8 @@
|
|
|
296
296
|
// Determine NEW vs MODIFIED by comparing path against the original file
|
|
297
297
|
var fileChanges = codeBlocks.map(function(code, i) {
|
|
298
298
|
var header = fileHeaders[i];
|
|
299
|
-
var path = header ? header.path : file;
|
|
300
|
-
var isNew = path !== file &&
|
|
299
|
+
var path = (header && header.path) ? header.path : file;
|
|
300
|
+
var isNew = !!(path && file && path !== file && path.indexOf(file) === -1 && file.indexOf(path) === -1);
|
|
301
301
|
return {
|
|
302
302
|
path: path,
|
|
303
303
|
isNew: isNew,
|
|
@@ -489,18 +489,17 @@
|
|
|
489
489
|
var preceding = i > 0 ? parts[i - 1] : "";
|
|
490
490
|
var path = null;
|
|
491
491
|
|
|
492
|
-
// Strategy 1: Bold header
|
|
493
|
-
|
|
494
|
-
var boldMatch = preceding.match(/\*\*(?:New file:\s*|Modified:\s*)?([a-zA-Z0-9_\/\.\-]+\.rb)\*\*/i);
|
|
492
|
+
// Strategy 1: Bold header (**New file: path.rb**, **Updated file: path.rb**, **Modified: path.rb**)
|
|
493
|
+
var boldMatch = preceding.match(/\*\*(?:(?:New|Updated|Modified)\s*(?:file)?:\s*)?([a-zA-Z0-9_\/\.\-]+\.rb)\*\*/i);
|
|
495
494
|
if (boldMatch) path = boldMatch[1];
|
|
496
495
|
|
|
497
|
-
// Strategy
|
|
496
|
+
// Strategy 3: Backtick-wrapped path above code block
|
|
498
497
|
if (!path) {
|
|
499
498
|
var tickMatch = preceding.match(/`([a-zA-Z0-9_\/\.\-]+\.rb)`\s*$/);
|
|
500
499
|
if (tickMatch) path = tickMatch[1];
|
|
501
500
|
}
|
|
502
501
|
|
|
503
|
-
// Strategy
|
|
502
|
+
// Strategy 4: Path as comment on first line inside code block
|
|
504
503
|
if (!path) {
|
|
505
504
|
var firstLine = code.split("\n")[0].trim();
|
|
506
505
|
var commentMatch = firstLine.match(/^#\s*([a-zA-Z0-9_\/\.\-]+\.rb)/);
|
data/lib/rubyn/version.rb
CHANGED
data/lib/rubyn.rb
CHANGED
|
@@ -52,6 +52,8 @@ require_relative "rubyn/context/project_scanner"
|
|
|
52
52
|
require_relative "rubyn/context/file_resolver"
|
|
53
53
|
require_relative "rubyn/context/context_builder"
|
|
54
54
|
require_relative "rubyn/context/codebase_indexer"
|
|
55
|
+
require_relative "rubyn/context/response_parser"
|
|
56
|
+
require_relative "rubyn/context/file_applier"
|
|
55
57
|
require_relative "rubyn/output/formatter"
|
|
56
58
|
require_relative "rubyn/output/diff_renderer"
|
|
57
59
|
require_relative "rubyn/output/spinner"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubyn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- matthewsuttles
|
|
@@ -297,8 +297,10 @@ files:
|
|
|
297
297
|
- lib/rubyn/config/settings.rb
|
|
298
298
|
- lib/rubyn/context/codebase_indexer.rb
|
|
299
299
|
- lib/rubyn/context/context_builder.rb
|
|
300
|
+
- lib/rubyn/context/file_applier.rb
|
|
300
301
|
- lib/rubyn/context/file_resolver.rb
|
|
301
302
|
- lib/rubyn/context/project_scanner.rb
|
|
303
|
+
- lib/rubyn/context/response_parser.rb
|
|
302
304
|
- lib/rubyn/engine/app/assets/images/rubyn/RubynLogo.png
|
|
303
305
|
- lib/rubyn/engine/app/assets/javascripts/rubyn/application.js
|
|
304
306
|
- lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css
|