caruso 0.5.4

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.
data/Rakefile ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.rspec_opts = "--tag ~live"
8
+ end
9
+
10
+ RSpec::Core::RakeTask.new("spec:live") do |t|
11
+ ENV["RUN_LIVE_TESTS"] = "true"
12
+ t.rspec_opts = "--tag live"
13
+ end
14
+
15
+ RSpec::Core::RakeTask.new("spec:all") do |_t|
16
+ ENV["RUN_LIVE_TESTS"] = "true"
17
+ end
18
+
19
+ task default: :spec
20
+
21
+ desc "Run all tests including live tests"
22
+ task test_all: "spec:all"
23
+
24
+ desc "Run only live tests (requires network)"
25
+ task test_live: "spec:live"
26
+
27
+ namespace :bump do
28
+ def bump_version(type)
29
+ version_file = "lib/caruso/version.rb"
30
+ content = File.read(version_file)
31
+
32
+ unless content =~ /VERSION = "(\d+)\.(\d+)\.(\d+)"/
33
+ raise "Could not find version in #{version_file}"
34
+ end
35
+
36
+ major, minor, patch = $1.to_i, $2.to_i, $3.to_i
37
+
38
+ case type
39
+ when :major
40
+ major += 1
41
+ minor = 0
42
+ patch = 0
43
+ when :minor
44
+ minor += 1
45
+ patch = 0
46
+ when :patch
47
+ patch += 1
48
+ end
49
+
50
+ new_version = "#{major}.#{minor}.#{patch}"
51
+ new_content = content.sub(/VERSION = ".*"/, "VERSION = \"#{new_version}\"")
52
+
53
+ File.write(version_file, new_content)
54
+ puts "Bumped version to #{new_version}"
55
+ end
56
+
57
+ desc "Bump patch version"
58
+ task :patch do
59
+ bump_version(:patch)
60
+ end
61
+
62
+ desc "Bump minor version"
63
+ task :minor do
64
+ bump_version(:minor)
65
+ end
66
+
67
+ desc "Bump major version"
68
+ task :major do
69
+ bump_version(:major)
70
+ end
71
+ end
data/bin/caruso ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "caruso"
5
+
6
+ Caruso::CLI.start(ARGV)
data/caruso.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/caruso/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "caruso"
7
+ spec.version = Caruso::VERSION
8
+ spec.authors = ["Philipp Comans"]
9
+ spec.email = ["philipp.comans@gmail.com"]
10
+
11
+ spec.summary = "Sync steering docs from Claude Marketplaces to other agents."
12
+ spec.description = "A tool to fetch Claude Code plugins and adapt them into Cursor Rules or other agent contexts."
13
+ spec.homepage = "https://github.com/pcomans/caruso"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/pcomans/caruso"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[test/ spec/ features/ .git .circleci appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = "bin"
29
+ spec.executables = ["caruso"]
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Runtime dependencies
33
+ spec.add_dependency "faraday", "~> 2.0" # For HTTP requests
34
+ spec.add_dependency "git", "~> 1.19" # For cloning repos
35
+ spec.add_dependency "thor", "~> 1.3" # For CLI
36
+
37
+ # Development dependencies
38
+ spec.add_development_dependency "aruba", "~> 2.1"
39
+ spec.add_development_dependency "rake", "~> 13.0"
40
+ spec.add_development_dependency "rspec", "~> 3.13"
41
+ spec.add_development_dependency "rubocop", "~> 1.60"
42
+ spec.add_development_dependency "timecop", "~> 0.9"
43
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+
6
+ module Caruso
7
+ class Adapter
8
+ attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
9
+
10
+ def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
11
+ @files = files
12
+ @target_dir = target_dir
13
+ @agent = agent
14
+ @marketplace_name = marketplace_name
15
+ @plugin_name = plugin_name
16
+ FileUtils.mkdir_p(@target_dir)
17
+ end
18
+
19
+ def adapt
20
+ created_files = []
21
+ files.each do |file_path|
22
+ content = File.read(file_path)
23
+ adapted_content = inject_metadata(content, file_path)
24
+ created_file = save_file(file_path, adapted_content)
25
+ created_files << created_file
26
+ end
27
+ created_files
28
+ end
29
+
30
+ private
31
+
32
+ def inject_metadata(content, file_path)
33
+ # Check if frontmatter exists
34
+ if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
35
+ # If it exists, we might need to append to it or modify it
36
+ # For now, we assume existing frontmatter is "good enough" but might need 'globs' for Cursor
37
+ ensure_cursor_globs(content) if agent == :cursor
38
+ else
39
+ # No frontmatter, prepend it
40
+ create_frontmatter(file_path) + content
41
+ end
42
+ end
43
+
44
+ def ensure_cursor_globs(content)
45
+ # Add required Cursor metadata fields if missing
46
+ # globs: [] enables semantic search (Apply Intelligently)
47
+ # alwaysApply: false means it won't apply to every chat session
48
+
49
+ unless content.include?("globs:")
50
+ content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
51
+ end
52
+
53
+ unless content.include?("alwaysApply:")
54
+ # Add after the first line of frontmatter
55
+ content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
56
+ end
57
+
58
+ content
59
+ end
60
+
61
+ def create_frontmatter(file_path)
62
+ filename = File.basename(file_path)
63
+ <<~YAML
64
+ ---
65
+ description: Imported rule from #{filename}
66
+ globs: []
67
+ alwaysApply: false
68
+ ---
69
+ YAML
70
+ end
71
+
72
+ def save_file(original_path, content)
73
+ filename = File.basename(original_path, ".*")
74
+
75
+ # Rename SKILL.md to the skill name (parent directory) to avoid collisions
76
+ if filename.casecmp("skill").zero?
77
+ filename = File.basename(File.dirname(original_path))
78
+ end
79
+
80
+ extension = agent == :cursor ? ".mdc" : ".md"
81
+ output_filename = "#{filename}#{extension}"
82
+
83
+ # Extract component type from original path (commands/agents/skills)
84
+ component_type = extract_component_type(original_path)
85
+
86
+ # Build nested directory structure for Cursor
87
+ # Build nested directory structure for Cursor
88
+ # Structure: .cursor/rules/caruso/marketplace/plugin/component-type/file.mdc
89
+ subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
90
+ output_dir = File.join(@target_dir, subdirs)
91
+ FileUtils.mkdir_p(output_dir)
92
+ target_path = File.join(output_dir, output_filename)
93
+
94
+ File.write(target_path, content)
95
+ puts "Saved: #{target_path}"
96
+
97
+ # Return relative path from target_dir
98
+ File.join(subdirs, output_filename)
99
+ end
100
+
101
+ def extract_component_type(file_path)
102
+ # Extract component type (commands/agents/skills) from path
103
+ return "commands" if file_path.include?("/commands/")
104
+ return "agents" if file_path.include?("/agents/")
105
+ return "skills" if file_path.include?("/skills/")
106
+
107
+ raise Caruso::Error, "Cannot determine component type from path: #{file_path}"
108
+ end
109
+ end
110
+ end