ruby_raider 2.0.0 → 3.0.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.
Files changed (224) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/steep.yml +21 -0
  4. data/.gitignore +1 -1
  5. data/.reek.yml +46 -4
  6. data/.ruby-version +1 -1
  7. data/README.md +138 -77
  8. data/Steepfile +22 -0
  9. data/assets/ruby_raider_logo.svg +51 -0
  10. data/lib/adopter/adopt_menu.rb +11 -15
  11. data/lib/adopter/converters/base_converter.rb +1 -2
  12. data/lib/adopter/converters/identity_converter.rb +3 -6
  13. data/lib/adopter/migration_plan.rb +0 -1
  14. data/lib/adopter/plan_builder.rb +2 -5
  15. data/lib/adopter/project_analyzer.rb +1 -5
  16. data/lib/adopter/project_detector.rb +3 -5
  17. data/lib/commands/adopt_commands.rb +0 -1
  18. data/lib/commands/plugin_commands.rb +0 -2
  19. data/lib/commands/scaffolding_commands.rb +220 -37
  20. data/lib/commands/utility_commands.rb +82 -2
  21. data/lib/generators/automation/automation_generator.rb +0 -7
  22. data/lib/generators/automation/templates/partials/element.tt +1 -1
  23. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -7
  24. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  25. data/lib/generators/common_generator.rb +12 -0
  26. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  27. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  28. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  29. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  30. data/lib/generators/cucumber/templates/feature.tt +0 -4
  31. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  32. data/lib/generators/cucumber/templates/partials/capybara_env.tt +19 -1
  33. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  34. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  35. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +6 -12
  37. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  38. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  39. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  40. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  41. data/lib/generators/generator.rb +38 -7
  42. data/lib/generators/helper_generator.rb +24 -7
  43. data/lib/generators/infrastructure/templates/github.tt +1 -1
  44. data/lib/generators/infrastructure/templates/github_appium.tt +2 -2
  45. data/lib/generators/infrastructure/templates/gitlab.tt +1 -1
  46. data/lib/generators/invoke_generators.rb +42 -9
  47. data/lib/generators/menu_generator.rb +120 -11
  48. data/lib/generators/minitest/minitest_generator.rb +16 -4
  49. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  50. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  51. data/lib/generators/minitest/templates/test.tt +5 -34
  52. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  53. data/lib/generators/rspec/rspec_generator.rb +16 -4
  54. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  55. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  56. data/lib/generators/rspec/templates/spec.tt +5 -35
  57. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  58. data/lib/generators/template_renderer/partial_cache.rb +11 -1
  59. data/lib/generators/template_renderer/partial_resolver.rb +17 -10
  60. data/lib/generators/template_renderer.rb +17 -1
  61. data/lib/generators/templates/common/gemfile.tt +21 -6
  62. data/lib/generators/templates/common/git_ignore.tt +6 -1
  63. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  64. data/lib/generators/templates/common/partials/web_config.tt +16 -7
  65. data/lib/generators/templates/common/rakefile.tt +36 -0
  66. data/lib/generators/templates/common/read_me.tt +41 -91
  67. data/lib/generators/templates/common/rspec.tt +3 -0
  68. data/lib/generators/templates/common/ruby_version.tt +1 -0
  69. data/lib/generators/templates/helpers/allure_helper.tt +11 -0
  70. data/lib/generators/templates/helpers/browser_helper.tt +12 -2
  71. data/lib/generators/templates/helpers/capybara_helper.tt +5 -1
  72. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  73. data/lib/generators/templates/helpers/driver_helper.tt +2 -10
  74. data/lib/generators/templates/helpers/partials/appium_driver.tt +0 -2
  75. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  76. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  77. data/lib/generators/templates/helpers/partials/driver_and_options.tt +1 -3
  78. data/lib/generators/templates/helpers/partials/selenium_driver.tt +8 -7
  79. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  80. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  81. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  82. data/lib/generators/templates/helpers/spec_helper.tt +57 -8
  83. data/lib/generators/templates/helpers/test_helper.tt +69 -1
  84. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  85. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  86. data/lib/llm/client.rb +79 -0
  87. data/lib/llm/config.rb +57 -0
  88. data/lib/llm/prompts.rb +84 -0
  89. data/lib/llm/provider.rb +27 -0
  90. data/lib/llm/providers/anthropic_provider.rb +43 -0
  91. data/lib/llm/providers/ollama_provider.rb +56 -0
  92. data/lib/llm/providers/openai_provider.rb +42 -0
  93. data/lib/llm/response_parser.rb +67 -0
  94. data/lib/plugin/plugin.rb +22 -20
  95. data/lib/plugin/plugin_exposer.rb +16 -38
  96. data/lib/ruby_raider.rb +47 -12
  97. data/lib/scaffolding/crud_generator.rb +94 -0
  98. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  99. data/lib/scaffolding/name_normalizer.rb +63 -0
  100. data/lib/scaffolding/page_introspector.rb +45 -0
  101. data/lib/scaffolding/project_detector.rb +72 -0
  102. data/lib/scaffolding/scaffold_menu.rb +103 -0
  103. data/lib/scaffolding/scaffolding.rb +158 -11
  104. data/lib/scaffolding/templates/component.tt +30 -0
  105. data/lib/scaffolding/templates/feature.tt +4 -4
  106. data/lib/scaffolding/templates/helper.tt +15 -1
  107. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  108. data/lib/scaffolding/templates/page_object.tt +50 -1
  109. data/lib/scaffolding/templates/spec.tt +33 -2
  110. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  111. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  112. data/lib/scaffolding/templates/steps.tt +17 -5
  113. data/lib/scaffolding/url_analyzer.rb +179 -0
  114. data/lib/utilities/desktop_downloader.rb +177 -0
  115. data/lib/utilities/logo.rb +83 -0
  116. data/lib/utilities/utilities.rb +53 -20
  117. data/lib/version +1 -1
  118. data/ruby_raider.gemspec +1 -0
  119. data/sig/adopter/adopt_menu.rbs +25 -0
  120. data/sig/adopter/converters/base_converter.rbs +23 -0
  121. data/sig/adopter/converters/identity_converter.rbs +16 -0
  122. data/sig/adopter/migration_plan.rbs +34 -0
  123. data/sig/adopter/migrator.rbs +21 -0
  124. data/sig/adopter/plan_builder.rbs +38 -0
  125. data/sig/adopter/project_analyzer.rbs +39 -0
  126. data/sig/adopter/project_detector.rbs +26 -0
  127. data/sig/commands/adopt_commands.rbs +8 -0
  128. data/sig/commands/loaded_commands.rbs +5 -0
  129. data/sig/commands/plugin_commands.rbs +9 -0
  130. data/sig/commands/scaffolding_commands.rbs +28 -0
  131. data/sig/commands/utility_commands.rbs +21 -0
  132. data/sig/generators/automation/automation_generator.rbs +20 -0
  133. data/sig/generators/common_generator.rbs +12 -0
  134. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  135. data/sig/generators/generator.rbs +40 -0
  136. data/sig/generators/helper_generator.rbs +18 -0
  137. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  138. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  139. data/sig/generators/invoke_generators.rbs +10 -0
  140. data/sig/generators/menu_generator.rbs +29 -0
  141. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  142. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  143. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  144. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  145. data/sig/generators/template_renderer/template_error.rbs +19 -0
  146. data/sig/generators/template_renderer.rbs +10 -0
  147. data/sig/llm/client.rbs +15 -0
  148. data/sig/llm/config.rbs +20 -0
  149. data/sig/llm/prompts.rbs +8 -0
  150. data/sig/llm/provider.rbs +12 -0
  151. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  152. data/sig/llm/providers/ollama_provider.rbs +18 -0
  153. data/sig/llm/providers/openai_provider.rbs +16 -0
  154. data/sig/llm/response_parser.rbs +13 -0
  155. data/sig/plugin/plugin.rbs +24 -0
  156. data/sig/plugin/plugin_exposer.rbs +20 -0
  157. data/sig/ruby_raider.rbs +15 -0
  158. data/sig/scaffolding/crud_generator.rbs +16 -0
  159. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  160. data/sig/scaffolding/name_normalizer.rbs +17 -0
  161. data/sig/scaffolding/page_introspector.rbs +14 -0
  162. data/sig/scaffolding/project_detector.rbs +14 -0
  163. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  164. data/sig/scaffolding/scaffolding.rbs +55 -0
  165. data/sig/scaffolding/url_analyzer.rbs +28 -0
  166. data/sig/utilities/desktop_downloader.rbs +23 -0
  167. data/sig/utilities/logger.rbs +13 -0
  168. data/sig/utilities/logo.rbs +16 -0
  169. data/sig/utilities/utilities.rbs +30 -0
  170. data/sig/vendor/thor.rbs +34 -0
  171. data/sig/vendor/tty_prompt.rbs +15 -0
  172. data/spec/adopter/adopt_menu_spec.rb +12 -12
  173. data/spec/adopter/migration_plan_spec.rb +1 -1
  174. data/spec/adopter/migrator_spec.rb +2 -2
  175. data/spec/adopter/project_detector_spec.rb +1 -1
  176. data/spec/commands/raider_commands_spec.rb +129 -0
  177. data/spec/generators/generator_spec.rb +23 -0
  178. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  179. data/spec/integration/commands/utility_commands_spec.rb +23 -3
  180. data/spec/integration/content/ci_content_spec.rb +119 -0
  181. data/spec/integration/content/common_content_spec.rb +288 -0
  182. data/spec/integration/content/config_content_spec.rb +175 -0
  183. data/spec/integration/content/content_helper.rb +32 -0
  184. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  185. data/spec/integration/content/helper_content_spec.rb +485 -0
  186. data/spec/integration/content/page_content_spec.rb +259 -0
  187. data/spec/integration/content/reporter_content_spec.rb +236 -0
  188. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  189. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  190. data/spec/integration/content/test_content_spec.rb +266 -0
  191. data/spec/integration/end_to_end_features_spec.rb +690 -0
  192. data/spec/integration/end_to_end_spec.rb +52 -16
  193. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  194. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  195. data/spec/integration/generators/common_generator_spec.rb +12 -13
  196. data/spec/integration/generators/config_features_spec.rb +155 -0
  197. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  198. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  199. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  200. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  201. data/spec/integration/generators/reporter_spec.rb +159 -0
  202. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  203. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  204. data/spec/integration/settings_helper.rb +0 -3
  205. data/spec/integration/spec_helper.rb +30 -13
  206. data/spec/llm/client_spec.rb +79 -0
  207. data/spec/llm/config_spec.rb +92 -0
  208. data/spec/llm/prompts_spec.rb +49 -0
  209. data/spec/llm/response_parser_spec.rb +92 -0
  210. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  211. data/spec/menus/menu_generator_spec.rb +263 -0
  212. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  213. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  214. data/spec/scaffolding/scaffold_project_detector_spec.rb +104 -0
  215. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  216. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  217. data/spec/system/adopt_matrix_spec.rb +537 -0
  218. data/spec/system/adopt_spec.rb +225 -0
  219. data/spec/system/support/system_test_helper.rb +0 -2
  220. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  221. metadata +150 -5
  222. data/lib/generators/automation/templates/visual_options.tt +0 -16
  223. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  224. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+
