rubyn 0.1.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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +251 -0
  4. data/Rakefile +12 -0
  5. data/exe/rubyn +5 -0
  6. data/lib/generators/rubyn/install_generator.rb +16 -0
  7. data/lib/rubyn/cli.rb +85 -0
  8. data/lib/rubyn/client/api_client.rb +172 -0
  9. data/lib/rubyn/commands/agent.rb +191 -0
  10. data/lib/rubyn/commands/base.rb +60 -0
  11. data/lib/rubyn/commands/config.rb +51 -0
  12. data/lib/rubyn/commands/dashboard.rb +85 -0
  13. data/lib/rubyn/commands/index.rb +101 -0
  14. data/lib/rubyn/commands/init.rb +166 -0
  15. data/lib/rubyn/commands/refactor.rb +175 -0
  16. data/lib/rubyn/commands/review.rb +61 -0
  17. data/lib/rubyn/commands/spec.rb +72 -0
  18. data/lib/rubyn/commands/usage.rb +56 -0
  19. data/lib/rubyn/config/credentials.rb +39 -0
  20. data/lib/rubyn/config/project_config.rb +42 -0
  21. data/lib/rubyn/config/settings.rb +53 -0
  22. data/lib/rubyn/context/codebase_indexer.rb +195 -0
  23. data/lib/rubyn/context/context_builder.rb +36 -0
  24. data/lib/rubyn/context/file_resolver.rb +235 -0
  25. data/lib/rubyn/context/project_scanner.rb +132 -0
  26. data/lib/rubyn/engine/app/assets/images/rubyn/RubynLogo.png +0 -0
  27. data/lib/rubyn/engine/app/assets/javascripts/rubyn/application.js +579 -0
  28. data/lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css +1073 -0
  29. data/lib/rubyn/engine/app/controllers/rubyn/agent_controller.rb +26 -0
  30. data/lib/rubyn/engine/app/controllers/rubyn/application_controller.rb +44 -0
  31. data/lib/rubyn/engine/app/controllers/rubyn/dashboard_controller.rb +19 -0
  32. data/lib/rubyn/engine/app/controllers/rubyn/feedback_controller.rb +17 -0
  33. data/lib/rubyn/engine/app/controllers/rubyn/files_controller.rb +54 -0
  34. data/lib/rubyn/engine/app/controllers/rubyn/refactor_controller.rb +56 -0
  35. data/lib/rubyn/engine/app/controllers/rubyn/reviews_controller.rb +32 -0
  36. data/lib/rubyn/engine/app/controllers/rubyn/settings_controller.rb +17 -0
  37. data/lib/rubyn/engine/app/controllers/rubyn/specs_controller.rb +33 -0
  38. data/lib/rubyn/engine/app/views/layouts/rubyn/application.html.erb +63 -0
  39. data/lib/rubyn/engine/app/views/rubyn/agent/show.html.erb +22 -0
  40. data/lib/rubyn/engine/app/views/rubyn/dashboard/index.html.erb +120 -0
  41. data/lib/rubyn/engine/app/views/rubyn/files/index.html.erb +45 -0
  42. data/lib/rubyn/engine/app/views/rubyn/refactor/show.html.erb +28 -0
  43. data/lib/rubyn/engine/app/views/rubyn/reviews/show.html.erb +28 -0
  44. data/lib/rubyn/engine/app/views/rubyn/settings/show.html.erb +42 -0
  45. data/lib/rubyn/engine/app/views/rubyn/specs/show.html.erb +28 -0
  46. data/lib/rubyn/engine/config/routes.rb +13 -0
  47. data/lib/rubyn/engine/engine.rb +18 -0
  48. data/lib/rubyn/output/diff_renderer.rb +106 -0
  49. data/lib/rubyn/output/formatter.rb +123 -0
  50. data/lib/rubyn/output/spinner.rb +26 -0
  51. data/lib/rubyn/tools/base_tool.rb +74 -0
  52. data/lib/rubyn/tools/bundle_add.rb +77 -0
  53. data/lib/rubyn/tools/create_file.rb +32 -0
  54. data/lib/rubyn/tools/delete_file.rb +29 -0
  55. data/lib/rubyn/tools/executor.rb +68 -0
  56. data/lib/rubyn/tools/find_files.rb +33 -0
  57. data/lib/rubyn/tools/find_references.rb +72 -0
  58. data/lib/rubyn/tools/git_commit.rb +65 -0
  59. data/lib/rubyn/tools/git_create_branch.rb +58 -0
  60. data/lib/rubyn/tools/git_diff.rb +42 -0
  61. data/lib/rubyn/tools/git_log.rb +43 -0
  62. data/lib/rubyn/tools/git_status.rb +26 -0
  63. data/lib/rubyn/tools/list_directory.rb +82 -0
  64. data/lib/rubyn/tools/move_file.rb +35 -0
  65. data/lib/rubyn/tools/patch_file.rb +47 -0
  66. data/lib/rubyn/tools/rails_generate.rb +40 -0
  67. data/lib/rubyn/tools/rails_migrate.rb +55 -0
  68. data/lib/rubyn/tools/rails_routes.rb +35 -0
  69. data/lib/rubyn/tools/read_file.rb +45 -0
  70. data/lib/rubyn/tools/registry.rb +28 -0
  71. data/lib/rubyn/tools/run_command.rb +48 -0
  72. data/lib/rubyn/tools/run_tests.rb +52 -0
  73. data/lib/rubyn/tools/search_files.rb +82 -0
  74. data/lib/rubyn/tools/write_file.rb +30 -0
  75. data/lib/rubyn/version.rb +5 -0
  76. data/lib/rubyn/version_checker.rb +74 -0
  77. data/lib/rubyn.rb +95 -0
  78. data/sig/rubyn.rbs +4 -0
  79. metadata +379 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+ require_relative "base_tool"
