rubyn 0.1.1 → 0.1.3

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: da809f272fcbe6c989b0a09ff5917f4aa91db8b345dd29b85d2229bfe2227421
4
- data.tar.gz: 78a8fb3249fb87a9696d73f8d4622e9421dd0d4254732eb292cb4e9f11c07f59
3
+ metadata.gz: 4a406bccd1e8bba621fd7465b792b8630c2dd586118d9116e1ad178061fe39dd
4
+ data.tar.gz: a49480476f8293d38a2998a0df0f6b1144ccc477dec6982ac853038a93866d0a
5
5
  SHA512:
6
- metadata.gz: 1ca7d13fd823a0089510cb255ed082ee1213beb066c435374013848305102ecb95bf14b5b31f285da9029a1cc84a0071a8925a772348c7626254f38317f78980
7
- data.tar.gz: b9a529dea633b3e2ab4ee0a92b6cb7dec54c8466a4458de65365498a14a13e06b795c7c1b6a0896d85ab135ce8f9700ebb7c1b11d23d58b4670774d4c89c4695
6
+ metadata.gz: a68aa5fac1c7c708e678976e89818bc0cc108ab1b4e902d6f32ec82d71872ddea82f060db656d9829fa095cba6655b9de3b24464ec8d151fcfbf4846fde8fe83
7
+ data.tar.gz: edb357abf5f2e8e0c106fe35a22866db7c4a70dda97d5edc9b90ff8c93077d99d1e148f780346b5ba90b7b88cdb9ba900ef8e610e0b62186dbdb4703258af8d6
@@ -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
- prompt_apply(file, file_content, full_response, formatter)
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 && !path.endsWith("/" + file) && !file.endsWith("/" + path);
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,18 @@
489
489
  var preceding = i > 0 ? parts[i - 1] : "";
490
490
  var path = null;
491
491
 
492
- // Strategy 1: Bold header above code block
493
- // **New file: path.rb** or **path.rb**
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**)
493
+ // Only match when preceded by New/Updated/Modified to avoid matching bold .rb references in prose
494
+ var boldMatch = preceding.match(/\*\*(?:New|Updated|Modified)\s*(?:file)?:\s*([a-zA-Z0-9_\/\.\-]+\.rb)\*\*/i);
495
495
  if (boldMatch) path = boldMatch[1];
496
496
 
497
- // Strategy 2: Backtick-wrapped path above code block
497
+ // Strategy 3: Backtick-wrapped path above code block
498
498
  if (!path) {
499
499
  var tickMatch = preceding.match(/`([a-zA-Z0-9_\/\.\-]+\.rb)`\s*$/);
500
500
  if (tickMatch) path = tickMatch[1];
501
501
  }
502
502
 
503
- // Strategy 3: Path as comment on first line inside code block
503
+ // Strategy 4: Path as comment on first line inside code block
504
504
  if (!path) {
505
505
  var firstLine = code.split("\n")[0].trim();
506
506
  var commentMatch = firstLine.match(/^#\s*([a-zA-Z0-9_\/\.\-]+\.rb)/);
data/lib/rubyn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyn
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
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.1
4
+ version: 0.1.3
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