6
+ class UrlAnalyzer
7
+ INTERACTIVE_TAGS = %w[input select textarea button].freeze
8
+
9
+ attr_reader :url, :page_name, :elements
10
+
11
+ def initialize(url, name_override: nil, ai: false) # rubocop:disable Naming/MethodParameterName
12
+ @url = url
13
+ @uri = URI.parse(url)
14
+ @page_name = name_override || derive_page_name
15
+ @elements = []
16
+ @ai = ai
17
+ end
18
+
19
+ def analyze
20
+ html = fetch_html
21
+ if @ai
22
+ ai_elements = analyze_with_llm(html)
23
+ @elements = ai_elements if ai_elements
24
+ end
25
+ parse_elements(html) if @elements.empty?
26
+ self
27
+ end
28
+
29
+ def to_h
30
+ { page_name: @page_name, url: @url, url_path: @uri.path, elements: @elements }
31
+ end
32
+
33
+ private
34
+
35
+ def fetch_html
36
+ response = Net::HTTP.get_response(@uri)
37
+ response.body
38
+ rescue StandardError => e
39
+ raise "Failed to fetch #{@url}: #{e.message}"
40
+ end
41
+
42
+ def parse_elements(html)
43
+ # Simple regex-based HTML parsing (no external dependency needed)
44
+ parse_inputs(html)
45
+ parse_selects(html)
46
+ parse_textareas(html)
47
+ parse_buttons(html)
48
+ parse_links(html)
49
+ end
50
+
51
+ def parse_inputs(html)
52
+ html.scan(/<input\s+([^>]*)>/i).each do |match|
53
+ attrs = parse_attributes(match[0])
54
+ next if %w[hidden submit].include?(attrs['type'])
55
+
56
+ @elements << build_element(
57
+ name: attrs['name'] || attrs['id'] || attrs['placeholder'] || 'input',
58
+ type: :input,
59
+ input_type: attrs['type'] || 'text',
60
+ locator: best_locator(attrs)
61
+ )
62
+ end
63
+ end
64
+
65
+ def parse_selects(html)
66
+ html.scan(/<select\s+([^>]*)>/i).each do |match|
67
+ attrs = parse_attributes(match[0])
68
+ @elements << build_element(
69
+ name: attrs['name'] || attrs['id'] || 'select',
70
+ type: :select,
71
+ locator: best_locator(attrs)
72
+ )
73
+ end
74
+ end
75
+
76
+ def parse_textareas(html)
77
+ html.scan(/<textarea\s+([^>]*)>/i).each do |match|
78
+ attrs = parse_attributes(match[0])
79
+ @elements << build_element(
80
+ name: attrs['name'] || attrs['id'] || 'textarea',
81
+ type: :textarea,
82
+ locator: best_locator(attrs)
83
+ )
84
+ end
85
+ end
86
+
87
+ def parse_buttons(html)
88
+ html.scan(%r{<button\s+([^>]*)>([^<]*)</button>}i).each do |match|
89
+ attrs = parse_attributes(match[0])
90
+ text = match[1].strip
91
+ @elements << build_element(
92
+ name: attrs['id'] || attrs['name'] || text.downcase.gsub(/\s+/, '_') || 'button',
93
+ type: :button,
94
+ text:,
95
+ locator: best_locator(attrs, text:)
96
+ )
97
+ end
98
+
99
+ html.scan(/<input\s+([^>]*type=["']submit["'][^>]*)>/i).each do |match|
100
+ attrs = parse_attributes(match[0])
101
+ @elements << build_element(
102
+ name: attrs['id'] || attrs['name'] || attrs['value']&.downcase&.gsub(/\s+/, '_') || 'submit',
103
+ type: :submit,
104
+ text: attrs['value'] || 'Submit',
105
+ locator: best_locator(attrs)
106
+ )
107
+ end
108
+ end
109
+
110
+ def parse_links(html)
111
+ html.scan(%r{<a\s+([^>]*)>([^<]*)</a>}i).first(5)&.each do |match|
112
+ attrs = parse_attributes(match[0])
113
+ text = match[1].strip
114
+ next if text.empty?
115
+
116
+ @elements << build_element(
117
+ name: text.downcase.gsub(/\s+/, '_'),
118
+ type: :link,
119
+ text:,
120
+ locator: best_locator(attrs, text:)
121
+ )
122
+ end
123
+ end
124
+
125
+ def parse_attributes(attr_string)
126
+ attrs = {}
127
+ attr_string.scan(/([\w-]+)=["']([^"']*)["']/).each do |key, value|
128
+ attrs[key] = value
129
+ end
130
+ attrs
131
+ end
132
+
133
+ def best_locator(attrs, text: nil)
134
+ if attrs['id']
135
+ { type: :id, value: attrs['id'] }
136
+ elsif attrs['name']
137
+ { type: :name, value: attrs['name'] }
138
+ elsif attrs['class'] && !attrs['class'].empty?
139
+ { type: :css, value: ".#{attrs['class'].split.first}" }
140
+ elsif text && !text.empty?
141
+ { type: :xpath, value: "//#{infer_tag(attrs)}[contains(text(), '#{text}')]" }
142
+ else
143
+ { type: :css, value: attrs.first&.last || 'unknown' }
144
+ end
145
+ end
146
+
147
+ def infer_tag(attrs)
148
+ return 'button' if attrs['type'] == 'button' || attrs['type'] == 'submit'
149
+ return 'a' if attrs['href']
150
+
151
+ '*'
152
+ end
153
+
154
+ def build_element(name:, type:, locator:, **extra)
155
+ clean_name = name.to_s.gsub(/[^a-z0-9_]/i, '_').gsub(/_+/, '_').downcase.delete_prefix('_').delete_suffix('_')
156
+ { name: clean_name, type:, locator: }.merge(extra)
157
+ end
158
+
159
+ def analyze_with_llm(html)
160
+ require_relative '../llm/client'
161
+ require_relative '../llm/prompts'
162
+ require_relative '../llm/response_parser'
163
+
164
+ return nil unless Llm::Client.available?
165
+
166
+ response = Llm::Client.complete(
167
+ Llm::Prompts.analyze_page(html, @url),
168
+ system_prompt: Llm::Prompts.system_prompt
169
+ )
170
+ Llm::ResponseParser.extract_elements(response)
171
+ end
172
+
173
+ def derive_page_name
174
+ path = @uri.path.to_s.gsub(%r{^/|/$}, '')
175
+ return 'home' if path.empty?
176
+
177
+ path.split('/').last.gsub(/[^a-z0-9]/i, '_').downcase
178
+ end
179
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'rbconfig'
7
+
8
+ # Downloads the latest Raider Desktop release for the current platform.
9
+ # Uses the GitHub API to discover the latest release and match the right artifact.
10
+ module DesktopDownloader
11
+ REPO = 'RaiderHQ/raider_desktop'
12
+ API_URL = "https://api.github.com/repos/#{REPO}/releases/latest".freeze
13
+
14
+ PLATFORM_PATTERNS = {
15
+ 'mac_arm' => /\.dmg$/i,
16
+ 'mac_intel' => /\.dmg$/i,
17
+ 'windows' => /setup\.exe$/i,
18
+ 'linux_deb' => /\.deb$/i,
19
+ 'linux_appimage' => /\.AppImage$/i
20
+ }.freeze
21
+
22
+ HTTP_OPEN_TIMEOUT = 10
23
+ HTTP_READ_TIMEOUT = 15
24
+
25
+ class << self
26
+ def download(destination_dir = nil)
27
+ destination_dir ||= default_download_dir
28
+ asset = find_asset
29
+ unless asset
30
+ warn '[Ruby Raider] No desktop release found for your platform'
31
+ return nil
32
+ end
33
+
34
+ destination = File.join(destination_dir, asset[:name])
35
+ puts "Downloading Raider Desktop (#{asset[:name]})..."
36
+ download_file(asset[:url], destination)
37
+ puts "Downloaded to: #{destination}"
38
+ destination
39
+ end
40
+
41
+ def latest_version
42
+ cached_release&.dig('tag_name')
43
+ end
44
+
45
+ def download_url
46
+ asset = find_asset
47
+ asset&.dig(:url)
48
+ end
49
+
50
+ # Clear cached release data (useful between operations or for testing)
51
+ def clear_cache
52
+ @cached_release = :unset
53
+ end
54
+
55
+ def platform
56
+ host_os = RbConfig::CONFIG['host_os']
57
+ arch = RbConfig::CONFIG['host_cpu']
58
+
59
+ case host_os
60
+ when /darwin|mac os/i
61
+ arch =~ /arm|aarch64/i ? 'mac_arm' : 'mac_intel'
62
+ when /mswin|mingw|cygwin|windows/i
63
+ 'windows'
64
+ when /linux/i
65
+ 'linux_appimage'
66
+ else
67
+ 'linux_appimage'
68
+ end
69
+ end
70
+
71
+ def platform_display_name
72
+ case platform
73
+ when 'mac_arm' then 'macOS (Apple Silicon)'
74
+ when 'mac_intel' then 'macOS (Intel)'
75
+ when 'windows' then 'Windows'
76
+ when 'linux_deb' then 'Linux (deb)'
77
+ when 'linux_appimage' then 'Linux (AppImage)'
78
+ else 'Unknown'
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def find_asset
85
+ release = cached_release
86
+ return nil unless release
87
+
88
+ assets = release['assets'] || []
89
+ pattern = PLATFORM_PATTERNS[platform]
90
+ return nil unless pattern
91
+
92
+ matched = assets.find { |a| a['name'].match?(pattern) }
93
+ return nil unless matched
94
+
95
+ { name: matched['name'], url: matched['browser_download_url'], size: matched['size'] }
96
+ end
97
+
98
+ # Cache release data to avoid duplicate GitHub API calls
99
+ # (latest_version, find_asset, and download_url all need the same data)
100
+ def cached_release
101
+ return @cached_release if defined?(@cached_release) && @cached_release != :unset
102
+
103
+ @cached_release = fetch_latest_release
104
+ end
105
+
106
+ def fetch_latest_release
107
+ uri = URI(API_URL)
108
+ http = Net::HTTP.new(uri.host, uri.port)
109
+ http.use_ssl = uri.scheme == 'https'
110
+ http.open_timeout = HTTP_OPEN_TIMEOUT
111
+ http.read_timeout = HTTP_READ_TIMEOUT
112
+ response = http.get(uri.request_uri)
113
+ return nil unless response.is_a?(Net::HTTPSuccess)
114
+
115
+ JSON.parse(response.body)
116
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
117
+ warn "[Ruby Raider] GitHub API timed out: #{e.message}"
118
+ nil
119
+ rescue StandardError => e
120
+ warn "[Ruby Raider] Failed to check releases: #{e.message}"
121
+ nil
122
+ end
123
+
124
+ def download_file(url, destination)
125
+ FileUtils.mkdir_p(File.dirname(destination))
126
+ uri = URI(url)
127
+ follow_redirects(uri, destination)
128
+ end
129
+
130
+ def follow_redirects(uri, destination, limit = 5)
131
+ raise 'Too many redirects' if limit.zero?
132
+
133
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
134
+ open_timeout: HTTP_OPEN_TIMEOUT, read_timeout: 60) do |http|
135
+ request = Net::HTTP::Get.new(uri)
136
+ http.request(request) do |response|
137
+ case response
138
+ when Net::HTTPRedirection
139
+ follow_redirects(URI(response['location']), destination, limit - 1)
140
+ when Net::HTTPSuccess
141
+ write_response(response, destination)
142
+ else
143
+ raise "Download failed: #{response.code} #{response.message}"
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ def write_response(response, destination)
150
+ total = response['content-length']&.to_i
151
+ downloaded = 0
152
+ File.open(destination, 'wb') do |file|
153
+ response.read_body do |chunk|
154
+ file.write(chunk)
155
+ downloaded += chunk.size
156
+ print_progress(downloaded, total) if total&.positive?
157
+ end
158
+ end
159
+ puts
160
+ end
161
+
162
+ def print_progress(downloaded, total)
163
+ percent = (downloaded * 100.0 / total).round(1)
164
+ bar_width = 30
165
+ filled = (percent / 100.0 * bar_width).round
166
+ bar = "#{'=' * filled}#{' ' * (bar_width - filled)}"
167
+ mb = (downloaded / 1_048_576.0).round(1)
168
+ total_mb = (total / 1_048_576.0).round(1)
169
+ print "\r [#{bar}] #{percent}% (#{mb}/#{total_mb} MB)"
170
+ end
171
+
172
+ def default_download_dir
173
+ downloads = File.expand_path('~/Downloads')
174
+ File.directory?(downloads) ? downloads : Dir.pwd
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRaider
4
+ # Displays the Ruby Raider logo as colored pixel art in the terminal.
5
+ # Renders a faceted gem icon alongside "RUBY RAIDER" in a pixel font,
6
+ # using Unicode half-block characters for compact, high-detail output.
7
+ module Logo
8
+ # Color palette: 0 = empty, 1 = dark, 2 = medium, 3 = bright
9
+ PALETTE = {
10
+ 1 => '130;30;48',
11
+ 2 => '180;45;65',
12
+ 3 => '230;70;90'
13
+ }.freeze
14
+
15
+ # Gem diamond with faceted shading (6 pixel rows × 9 cols)
16
+ GEM = [
17
+ [0, 0, 0, 3, 3, 3, 0, 0, 0],
18
+ [0, 2, 3, 3, 3, 3, 3, 2, 0],
19
+ [2, 3, 2, 3, 3, 3, 2, 3, 2],
20
+ [2, 2, 1, 2, 2, 2, 1, 2, 2],
21
+ [0, 1, 2, 1, 1, 1, 2, 1, 0],
22
+ [0, 0, 0, 1, 1, 1, 0, 0, 0]
23
+ ].freeze
24
+
25
+ # 5-row pixel font glyphs
26
+ GLYPHS = {
27
+ 'R' => [[1, 1, 1, 0], [1, 0, 0, 1], [1, 1, 1, 0], [1, 0, 1, 0], [1, 0, 0, 1]],
28
+ 'U' => [[1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1], [0, 1, 1, 0]],
29
+ 'B' => [[1, 1, 1, 0], [1, 0, 0, 1], [1, 1, 1, 0], [1, 0, 0, 1], [1, 1, 1, 0]],
30
+ 'Y' => [[1, 0, 0, 1], [1, 0, 0, 1], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]],
31
+ ' ' => [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]],
32
+ 'A' => [[0, 1, 1, 0], [1, 0, 0, 1], [1, 1, 1, 1], [1, 0, 0, 1], [1, 0, 0, 1]],
33
+ 'I' => [[1, 1, 1], [0, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]],
34
+ 'D' => [[1, 1, 1, 0], [1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1], [1, 1, 1, 0]],
35
+ 'E' => [[1, 1, 1, 1], [1, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0], [1, 1, 1, 1]]
36
+ }.freeze
37
+
38
+ TEXT_COLOR = 3
39
+
40
+ def self.display
41
+ text_rows = build_text('RUBY RAIDER')
42
+ text_width = text_rows.first.length
43
+
44
+ # Build 6-row text grid: blank top row centers text vertically with gem
45
+ text_grid = [Array.new(text_width, 0)]
46
+ text_rows.each { |row| text_grid << row.map { |px| px == 1 ? TEXT_COLOR : 0 } }
47
+
48
+ # Compose: gem(9) + gap(2) + text
49
+ grid = 6.times.map { |y| GEM[y] + [0, 0] + text_grid[y] }
50
+
51
+ # Render 3 terminal rows (each encodes 2 pixel rows via half-blocks)
52
+ 3.times do |tr|
53
+ line = +''
54
+ top = grid[tr * 2]
55
+ bot = grid[tr * 2 + 1]
56
+ top.length.times { |x| line << halfblock(top[x], bot[x]) }
57
+ puts " #{line.rstrip}"
58
+ end
59
+ puts
60
+ end
61
+
62
+ def self.halfblock(top, bot)
63
+ return ' ' if top.zero? && bot.zero?
64
+ return "\e[38;2;#{PALETTE[top]}m█\e[0m" if top == bot
65
+ return "\e[38;2;#{PALETTE[bot]}m▄\e[0m" if top.zero?
66
+ return "\e[38;2;#{PALETTE[top]}m▀\e[0m" if bot.zero?
67
+
68
+ "\e[38;2;#{PALETTE[top]};48;2;#{PALETTE[bot]}m▀\e[0m"
69
+ end
70
+
71
+ def self.build_text(str)
72
+ rows = Array.new(5) { [] }
73
+ str.each_char.with_index do |ch, i|
74
+ glyph = GLYPHS.fetch(ch.upcase, GLYPHS[' '])
75
+ 5.times { |r| rows[r].push(0, 0) } if i.positive?
76
+ 5.times { |r| rows[r].concat(glyph[r]) }
77
+ end
78
+ rows
79
+ end
80
+
81
+ private_class_method :halfblock, :build_text
82
+ end
83
+ end
@@ -7,46 +7,44 @@ module Utilities
7
7
 