6
+ require_relative "registry"
7
+
8
+ module Rubyn
9
+ module Tools
10
+ class RunCommand < BaseTool
11
+ DESCRIPTION = "Execute a shell command in the project directory"
12
+ PARAMETERS = {
13
+ command: { type: :string, description: "Shell command to execute", required: true },
14
+ timeout: { type: :integer, description: "Timeout in seconds (default 30)", required: false }
15
+ }.freeze
16
+ REQUIRES_CONFIRMATION = true
17
+
18
+ DEFAULT_TIMEOUT = 30
19
+
20
+ def execute(params)
21
+ command = params[:command]
22
+ return error("command is required") unless command && !command.strip.empty?
23
+
24
+ timeout_seconds = (params[:timeout] || DEFAULT_TIMEOUT).to_i
25
+
26
+ stdout, stderr, status = nil
27
+ begin
28
+ Timeout.timeout(timeout_seconds) do
29
+ stdout, stderr, status = Open3.capture3(command, chdir: project_root)
30
+ end
31
+ rescue Timeout::Error
32
+ return error("Command timed out after #{timeout_seconds} seconds")
33
+ rescue StandardError => e
34
+ return error("Failed to execute command: #{e.message}")
35
+ end
36
+
37
+ stdout = truncate_output(stdout)
38
+ stderr = truncate_output(stderr)
39
+
40
+ success(command: command, stdout: stdout, stderr: stderr, exit_code: status.exitstatus)
41
+ end
42
+
43
+ private
44
+ end
45
+
46
+ Registry.register("run_command", RunCommand)
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "base_tool"
5
+ require_relative "registry"
6
+
7
+ module Rubyn
8
+ module Tools
9
+ class RunTests < BaseTool
10
+ DESCRIPTION = "Run tests for a specific file or the full test suite"
11
+ PARAMETERS = {
12
+ path: { type: :string, description: "Specific test file to run", required: false },
13
+ options: { type: :string, description: "Extra flags like --fail-fast", required: false }
14
+ }.freeze
15
+ REQUIRES_CONFIRMATION = true
16
+
17
+ def execute(params)
18
+ command = detect_test_command
19
+ return error("No test framework detected (no spec/ or test/ directory found)") unless command
20
+
21
+ command = "#{command} #{params[:path]}" if params[:path] && !params[:path].strip.empty?
22
+ command = "#{command} #{params[:options]}" if params[:options] && !params[:options].strip.empty?
23
+
24
+ begin
25
+ stdout, stderr, status = Open3.capture3(command, chdir: project_root)
26
+ rescue StandardError => e
27
+ return error("Failed to run tests: #{e.message}")
28
+ end
29
+
30
+ success(
31
+ command: command,
32
+ stdout: stdout,
33
+ stderr: stderr,
34
+ exit_code: status.exitstatus,
35
+ passed: status.exitstatus.zero?
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def detect_test_command
42
+ if Dir.exist?(File.join(project_root, "spec"))
43
+ "bundle exec rspec"
44
+ elsif Dir.exist?(File.join(project_root, "test"))
45
+ "bundle exec rails test"
46
+ end
47
+ end
48
+ end
49
+
50
+ Registry.register("run_tests", RunTests)
51
+ end
52
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ module Tools
5
+ class SearchFiles < BaseTool
6
+ DESCRIPTION = "Search file contents for a pattern (grep)"
7
+ PARAMETERS = {
8
+ pattern: { type: "string", required: true, description: "Search pattern (regular expression)" },
9
+ path: { type: "string", required: false, description: "Directory to search in (defaults to project root)" },
10
+ file_pattern: { type: "string", required: false, description: "File glob pattern to filter (e.g. '*.rb')" }
11
+ }.freeze
12
+ REQUIRES_CONFIRMATION = false
13
+
14
+ MAX_MATCHES = 100
15
+ MAX_LINE_LENGTH = 500
16
+ MAX_FILE_SIZE = 2_000_000
17
+
18
+ def execute(params)
19
+ return error("Missing required parameter: pattern") unless params[:pattern]
20
+
21
+ begin
22
+ regex = Regexp.new(params[:pattern])
23
+ rescue RegexpError => e
24
+ return error("Invalid regex pattern: #{e.message}")
25
+ end
26
+
27
+ search_path = params[:path] || "."
28
+ file_pattern = params[:file_pattern]
29
+
30
+ resolved = resolve_path(search_path)
31
+ return resolved if resolved.is_a?(Hash) && resolved[:error]
32
+
33
+ return error("Path not found: #{search_path}") unless File.exist?(resolved)
34
+
35
+ matches = []
36
+ glob = file_pattern ? File.join(resolved, "**", file_pattern) : File.join(resolved, "**", "*")
37
+
38
+ Dir.glob(glob).each do |file|
39
+ break if matches.size >= MAX_MATCHES
40
+ next unless File.file?(file)
41
+ next if excluded?(file)
42
+ next if binary?(file)
43
+
44
+ search_in_file(file, regex, matches)
45
+ end
46
+
47
+ success(pattern: params[:pattern], matches: matches)
48
+ end
49
+
50
+ private
51
+
52
+ def search_in_file(file, regex, matches)
53
+ File.open(file, "r:UTF-8") do |f|
54
+ f.each_line.with_index(1) do |line, line_number|
55
+ break if matches.size >= MAX_MATCHES
56
+
57
+ if line.match?(regex)
58
+ matches << {
59
+ file: relative_path(file),
60
+ line_number: line_number,
61
+ line: line.chomp.slice(0, MAX_LINE_LENGTH)
62
+ }
63
+ end
64
+ end
65
+ end
66
+ rescue ArgumentError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
67
+ # Skip files with encoding issues
68
+ end
69
+
70
+ def binary?(file)
71
+ return true if File.size(file) > MAX_FILE_SIZE
72
+
73
+ sample = File.read(file, 8192) || ""
74
+ sample.include?("\x00")
75
+ rescue Errno::EACCES, Errno::ENOENT, IOError
76
+ true
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ Rubyn::Tools::Registry.register("search_files", Rubyn::Tools::SearchFiles)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "base_tool"
5
+ require_relative "registry"
6
+
7
+ module Rubyn
8
+ module Tools
9
+ class WriteFile < BaseTool
10
+ DESCRIPTION = "Write content to a file, creating or overwriting it"
11
+ PARAMETERS = {
12
+ path: { type: :string, description: "Path to the file to write", required: true },
13
+ content: { type: :string, description: "Content to write to the file", required: true }
14
+ }.freeze
15
+ REQUIRES_CONFIRMATION = true
16
+
17
+ def execute(params)
18
+ resolved = resolve_path(params[:path])
19
+ return resolved if resolved.is_a?(Hash) && resolved[:error]
20
+
21
+ FileUtils.mkdir_p(File.dirname(resolved))
22
+ bytes_written = File.write(resolved, params[:content])
23
+
24
+ success(path: params[:path], bytes_written: bytes_written)
25
+ end
26
+ end
27
+
28
+ Registry.register("write_file", WriteFile)
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "fileutils"
6
+
7
+ module Rubyn
8
+ class VersionChecker
9
+ CACHE_FILE = File.join(Dir.home, ".rubyn", "version_cache.json")
10
+ CACHE_TTL = 86_400 # 24 hours
11
+ RUBYGEMS_URL = "https://rubygems.org/api/v1/versions/rubyn/latest.json"
12
+
13
+ class << self
14
+ def check
15
+ cached = read_cache
16
+ return cached if cached
17
+
18
+ latest = fetch_latest_version
19
+ return nil unless latest
20
+
21
+ write_cache(latest)
22
+ latest
23
+ rescue Net::HTTPError, SocketError, JSON::ParserError, Errno::ENOENT, Errno::EACCES
24
+ nil
25
+ end
26
+
27
+ def update_available?
28
+ latest = check
29
+ return false unless latest && Gem::Version.correct?(latest)
30
+
31
+ Gem::Version.new(latest) > Gem::Version.new(Rubyn::VERSION)
32
+ end
33
+
34
+ def update_message
35
+ latest = check
36
+ return nil unless latest && Gem::Version.correct?(latest)
37
+ return nil unless Gem::Version.new(latest) > Gem::Version.new(Rubyn::VERSION)
38
+
39
+ "Rubyn #{latest} is available (you have #{Rubyn::VERSION}). Run `gem update rubyn` to upgrade."
40
+ end
41
+
42
+ private
43
+
44
+ def fetch_latest_version
45
+ uri = URI(RUBYGEMS_URL)
46
+ response = Net::HTTP.get_response(uri)
47
+ return nil unless response.is_a?(Net::HTTPSuccess)
48
+
49
+ data = JSON.parse(response.body)
50
+ data["version"]
51
+ rescue Net::HTTPError, SocketError, JSON::ParserError, Timeout::Error, Errno::ECONNREFUSED
52
+ nil
53
+ end
54
+
55
+ def read_cache
56
+ return nil unless File.exist?(CACHE_FILE)
57
+
58
+ data = JSON.parse(File.read(CACHE_FILE))
59
+ return nil if Time.now.to_i - data["checked_at"].to_i > CACHE_TTL
60
+
61
+ data["version"]
62
+ rescue JSON::ParserError, Errno::ENOENT, Errno::EACCES
63
+ nil
64
+ end
65
+
66
+ def write_cache(version)
67
+ FileUtils.mkdir_p(File.dirname(CACHE_FILE))
68
+ File.write(CACHE_FILE, JSON.generate(version: version, checked_at: Time.now.to_i))
69
+ rescue Errno::EACCES, Errno::ENOENT, Errno::ENOSPC
70
+ # Non-critical — skip if we can't write cache
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/rubyn.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "dotenv/load"
5
+ rescue LoadError
6
+ # dotenv is optional — only needed for local development with .env files
7
+ end
8
+
9
+ require_relative "rubyn/version"
10
+ require_relative "rubyn/version_checker"
11
+
12
+ module Rubyn
13
+ class Error < StandardError; end
14
+ class AuthenticationError < Error; end
15
+ class ConfigurationError < Error; end
16
+ class APIError < Error; end
17
+
18
+ class Configuration
19
+ attr_accessor :api_url, :api_key
20
+
21
+ def initialize
22
+ @api_url = ENV.fetch("RUBYN_API_URL", "https://app.rubyn.ai")
23
+ @api_key = ENV.fetch("RUBYN_API_KEY", nil)
24
+ end
25
+ end
26
+
27
+ class << self
28
+ def configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def configure
33
+ yield(configuration)
34
+ end
35
+
36
+ def api_client
37
+ @api_client ||= Client::ApiClient.new
38
+ end
39
+
40
+ def reset!
41
+ @configuration = nil
42
+ @api_client = nil
43
+ end
44
+ end
45
+ end
46
+
47
+ require_relative "rubyn/config/credentials"
48
+ require_relative "rubyn/config/settings"
49
+ require_relative "rubyn/config/project_config"
50
+ require_relative "rubyn/client/api_client"
51
+ require_relative "rubyn/context/project_scanner"
52
+ require_relative "rubyn/context/file_resolver"
53
+ require_relative "rubyn/context/context_builder"
54
+ require_relative "rubyn/context/codebase_indexer"
55
+ require_relative "rubyn/output/formatter"
56
+ require_relative "rubyn/output/diff_renderer"
57
+ require_relative "rubyn/output/spinner"
58
+ require_relative "rubyn/tools/registry"
59
+ require_relative "rubyn/tools/base_tool"
60
+ require_relative "rubyn/tools/executor"
61
+ require_relative "rubyn/tools/read_file"
62
+ require_relative "rubyn/tools/write_file"
63
+ require_relative "rubyn/tools/create_file"
64
+ require_relative "rubyn/tools/patch_file"
65
+ require_relative "rubyn/tools/delete_file"
66
+ require_relative "rubyn/tools/move_file"
67
+ require_relative "rubyn/tools/list_directory"
68
+ require_relative "rubyn/tools/find_files"
69
+ require_relative "rubyn/tools/search_files"
70
+ require_relative "rubyn/tools/find_references"
71
+ require_relative "rubyn/tools/run_command"
72
+ require_relative "rubyn/tools/run_tests"
73
+ require_relative "rubyn/tools/rails_routes"
74
+ require_relative "rubyn/tools/rails_generate"
75
+ require_relative "rubyn/tools/rails_migrate"
76
+ require_relative "rubyn/tools/bundle_add"
77
+ require_relative "rubyn/tools/git_status"
78
+ require_relative "rubyn/tools/git_diff"
79
+ require_relative "rubyn/tools/git_log"
80
+ require_relative "rubyn/tools/git_commit"
81
+ require_relative "rubyn/tools/git_create_branch"
82
+ require_relative "rubyn/commands/base"
83
+ require_relative "rubyn/commands/init"
84
+ require_relative "rubyn/commands/refactor"
85
+ require_relative "rubyn/commands/spec"
86
+ require_relative "rubyn/commands/review"
87
+ require_relative "rubyn/commands/agent"
88
+ require_relative "rubyn/commands/index"
89
+ require_relative "rubyn/commands/usage"
90
+ require_relative "rubyn/commands/config"
91
+ require_relative "rubyn/commands/dashboard"
92
+ require_relative "rubyn/cli"
93
+
94
+ # Load Rails engine when running inside a Rails app
95
+ require_relative "rubyn/engine/engine" if defined?(Rails::Engine)
data/sig/rubyn.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rubyn
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end