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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +251 -0
- data/Rakefile +12 -0
- data/exe/rubyn +5 -0
- data/lib/generators/rubyn/install_generator.rb +16 -0
- data/lib/rubyn/cli.rb +85 -0
- data/lib/rubyn/client/api_client.rb +172 -0
- data/lib/rubyn/commands/agent.rb +191 -0
- data/lib/rubyn/commands/base.rb +60 -0
- data/lib/rubyn/commands/config.rb +51 -0
- data/lib/rubyn/commands/dashboard.rb +85 -0
- data/lib/rubyn/commands/index.rb +101 -0
- data/lib/rubyn/commands/init.rb +166 -0
- data/lib/rubyn/commands/refactor.rb +175 -0
- data/lib/rubyn/commands/review.rb +61 -0
- data/lib/rubyn/commands/spec.rb +72 -0
- data/lib/rubyn/commands/usage.rb +56 -0
- data/lib/rubyn/config/credentials.rb +39 -0
- data/lib/rubyn/config/project_config.rb +42 -0
- data/lib/rubyn/config/settings.rb +53 -0
- data/lib/rubyn/context/codebase_indexer.rb +195 -0
- data/lib/rubyn/context/context_builder.rb +36 -0
- data/lib/rubyn/context/file_resolver.rb +235 -0
- data/lib/rubyn/context/project_scanner.rb +132 -0
- data/lib/rubyn/engine/app/assets/images/rubyn/RubynLogo.png +0 -0
- data/lib/rubyn/engine/app/assets/javascripts/rubyn/application.js +579 -0
- data/lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css +1073 -0
- data/lib/rubyn/engine/app/controllers/rubyn/agent_controller.rb +26 -0
- data/lib/rubyn/engine/app/controllers/rubyn/application_controller.rb +44 -0
- data/lib/rubyn/engine/app/controllers/rubyn/dashboard_controller.rb +19 -0
- data/lib/rubyn/engine/app/controllers/rubyn/feedback_controller.rb +17 -0
- data/lib/rubyn/engine/app/controllers/rubyn/files_controller.rb +54 -0
- data/lib/rubyn/engine/app/controllers/rubyn/refactor_controller.rb +56 -0
- data/lib/rubyn/engine/app/controllers/rubyn/reviews_controller.rb +32 -0
- data/lib/rubyn/engine/app/controllers/rubyn/settings_controller.rb +17 -0
- data/lib/rubyn/engine/app/controllers/rubyn/specs_controller.rb +33 -0
- data/lib/rubyn/engine/app/views/layouts/rubyn/application.html.erb +63 -0
- data/lib/rubyn/engine/app/views/rubyn/agent/show.html.erb +22 -0
- data/lib/rubyn/engine/app/views/rubyn/dashboard/index.html.erb +120 -0
- data/lib/rubyn/engine/app/views/rubyn/files/index.html.erb +45 -0
- data/lib/rubyn/engine/app/views/rubyn/refactor/show.html.erb +28 -0
- data/lib/rubyn/engine/app/views/rubyn/reviews/show.html.erb +28 -0
- data/lib/rubyn/engine/app/views/rubyn/settings/show.html.erb +42 -0
- data/lib/rubyn/engine/app/views/rubyn/specs/show.html.erb +28 -0
- data/lib/rubyn/engine/config/routes.rb +13 -0
- data/lib/rubyn/engine/engine.rb +18 -0
- data/lib/rubyn/output/diff_renderer.rb +106 -0
- data/lib/rubyn/output/formatter.rb +123 -0
- data/lib/rubyn/output/spinner.rb +26 -0
- data/lib/rubyn/tools/base_tool.rb +74 -0
- data/lib/rubyn/tools/bundle_add.rb +77 -0
- data/lib/rubyn/tools/create_file.rb +32 -0
- data/lib/rubyn/tools/delete_file.rb +29 -0
- data/lib/rubyn/tools/executor.rb +68 -0
- data/lib/rubyn/tools/find_files.rb +33 -0
- data/lib/rubyn/tools/find_references.rb +72 -0
- data/lib/rubyn/tools/git_commit.rb +65 -0
- data/lib/rubyn/tools/git_create_branch.rb +58 -0
- data/lib/rubyn/tools/git_diff.rb +42 -0
- data/lib/rubyn/tools/git_log.rb +43 -0
- data/lib/rubyn/tools/git_status.rb +26 -0
- data/lib/rubyn/tools/list_directory.rb +82 -0
- data/lib/rubyn/tools/move_file.rb +35 -0
- data/lib/rubyn/tools/patch_file.rb +47 -0
- data/lib/rubyn/tools/rails_generate.rb +40 -0
- data/lib/rubyn/tools/rails_migrate.rb +55 -0
- data/lib/rubyn/tools/rails_routes.rb +35 -0
- data/lib/rubyn/tools/read_file.rb +45 -0
- data/lib/rubyn/tools/registry.rb +28 -0
- data/lib/rubyn/tools/run_command.rb +48 -0
- data/lib/rubyn/tools/run_tests.rb +52 -0
- data/lib/rubyn/tools/search_files.rb +82 -0
- data/lib/rubyn/tools/write_file.rb +30 -0
- data/lib/rubyn/version.rb +5 -0
- data/lib/rubyn/version_checker.rb +74 -0
- data/lib/rubyn.rb +95 -0
- data/sig/rubyn.rbs +4 -0
- metadata +379 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyn
|
|
4
|
+
module Tools
|
|
5
|
+
class FindReferences < BaseTool
|
|
6
|
+
DESCRIPTION = "Find all references to a class, method, or constant across the project"
|
|
7
|
+
PARAMETERS = {
|
|
8
|
+
identifier: { type: "string", required: true, description: "The class, method, or constant name to find" },
|
|
9
|
+
type: { type: "string", required: false,
|
|
10
|
+
description: "Type of identifier: 'class', 'method', 'constant', or 'any' (default 'any')" }
|
|
11
|
+
}.freeze
|
|
12
|
+
REQUIRES_CONFIRMATION = false
|
|
13
|
+
|
|
14
|
+
MAX_RESULTS = 100
|
|
15
|
+
|
|
16
|
+
def execute(params)
|
|
17
|
+
identifier = params[:identifier]
|
|
18
|
+
return error("Missing required parameter: identifier") unless identifier
|
|
19
|
+
|
|
20
|
+
ref_type = params[:type] || "any"
|
|
21
|
+
|
|
22
|
+
regex = build_regex(identifier, ref_type)
|
|
23
|
+
return error("Invalid reference type: #{ref_type}") unless regex
|
|
24
|
+
|
|
25
|
+
references = []
|
|
26
|
+
rb_files = Dir.glob(File.join(project_root, "**", "*.rb"))
|
|
27
|
+
|
|
28
|
+
rb_files.each do |file|
|
|
29
|
+
break if references.size >= MAX_RESULTS
|
|
30
|
+
next if excluded?(file)
|
|
31
|
+
|
|
32
|
+
search_file_for_references(file, regex, references)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
success(identifier: identifier, references: references)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_regex(identifier, type)
|
|
41
|
+
escaped = Regexp.escape(identifier)
|
|
42
|
+
case type
|
|
43
|
+
when "method"
|
|
44
|
+
Regexp.new("(?:\\.#{escaped}\\b|\\bdef\\s+#{escaped}\\b)")
|
|
45
|
+
when "class", "constant", "any"
|
|
46
|
+
Regexp.new("\\b#{escaped}\\b")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def search_file_for_references(file, regex, references)
|
|
51
|
+
File.open(file, "r:UTF-8") do |f|
|
|
52
|
+
f.each_line.with_index(1) do |line, line_number|
|
|
53
|
+
break if references.size >= MAX_RESULTS
|
|
54
|
+
|
|
55
|
+
if line.match?(regex)
|
|
56
|
+
references << {
|
|
57
|
+
file: relative_path(file),
|
|
58
|
+
line_number: line_number,
|
|
59
|
+
line: line.chomp.slice(0, 500)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
65
|
+
# Skip files with encoding issues
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Rubyn::Tools::Registry.register("find_references", Rubyn::Tools::FindReferences)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "base_tool"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Rubyn
|
|
9
|
+
module Tools
|
|
10
|
+
class GitCommit < BaseTool
|
|
11
|
+
DESCRIPTION = "Stage files and create a git commit"
|
|
12
|
+
PARAMETERS = {
|
|
13
|
+
message: { type: :string, description: "Commit message", required: true },
|
|
14
|
+
files: { type: :array, description: "Specific files to stage (defaults to all changed)", required: false }
|
|
15
|
+
}.freeze
|
|
16
|
+
REQUIRES_CONFIRMATION = true
|
|
17
|
+
|
|
18
|
+
def execute(params)
|
|
19
|
+
message = params[:message]
|
|
20
|
+
return error("message is required") unless message && !message.strip.empty?
|
|
21
|
+
|
|
22
|
+
files = params[:files]
|
|
23
|
+
|
|
24
|
+
# Stage files
|
|
25
|
+
add_command = if files.is_a?(Array) && !files.empty?
|
|
26
|
+
escaped_files = files.map { |f| Shellwords.escape(f) }.join(" ")
|
|
27
|
+
"git add #{escaped_files}"
|
|
28
|
+
else
|
|
29
|
+
"git add -A"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
_stdout, stderr, status = Open3.capture3(add_command, chdir: project_root)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
return error("Failed to stage files: #{e.message}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return error("git add failed: #{stderr}") unless status.exitstatus.zero?
|
|
39
|
+
|
|
40
|
+
# Create commit
|
|
41
|
+
begin
|
|
42
|
+
stdout, stderr, status = Open3.capture3("git", "commit", "-m", message, chdir: project_root)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
return error("Failed to create commit: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return error("git commit failed: #{stderr}") unless status.exitstatus.zero?
|
|
48
|
+
|
|
49
|
+
# Extract commit hash
|
|
50
|
+
commit_hash = extract_commit_hash(stdout)
|
|
51
|
+
|
|
52
|
+
success(hash: commit_hash, message: message)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def extract_commit_hash(output)
|
|
58
|
+
match = output.match(%r{\[[\w\-/]+\s+([a-f0-9]+)\]})
|
|
59
|
+
match ? match[1] : "unknown"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Registry.register("git_commit", GitCommit)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
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 GitCreateBranch < BaseTool
|
|
10
|
+
DESCRIPTION = "Create and checkout a new git branch"
|
|
11
|
+
PARAMETERS = {
|
|
12
|
+
branch_name: { type: :string, description: "Name of the new branch", required: true },
|
|
13
|
+
from: { type: :string, description: "Base branch (defaults to current branch)", required: false }
|
|
14
|
+
}.freeze
|
|
15
|
+
REQUIRES_CONFIRMATION = true
|
|
16
|
+
|
|
17
|
+
VALID_BRANCH_PATTERN = %r{\A[a-zA-Z0-9\-_/]+\z}
|
|
18
|
+
|
|
19
|
+
def execute(params)
|
|
20
|
+
branch_name = params[:branch_name]
|
|
21
|
+
return error("branch_name is required") unless branch_name && !branch_name.strip.empty?
|
|
22
|
+
|
|
23
|
+
unless branch_name.match?(VALID_BRANCH_PATTERN)
|
|
24
|
+
return error(
|
|
25
|
+
"Invalid branch name: '#{branch_name}'. Use letters, numbers, hyphens, underscores, or slashes."
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
from = params[:from]
|
|
30
|
+
if from && !from.strip.empty?
|
|
31
|
+
unless from.match?(VALID_BRANCH_PATTERN)
|
|
32
|
+
return error(
|
|
33
|
+
"Invalid base branch name: '#{from}'. Use letters, numbers, hyphens, underscores, or slashes."
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
cmd = if from && !from.strip.empty?
|
|
39
|
+
["git", "checkout", "-b", branch_name, from]
|
|
40
|
+
else
|
|
41
|
+
["git", "checkout", "-b", branch_name]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
_, stderr, status = Open3.capture3(*cmd, chdir: project_root)
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
return error("Failed to create branch: #{e.message}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return error("git checkout failed: #{stderr}") unless status.exitstatus.zero?
|
|
51
|
+
|
|
52
|
+
success(branch: branch_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Registry.register("git_create_branch", GitCreateBranch)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "base_tool"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Rubyn
|
|
9
|
+
module Tools
|
|
10
|
+
class GitDiff < BaseTool
|
|
11
|
+
DESCRIPTION = "Show current git diff"
|
|
12
|
+
PARAMETERS = {
|
|
13
|
+
path: { type: :string, description: "Specific file to diff", required: false },
|
|
14
|
+
staged: { type: :boolean, description: "Show staged changes (default false)", required: false }
|
|
15
|
+
}.freeze
|
|
16
|
+
REQUIRES_CONFIRMATION = false
|
|
17
|
+
|
|
18
|
+
DEFAULT_MAX_OUTPUT_LENGTH = 15_000
|
|
19
|
+
|
|
20
|
+
def execute(params)
|
|
21
|
+
command = "git diff"
|
|
22
|
+
command = "#{command} --cached" if params[:staged]
|
|
23
|
+
command = "#{command} -- #{Shellwords.shellescape(params[:path])}" if params[:path] && !params[:path].strip.empty?
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
stdout, stderr, status = Open3.capture3(command, chdir: project_root)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
return error("Failed to get git diff: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return error("git diff failed: #{stderr}") unless status.exitstatus.zero?
|
|
32
|
+
|
|
33
|
+
diff = truncate_output(stdout)
|
|
34
|
+
success(diff: diff)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Registry.register("git_diff", GitDiff)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "base_tool"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Rubyn
|
|
9
|
+
module Tools
|
|
10
|
+
class GitLog < BaseTool
|
|
11
|
+
DESCRIPTION = "Show recent git commit history"
|
|
12
|
+
PARAMETERS = {
|
|
13
|
+
count: { type: :integer, description: "Number of commits to show (default 10)", required: false },
|
|
14
|
+
path: { type: :string, description: "Filter by file path", required: false }
|
|
15
|
+
}.freeze
|
|
16
|
+
REQUIRES_CONFIRMATION = false
|
|
17
|
+
DEFAULT_COUNT = 10
|
|
18
|
+
|
|
19
|
+
def execute(params)
|
|
20
|
+
count = (params[:count] || DEFAULT_COUNT).to_i
|
|
21
|
+
command = "git log --oneline -#{count}"
|
|
22
|
+
command = "#{command} -- #{Shellwords.shellescape(params[:path])}" if params[:path] && !params[:path].strip.empty?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
stdout, stderr, status = Open3.capture3(command, chdir: project_root)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
return error("Failed to get git log: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return error("git log failed: #{stderr}") unless status.exitstatus.zero?
|
|
31
|
+
|
|
32
|
+
commits = stdout.strip.split("\n").map do |line|
|
|
33
|
+
hash, *message_parts = line.split
|
|
34
|
+
{ hash: hash, message: message_parts.join(" ") }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
success(commits: commits)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Registry.register("git_log", GitLog)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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 GitStatus < BaseTool
|
|
10
|
+
DESCRIPTION = "Show git working tree status"
|
|
11
|
+
PARAMETERS = {}.freeze
|
|
12
|
+
REQUIRES_CONFIRMATION = false
|
|
13
|
+
|
|
14
|
+
def execute(_params)
|
|
15
|
+
stdout, stderr, status = Open3.capture3("git status --short", chdir: project_root)
|
|
16
|
+
return error("git status failed: #{stderr}") unless status.exitstatus.zero?
|
|
17
|
+
|
|
18
|
+
success(status: stdout, clean: stdout.strip.empty?)
|
|
19
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
20
|
+
error("Failed to get git status: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Registry.register("git_status", GitStatus)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyn
|
|
4
|
+
module Tools
|
|
5
|
+
class ListDirectory < BaseTool
|
|
6
|
+
DESCRIPTION = "List files and directories at a given path"
|
|
7
|
+
PARAMETERS = {
|
|
8
|
+
path: { type: "string", required: false, description: "Directory path (defaults to project root)" },
|
|
9
|
+
recursive: { type: "boolean", required: false, description: "List recursively (default false)" }
|
|
10
|
+
}.freeze
|
|
11
|
+
REQUIRES_CONFIRMATION = false
|
|
12
|
+
|
|
13
|
+
MAX_ENTRIES = 500
|
|
14
|
+
|
|
15
|
+
def execute(params)
|
|
16
|
+
target_path = params[:path] || "."
|
|
17
|
+
recursive = params[:recursive] || false
|
|
18
|
+
|
|
19
|
+
resolved = resolve_path(target_path)
|
|
20
|
+
return resolved if resolved.is_a?(Hash) && resolved[:error]
|
|
21
|
+
|
|
22
|
+
return error("Directory not found: #{target_path}") unless Dir.exist?(resolved)
|
|
23
|
+
|
|
24
|
+
entries = if recursive
|
|
25
|
+
list_recursive(resolved)
|
|
26
|
+
else
|
|
27
|
+
list_flat(resolved)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
success(path: relative_path(resolved), entries: entries)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def list_flat(dir)
|
|
36
|
+
entries = []
|
|
37
|
+
Dir.children(dir).sort.each do |name|
|
|
38
|
+
break if entries.size >= MAX_ENTRIES
|
|
39
|
+
|
|
40
|
+
next if excluded?(name)
|
|
41
|
+
|
|
42
|
+
full = File.join(dir, name)
|
|
43
|
+
entries << entry_info(name, full)
|
|
44
|
+
end
|
|
45
|
+
entries
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def list_recursive(dir, prefix: "")
|
|
49
|
+
entries = []
|
|
50
|
+
Dir.children(dir).sort.each do |name|
|
|
51
|
+
return entries if entries.size >= MAX_ENTRIES
|
|
52
|
+
|
|
53
|
+
next if excluded?(name)
|
|
54
|
+
|
|
55
|
+
full = File.join(dir, name)
|
|
56
|
+
rel = prefix.empty? ? name : File.join(prefix, name)
|
|
57
|
+
|
|
58
|
+
entries << entry_info(rel, full)
|
|
59
|
+
|
|
60
|
+
entries.concat(list_recursive(full, prefix: rel)) if File.directory?(full)
|
|
61
|
+
end
|
|
62
|
+
entries
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def entry_info(name, full_path)
|
|
66
|
+
if File.directory?(full_path)
|
|
67
|
+
{ name: name, type: "directory", size: 0 }
|
|
68
|
+
else
|
|
69
|
+
{ name: name, type: "file", size: File.size(full_path) }
|
|
70
|
+
end
|
|
71
|
+
rescue SystemCallError
|
|
72
|
+
{ name: name, type: "unknown", size: 0 }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def excluded?(name)
|
|
76
|
+
EXCLUDED_DIRS.any? { |ex| name == ex || name.start_with?("#{ex}/") }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Rubyn::Tools::Registry.register("list_directory", Rubyn::Tools::ListDirectory)
|
|
@@ -0,0 +1,35 @@
|
|
|
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 MoveFile < BaseTool
|
|
10
|
+
DESCRIPTION = "Move or rename a file"
|
|
11
|
+
PARAMETERS = {
|
|
12
|
+
source: { type: :string, description: "Path to the source file", required: true },
|
|
13
|
+
destination: { type: :string, description: "Path to the destination", required: true }
|
|
14
|
+
}.freeze
|
|
15
|
+
REQUIRES_CONFIRMATION = true
|
|
16
|
+
|
|
17
|
+
def execute(params)
|
|
18
|
+
resolved_source = resolve_path(params[:source])
|
|
19
|
+
return resolved_source if resolved_source.is_a?(Hash) && resolved_source[:error]
|
|
20
|
+
|
|
21
|
+
resolved_destination = resolve_path(params[:destination])
|
|
22
|
+
return resolved_destination if resolved_destination.is_a?(Hash) && resolved_destination[:error]
|
|
23
|
+
|
|
24
|
+
return error("Source file not found: #{params[:source]}") unless File.exist?(resolved_source)
|
|
25
|
+
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(resolved_destination))
|
|
27
|
+
FileUtils.mv(resolved_source, resolved_destination)
|
|
28
|
+
|
|
29
|
+
success(source: params[:source], destination: params[:destination])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Registry.register("move_file", MoveFile)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_tool"
|
|
4
|
+
require_relative "registry"
|
|
5
|
+
|
|
6
|
+
module Rubyn
|
|
7
|
+
module Tools
|
|
8
|
+
class PatchFile < BaseTool
|
|
9
|
+
DESCRIPTION = "Apply a targeted find-and-replace edit to a file"
|
|
10
|
+
PARAMETERS = {
|
|
11
|
+
path: { type: :string, description: "Path to the file to patch", required: true },
|
|
12
|
+
find: { type: :string, description: "The string to find in the file", required: true },
|
|
13
|
+
replace: { type: :string, description: "The string to replace with", required: true },
|
|
14
|
+
all_occurrences: { type: :boolean, description: "Replace all occurrences (default: false)", required: false }
|
|
15
|
+
}.freeze
|
|
16
|
+
REQUIRES_CONFIRMATION = true
|
|
17
|
+
|
|
18
|
+
def execute(params)
|
|
19
|
+
resolved = resolve_path(params[:path])
|
|
20
|
+
return resolved if resolved.is_a?(Hash) && resolved[:error]
|
|
21
|
+
|
|
22
|
+
return error("File not found: #{params[:path]}") unless File.exist?(resolved)
|
|
23
|
+
|
|
24
|
+
content = File.read(resolved)
|
|
25
|
+
find = params[:find]
|
|
26
|
+
|
|
27
|
+
return error("Find string not found in file: #{params[:path]}") unless content.include?(find)
|
|
28
|
+
|
|
29
|
+
all_occurrences = params[:all_occurrences] || false
|
|
30
|
+
|
|
31
|
+
if all_occurrences
|
|
32
|
+
replacements = content.scan(find).length
|
|
33
|
+
new_content = content.gsub(find, params[:replace])
|
|
34
|
+
else
|
|
35
|
+
replacements = 1
|
|
36
|
+
new_content = content.sub(find, params[:replace])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
File.write(resolved, new_content)
|
|
40
|
+
|
|
41
|
+
success(path: params[:path], replacements: replacements)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Registry.register("patch_file", PatchFile)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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 RailsGenerate < BaseTool
|
|
10
|
+
DESCRIPTION = "Run a Rails generator"
|
|
11
|
+
PARAMETERS = {
|
|
12
|
+
generator: { type: :string, description: "Generator name (e.g. migration, model)", required: true },
|
|
13
|
+
args: { type: :string, description: "Generator arguments (e.g. AddDeletedAtToOrders deleted_at:datetime)",
|
|
14
|
+
required: true }
|
|
15
|
+
}.freeze
|
|
16
|
+
REQUIRES_CONFIRMATION = true
|
|
17
|
+
|
|
18
|
+
def execute(params)
|
|
19
|
+
generator = params[:generator]
|
|
20
|
+
args = params[:args]
|
|
21
|
+
return error("generator is required") unless generator && !generator.strip.empty?
|
|
22
|
+
return error("args is required") unless args && !args.strip.empty?
|
|
23
|
+
|
|
24
|
+
command = "bundle exec rails generate #{generator} #{args}"
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
stdout, stderr, status = Open3.capture3(command, chdir: project_root)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
return error("Failed to run rails generate: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return error("rails generate failed: #{stderr}") unless status.exitstatus.zero?
|
|
33
|
+
|
|
34
|
+
success(output: stdout)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Registry.register("rails_generate", RailsGenerate)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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 RailsMigrate < BaseTool
|
|
10
|
+
DESCRIPTION = "Run pending database migrations"
|
|
11
|
+
PARAMETERS = {
|
|
12
|
+
direction: { type: :string, description: "Migration direction: up or down (default up)", required: false },
|
|
13
|
+
version: { type: :string, description: "Specific migration version", required: false }
|
|
14
|
+
}.freeze
|
|
15
|
+
REQUIRES_CONFIRMATION = true
|
|
16
|
+
|
|
17
|
+
def execute(params)
|
|
18
|
+
direction = (params[:direction] || "up").strip.downcase
|
|
19
|
+
version = params[:version]
|
|
20
|
+
|
|
21
|
+
command = build_command(direction, version)
|
|
22
|
+
return command if command.is_a?(Hash) && command[:error]
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
stdout, stderr, status = Open3.capture3(command, chdir: project_root)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
return error("Failed to run migration: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return error("Migration failed: #{stderr}") unless status.exitstatus.zero?
|
|
31
|
+
|
|
32
|
+
success(output: stdout)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_command(direction, version)
|
|
38
|
+
case direction
|
|
39
|
+
when "up"
|
|
40
|
+
"bundle exec rails db:migrate"
|
|
41
|
+
when "down"
|
|
42
|
+
if version && !version.strip.empty?
|
|
43
|
+
"bundle exec rails db:migrate:down VERSION=#{version}"
|
|
44
|
+
else
|
|
45
|
+
"bundle exec rails db:rollback"
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
error("Invalid direction: #{direction}. Must be 'up' or 'down'")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Registry.register("rails_migrate", RailsMigrate)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "base_tool"
|
|
6
|
+
require_relative "registry"
|
|
7
|
+
|
|
8
|
+
module Rubyn
|
|
9
|
+
module Tools
|
|
10
|
+
class RailsRoutes < BaseTool
|
|
11
|
+
DESCRIPTION = "Show Rails routes, optionally filtered by a pattern"
|
|
12
|
+
PARAMETERS = {
|
|
13
|
+
pattern: { type: :string, description: "Grep filter for routes", required: false }
|
|
14
|
+
}.freeze
|
|
15
|
+
REQUIRES_CONFIRMATION = false
|
|
16
|
+
|
|
17
|
+
def execute(params)
|
|
18
|
+
command = "bundle exec rails routes"
|
|
19
|
+
command = "#{command} -g #{Shellwords.shellescape(params[:pattern])}" if params[:pattern] && !params[:pattern].strip.empty?
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
stdout, stderr, status = Open3.capture3(command, chdir: project_root)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
return error("Failed to run rails routes: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return error("rails routes failed: #{stderr}") unless status.exitstatus.zero?
|
|
28
|
+
|
|
29
|
+
success(routes: stdout)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Registry.register("rails_routes", RailsRoutes)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_tool"
|
|
4
|
+
require_relative "registry"
|
|
5
|
+
|
|
6
|
+
module Rubyn
|
|
7
|
+
module Tools
|
|
8
|
+
class ReadFile < BaseTool
|
|
9
|
+
DESCRIPTION = "Read the contents of a file"
|
|
10
|
+
PARAMETERS = {
|
|
11
|
+
path: { type: :string, description: "Path to the file to read", required: true },
|
|
12
|
+
line_start: { type: :integer, description: "Starting line number (1-based)", required: false },
|
|
13
|
+
line_end: { type: :integer, description: "Ending line number (1-based, inclusive)", required: false }
|
|
14
|
+
}.freeze
|
|
15
|
+
REQUIRES_CONFIRMATION = false
|
|
16
|
+
|
|
17
|
+
def execute(params)
|
|
18
|
+
resolved = resolve_path(params[:path])
|
|
19
|
+
return resolved if resolved.is_a?(Hash) && resolved[:error]
|
|
20
|
+
|
|
21
|
+
return error("File not found: #{params[:path]}") unless File.exist?(resolved)
|
|
22
|
+
|
|
23
|
+
return error("Not a file: #{params[:path]}") unless File.file?(resolved)
|
|
24
|
+
|
|
25
|
+
lines = File.readlines(resolved)
|
|
26
|
+
total_lines = lines.length
|
|
27
|
+
|
|
28
|
+
line_start = params[:line_start]&.to_i
|
|
29
|
+
line_end = params[:line_end]&.to_i
|
|
30
|
+
|
|
31
|
+
return success(path: params[:path], content: lines.join, line_count: total_lines) unless line_start || line_end
|
|
32
|
+
|
|
33
|
+
line_start = [line_start || 1, 1].max
|
|
34
|
+
line_end = [line_end || total_lines, total_lines].min
|
|
35
|
+
|
|
36
|
+
return error("line_start (#{line_start}) cannot be greater than line_end (#{line_end})") if line_start > line_end
|
|
37
|
+
|
|
38
|
+
content = lines[(line_start - 1)..(line_end - 1)].join
|
|
39
|
+
success(path: params[:path], content: content, line_count: total_lines)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Registry.register("read_file", ReadFile)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyn
|
|
4
|
+
module Tools
|
|
5
|
+
class Registry
|
|
6
|
+
class << self
|
|
7
|
+
def register(name, klass)
|
|
8
|
+
@tools ||= {}
|
|
9
|
+
@tools[name.to_s] = klass
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get(name)
|
|
13
|
+
@tools ||= {}
|
|
14
|
+
@tools[name.to_s]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def all
|
|
18
|
+
@tools ||= {}
|
|
19
|
+
@tools.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tool_definitions
|
|
23
|
+
all.map { |name, klass| { name: name, description: klass::DESCRIPTION, parameters: klass::PARAMETERS } }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|