rubyn 0.1.0 → 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 -131
- 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 +34 -13
- data/lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css +1 -0
- 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,141 +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
|
-
# Match path header lines directly above ```ruby blocks
|
|
63
|
-
# Handles: `[NEW] path.rb`, `path.rb`, backtick-wrapped or plain
|
|
64
|
-
response.scan(/(?:\[NEW\]\s*)?`?([a-zA-Z0-9_\/\.\-]+\.rb)`?\s*\n```ruby\n(.*?)```/m) do
|
|
65
|
-
path = $1
|
|
66
|
-
code = $2
|
|
67
|
-
is_new = $~[0].include?("[NEW]")
|
|
68
|
-
blocks << { tag: is_new ? "NEW" : nil, path: path, code: code }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Fallback: no path headers found, grab first code block
|
|
72
|
-
if blocks.empty?
|
|
73
|
-
code_match = response.match(/```ruby\n(.*?)```/m)
|
|
74
|
-
blocks << { tag: nil, path: nil, code: code_match[1] } if code_match
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
blocks
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def prompt_apply_single(file, original, new_content, formatter)
|
|
81
|
-
print "\nApply changes? (y/n/diff) "
|
|
82
|
-
choice = $stdin.gets&.strip&.downcase
|
|
83
|
-
|
|
84
|
-
case choice
|
|
85
|
-
when "y"
|
|
86
|
-
File.write(file, new_content)
|
|
87
|
-
formatter.success("Changes applied to #{file}")
|
|
88
|
-
when "diff"
|
|
89
|
-
Rubyn::Output::DiffRenderer.render(original: original, modified: new_content)
|
|
90
|
-
print "\nApply changes? (y/n) "
|
|
91
|
-
if $stdin.gets&.strip&.downcase == "y"
|
|
92
|
-
File.write(file, new_content)
|
|
93
|
-
formatter.success("Changes applied to #{file}")
|
|
94
|
-
else
|
|
95
|
-
formatter.info("Changes discarded.")
|
|
96
|
-
end
|
|
97
|
-
else
|
|
98
|
-
formatter.info("Changes discarded.")
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def prompt_apply_multi(file, original, file_blocks, formatter)
|
|
103
|
-
formatter.newline
|
|
104
|
-
formatter.info("This refactor produces #{file_blocks.length} file(s):")
|
|
105
|
-
file_blocks.each do |block|
|
|
106
|
-
label = block[:tag] == "NEW" ? "NEW" : "MODIFIED"
|
|
107
|
-
path = block[:path] || file
|
|
108
|
-
formatter.info(" [#{label}] #{path}")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
print "\nApply all changes? (y/n/select) "
|
|
112
|
-
choice = $stdin.gets&.strip&.downcase
|
|
113
|
-
|
|
114
|
-
case choice
|
|
115
|
-
when "y"
|
|
116
|
-
apply_all_blocks(file, original, file_blocks, formatter)
|
|
117
|
-
when "select"
|
|
118
|
-
apply_selected_blocks(file, original, file_blocks, formatter)
|
|
119
|
-
else
|
|
120
|
-
formatter.info("All changes discarded.")
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def apply_all_blocks(file, original, file_blocks, formatter)
|
|
125
|
-
file_blocks.each do |block|
|
|
126
|
-
write_block(file, block, formatter)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def apply_selected_blocks(file, original, file_blocks, formatter)
|
|
131
|
-
file_blocks.each do |block|
|
|
132
|
-
label = block[:tag] == "NEW" ? "NEW" : "MODIFIED"
|
|
133
|
-
path = block[:path] || file
|
|
134
|
-
print " Apply [#{label}] #{path}? (y/n/diff) "
|
|
135
|
-
choice = $stdin.gets&.strip&.downcase
|
|
136
|
-
|
|
137
|
-
case choice
|
|
138
|
-
when "y"
|
|
139
|
-
write_block(file, block, formatter)
|
|
140
|
-
when "diff"
|
|
141
|
-
existing = block[:tag] == "NEW" ? "" : File.read(resolve_path(file, block))
|
|
142
|
-
Rubyn::Output::DiffRenderer.render(original: existing, modified: block[:code])
|
|
143
|
-
print " Apply? (y/n) "
|
|
144
|
-
if $stdin.gets&.strip&.downcase == "y"
|
|
145
|
-
write_block(file, block, formatter)
|
|
146
|
-
else
|
|
147
|
-
formatter.info(" Skipped #{path}")
|
|
148
|
-
end
|
|
149
|
-
else
|
|
150
|
-
formatter.info(" Skipped #{path}")
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def write_block(original_file, block, formatter)
|
|
156
|
-
target = resolve_path(original_file, block)
|
|
157
|
-
dir = File.dirname(target)
|
|
158
|
-
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
159
|
-
File.write(target, block[:code])
|
|
160
|
-
label = block[:tag] == "NEW" ? "Created" : "Updated"
|
|
161
|
-
formatter.success("#{label} #{target}")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def resolve_path(original_file, block)
|
|
165
|
-
return original_file unless block[:path]
|
|
166
|
-
|
|
167
|
-
if block[:path].start_with?("/")
|
|
168
|
-
block[:path]
|
|
169
|
-
else
|
|
170
|
-
File.join(Dir.pwd, block[:path])
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
48
|
end
|
|
174
49
|
end
|
|
175
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
|
|
@@ -293,22 +293,24 @@
|
|
|
293
293
|
|
|
294
294
|
// Build the file-to-code mapping
|
|
295
295
|
// Headers and code blocks are in the same order — zip them together
|
|
296
|
+
// Determine NEW vs MODIFIED by comparing path against the original file
|
|
296
297
|
var fileChanges = codeBlocks.map(function(code, i) {
|
|
297
298
|
var header = fileHeaders[i];
|
|
299
|
+
var path = (header && header.path) ? header.path : file;
|
|
300
|
+
var isNew = !!(path && file && path !== file && path.indexOf(file) === -1 && file.indexOf(path) === -1);
|
|
298
301
|
return {
|
|
299
|
-
path:
|
|
300
|
-
|
|
302
|
+
path: path,
|
|
303
|
+
isNew: isNew,
|
|
301
304
|
code: code
|
|
302
305
|
};
|
|
303
306
|
});
|
|
304
307
|
|
|
305
|
-
// Show each code block with its file path
|
|
308
|
+
// Show each code block with its file path and NEW badge if applicable
|
|
306
309
|
fileChanges.forEach(function(change, i) {
|
|
307
310
|
html += '<div class="rubyn-code-block">';
|
|
308
311
|
html += '<div class="rubyn-code-block-header">';
|
|
309
|
-
if (change.
|
|
310
|
-
|
|
311
|
-
html += '<span class="rubyn-file-tag ' + tagClass + '">' + change.tag + '</span>';
|
|
312
|
+
if (change.isNew) {
|
|
313
|
+
html += '<span class="rubyn-file-tag rubyn-tag-new">NEW</span>';
|
|
312
314
|
}
|
|
313
315
|
html += '<span class="rubyn-tool-filepath">' + escapeHtml(change.path) + '</span>';
|
|
314
316
|
html += '<div class="rubyn-code-block-actions">';
|
|
@@ -478,14 +480,33 @@
|
|
|
478
480
|
|
|
479
481
|
function extractFileHeaders(text) {
|
|
480
482
|
var headers = [];
|
|
483
|
+
var parts = text.split(/(```ruby\n[\s\S]*?```)/g);
|
|
481
484
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
var
|
|
488
|
-
|
|
485
|
+
for (var i = 0; i < parts.length; i++) {
|
|
486
|
+
if (parts[i].indexOf("```ruby\n") !== 0) continue;
|
|
487
|
+
|
|
488
|
+
var code = parts[i].replace(/^```ruby\n/, "").replace(/```$/, "");
|
|
489
|
+
var preceding = i > 0 ? parts[i - 1] : "";
|
|
490
|
+
var path = null;
|
|
491
|
+
|
|
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);
|
|
494
|
+
if (boldMatch) path = boldMatch[1];
|
|
495
|
+
|
|
496
|
+
// Strategy 3: Backtick-wrapped path above code block
|
|
497
|
+
if (!path) {
|
|
498
|
+
var tickMatch = preceding.match(/`([a-zA-Z0-9_\/\.\-]+\.rb)`\s*$/);
|
|
499
|
+
if (tickMatch) path = tickMatch[1];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Strategy 4: Path as comment on first line inside code block
|
|
503
|
+
if (!path) {
|
|
504
|
+
var firstLine = code.split("\n")[0].trim();
|
|
505
|
+
var commentMatch = firstLine.match(/^#\s*([a-zA-Z0-9_\/\.\-]+\.rb)/);
|
|
506
|
+
if (commentMatch) path = commentMatch[1];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
headers.push({ path: path });
|
|
489
510
|
}
|
|
490
511
|
|
|
491
512
|
return headers;
|
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
|