8
8
  class << self
9
9
  def browser=(browser)
10
- config['browser'] = browser
11
- overwrite_yaml
10
+ set('browser', browser)
12
11
  end
13
12
 
14
13
  def page_path=(path)
15
- config['page_path'] = path
16
- overwrite_yaml
14
+ set('page_path', path)
17
15
  end
18
16
 
19
17
  def spec_path=(path)
20
- config['spec_path'] = path
21
- overwrite_yaml
18
+ set('spec_path', path)
22
19
  end
23
20
 
24
21
  def feature_path=(path)
25
- config['feature_path'] = path
26
- overwrite_yaml
22
+ set('feature_path', path)
27
23
  end
28
24
 
29
25
  def helper_path=(path)
30
- config['helper_path'] = path
31
- overwrite_yaml
26
+ set('helper_path', path)
32
27
  end
33
28
 
34
29
  def url=(url)
35
- config['url'] = url
36
- overwrite_yaml
30
+ set('url', url)
31
+ end
32
+
33
+ def timeout=(seconds)
34
+ set('timeout', seconds.to_i)
35
+ end
36
+
37
+ def viewport=(dimensions)
38
+ width, height = dimensions.split('x').map(&:to_i)
39
+ set('viewport', { 'width' => width, 'height' => height })
37
40
  end
