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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b1ca55e10bd9ae4ec17fbdbfa196577fda468788c083905679a2c1e7e782181
4
- data.tar.gz: f82e928da5975b754dca641f00488bb6360ffbd4cd45d538909348a4b1ff6b05
3
+ metadata.gz: db58e485a2422294e1c127b5a6347ac42304534013835d28c2fc67d59aea06df
4
+ data.tar.gz: 8e5ff68de409481dd622746ccb4331465db048df15743a357f18ac80a5a239c3
5
5
  SHA512:
6
- metadata.gz: da671165752719684baaf371f4c1a47d7a9f3e8604af37360b106827d15c08a1508219b7fc47ae859a1c8f54f9e756a7e7a459c66fff4cbb718e70610eb4684a
7
- data.tar.gz: 7f6f19c30b1ba42e74aeb0f3b0ec936d8f14ac63b6d52949d87c0714fce3b76c041c3cc8bf67a8eb07c36453b6d7602ae35736c46b1771646ce71bcebac748b4
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
- 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
- # 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: header ? header.path : file,
300
- tag: header ? header.tag : null,
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, tag, and individual apply button
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.tag) {
310
- var tagClass = change.tag === "NEW" ? "rubyn-tag-new" : "rubyn-tag-modified";
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
- // Match path headers directly above ```ruby blocks
483
- // Handles: `[NEW] path.rb`, `path.rb`, #### path.rb, etc.
484
- var regex = /(?:\[NEW\]\s*)?`?([a-zA-Z0-9_\/\.\-]+\.rb)`?\s*\n```ruby/g;
485
- var match;
486
- while ((match = regex.exec(text)) !== null) {
487
- var isNew = match[0].indexOf("[NEW]") !== -1;
488
- headers.push({ tag: isNew ? "NEW" : null, path: match[1] });
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;
@@ -735,6 +735,7 @@ body {
735
735
  color: var(--rubyn-success);
736
736
  }
737
737
 
738
+
738
739
  .rubyn-code-block .rubyn-tool-output {
739
740
  margin: 0;
740
741
  padding: 1rem;
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.0"
4
+ VERSION = "0.1.2"
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.0
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