38
41
 
39
42
  def platform=(platform)
40
- config['platform'] = platform
41
- overwrite_yaml
43
+ set('platform', platform)
42
44
  end
43
45
 
44
- def browser_options=(*opts)
45
- args = opts.flatten
46
- browser_args = config['browser_arguments']
47
- browser = args.first&.to_sym
48
- browser_args[browser] = browser_args[browser] + args[1..] if browser_args.key?(browser)
49
- overwrite_yaml
46
+ def browser_options=(opts)
47
+ set('browser_options', Array(opts).flatten)
50
48
  end
51
49
 
52
50
  def delete_browser_options
@@ -54,6 +52,35 @@ module Utilities
54
52
  overwrite_yaml
55
53
  end
56
54
 
55
+ def llm_provider=(provider)
56
+ set('llm_provider', provider)
57
+ end
58
+
59
+ def llm_api_key=(key)
60
+ set('llm_api_key', key)
61
+ end
62
+
63
+ def llm_model=(model)
64
+ set('llm_model', model)
65
+ end
66
+
67
+ def llm_url=(url)
68
+ set('llm_url', url)
69
+ end
70
+
71
+ def debug=(enabled)
72
+ config['debug'] ||= {}
73
+ config['debug']['enabled'] = enabled
74
+ overwrite_yaml
75
+ end
76
+
77
+ # Set multiple config keys in a single YAML write.
78
+ # Usage: Utilities.batch_update(browser: 'chrome', timeout: 30)
79
+ def batch_update(**settings)
80
+ settings.each { |key, value| config[key.to_s] = value }
81
+ overwrite_yaml
82
+ end
83
+
57
84
  def run(opts = nil)
58
85
  command = File.directory?('spec') ? 'rspec spec/' : 'cucumber features'
59
86
  system "#{command} #{opts}"
@@ -66,6 +93,12 @@ module Utilities
66
93
 
67
94
  private
68
95
 
96
+ # Single-key setter: updates in-memory config and writes once
97
+ def set(key, value)
98
+ config[key] = value
99
+ overwrite_yaml
100
+ end
101
+
69
102
  def overwrite_yaml
70
103
  File.open(@path, 'w') { |file| YAML.dump(config, file) }
71
104
  end
data/lib/version CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 3.0.0
data/ruby_raider.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.add_development_dependency 'rubocop', '~> 1.27'
20
20
  s.add_development_dependency 'rubocop-performance', '~> 1.15.0'
21
21
  s.add_development_dependency 'rubocop-rspec', '~> 2.9.0'
22
+ s.add_development_dependency 'steep', '~> 1.9'
22
23
 
23
24
  s.add_runtime_dependency 'thor', '~> 1.2.1'
24
25
  s.add_runtime_dependency 'tty-prompt', '~> 0.23.1'
@@ -0,0 +1,25 @@
1
+ # Type definitions for Adopter::AdoptMenu
2
+ module Adopter
3
+ class AdoptMenu
4
+ WEB_AUTOMATIONS: Array[String]
5
+ TEST_FRAMEWORKS: Array[String]
6
+ CI_PLATFORMS: Array[String]
7
+
8
+ def initialize: () -> void
9
+ def run: () -> void
10
+
11
+ def self.adopt: (Hash[Symbol, untyped] params) -> Hash[Symbol, untyped]
12
+ def self.validate_params!: (Hash[Symbol, untyped] params) -> void
13
+
14
+ private
15
+
16
+ def ask_source_path: () -> String
17
+ def preview_detection: (String source_path) -> Hash[Symbol, untyped]
18
+ def ask_target_automation: (String? detected) -> String
19
+ def ask_target_framework: (String? detected) -> String
20
+ def ask_output_path: () -> String?
21
+ def ask_ci_platform: (String? detected) -> String?
22
+ def execute_adoption: (Hash[Symbol, untyped] params) -> void
23
+ def print_results: (MigrationPlan plan, Hash[Symbol, untyped] results) -> void
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # Type definitions for Adopter::Converters::BaseConverter
2
+ module Adopter
3
+ module Converters
4
+ class BaseConverter
5
+ RAIDER_BASE_CLASSES: Array[String]
6
+
7
+ def convert_page: (String content, untyped _page_info) -> String
8
+ def convert_test: (String content, untyped _test_info) -> String
9
+
10
+ private
11
+
12
+ def ensure_frozen_string_literal: (String content) -> String
13
+ def update_base_class: (String content, String target_base) -> String
14
+ def raider_compatible_base?: (String base_class) -> bool
15
+ def update_require_paths: (String content, String target_automation) -> String
16
+ def update_page_requires: (String content) -> String
17
+ def update_helper_requires: (String content, String target_automation) -> String
18
+ def remove_driver_arg: (String content) -> String
19
+ def swap_driver_arg: (String content, String target_arg) -> String
20
+ def driver_arg_for: (String target_automation) -> String?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # Type definitions for Adopter::Converters::IdentityConverter
2
+ module Adopter
3
+ module Converters
4
+ class IdentityConverter < BaseConverter
5
+ def initialize: (String target_automation) -> void
6
+ def convert_page: (String content, untyped _page_info) -> String
7
+ def convert_test: (String content, untyped _test_info) -> String
8
+ def convert_step: (String content) -> String
9
+
10
+ private
11
+
12
+ def add_page_require: (String content) -> String
13
+ def update_page_instantiation: (String content) -> String
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ # Type definitions for Adopter::MigrationPlan and ConvertedFile
2
+ module Adopter
3
+ class MigrationPlan
4
+ attr_reader source_path: String
5
+ attr_reader output_path: String
6
+ attr_reader target_automation: String
7
+ attr_reader target_framework: String
8
+ attr_reader ci_platform: String?
9
+ attr_reader skeleton_structure: Hash[Symbol, untyped]
10
+ attr_reader converted_pages: Array[ConvertedFile]
11
+ attr_reader converted_tests: Array[ConvertedFile]
12
+ attr_reader converted_features: Array[ConvertedFile]
13
+ attr_reader converted_steps: Array[ConvertedFile]
14
+ attr_reader gemfile_additions: Array[String]
15
+ attr_reader config_overrides: Hash[String, untyped]
16
+ attr_reader warnings: Array[String]
17
+ attr_reader manual_actions: Array[String]
18
+
19
+ def initialize: (?Hash[Symbol, untyped]? attrs) -> void
20
+ def to_h: () -> Hash[Symbol, untyped]
21
+ def to_json: (*untyped args) -> String
22
+ def summary: () -> Hash[Symbol, untyped]
23
+ end
24
+
25
+ class ConvertedFile
26
+ attr_accessor output_path: String
27
+ attr_accessor content: String
28
+ attr_accessor source_file: String?
29
+ attr_accessor conversion_notes: String?
30
+
31
+ def initialize: (?output_path: String, ?content: String, ?source_file: String?, ?conversion_notes: String?) -> void
32
+ def to_h: () -> Hash[Symbol, untyped]
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # Type definitions for Adopter::Migrator
2
+ module Adopter
3
+ class Migrator
4
+ attr_reader plan: MigrationPlan
5
+ attr_reader results: Hash[Symbol, untyped]
6
+
7
+ def initialize: (MigrationPlan plan) -> void
8
+ def execute: () -> Hash[Symbol, untyped]
9
+
10
+ private
11
+
12
+ def generate_skeleton: () -> void
13
+ def write_converted_pages: () -> void
14
+ def write_converted_tests: () -> void
15
+ def write_converted_features: () -> void
16
+ def write_converted_steps: () -> void
17
+ def write_file: (ConvertedFile converted_file) -> void
18
+ def merge_gemfile: () -> void
19
+ def apply_config_overrides: () -> void
20
+ end
21
+ end