caruso 0.6.0 → 0.6.3
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 +4 -4
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +7 -0
- data/caruso.json +5 -0
- data/impl.md +111 -0
- data/lib/caruso/adapter.rb +10 -97
- data/lib/caruso/adapters/base.rb +102 -0
- data/lib/caruso/adapters/dispatcher.rb +53 -0
- data/lib/caruso/adapters/markdown_adapter.rb +23 -0
- data/lib/caruso/adapters/skill_adapter.rb +100 -0
- data/lib/caruso/cli.rb +21 -24
- data/lib/caruso/config_manager.rb +17 -0
- data/lib/caruso/fetcher.rb +69 -29
- data/lib/caruso/remover.rb +60 -0
- data/lib/caruso/version.rb +1 -1
- data/lib/caruso.rb +1 -0
- data/reference/claude_code_create_marketplaces.md +511 -0
- data/reference/claude_code_hooks.md +1137 -0
- data/reference/claude_code_plugins.md +769 -0
- data/reference/claude_code_slash_commands.md +515 -0
- data/reference/cursor_commands.md +90 -0
- data/reference/cursor_hooks.md +467 -0
- data/reference/cursor_modes.md +105 -0
- data/reference/cursor_rules.md +246 -0
- data/reference/steering_docs.md +57 -0
- data/tasks.md +22 -0
- metadata +19 -3
- data/reference/plugins_reference.md +0 -376
- /data/reference/{marketplace.md → claude_code_marketplaces.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f6520caf8c5e8a4946369e9722ca36685a6c79b933b67336bc02b39b0fca359
|
|
4
|
+
data.tar.gz: d1b2f7b52ae5746909e173b75fc31e601417bca1ef55af889a56f802a8ae1d24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48d8c7c701c7c1a380672ac77f3025dc69c90c4a5ef7d6fd6e72e50358e69440a9c57ff022f52f94c4970faead782ba04c223246bf766045e4508bca427cf4f8
|
|
7
|
+
data.tar.gz: 293893a825850e919ca8141203f307c300e9fdba931f80aafadad15a766dd5f28ff6bd061eae53891c5a4da41f061e2b4b6bea5f0108d9b79396e63d82f49296
|
data/AGENTS.md
CHANGED
|
@@ -273,4 +273,5 @@ Version is managed in `lib/caruso/version.rb`.
|
|
|
273
273
|
|
|
274
274
|
# Memory
|
|
275
275
|
- The goal is a clean, correct, consistent implementation. Never implement fallbacks that hide errors or engage in defensive programming.
|
|
276
|
+
- **Idempotency**: Removal commands (`marketplace remove`, `plugin uninstall`) are designed to be idempotent. They exit successfully (0) if the target does not exist. This is intentional for automation friendliness and is NOT considered "hiding errors".
|
|
276
277
|
- Treat the vendor directory .cursor/rules/caruso/ as a build artifact
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.2] - 2025-12-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `marketplace remove` now exits gracefully (code 0) when marketplace is not found, making it idempotent
|
|
14
|
+
- Documented idempotent behavior of `marketplace remove` and `plugin uninstall` in README
|
|
15
|
+
|
|
10
16
|
## [0.6.0] - 2025-11-24
|
|
11
17
|
|
|
12
18
|
### Security
|
|
@@ -20,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
26
|
- `Adapter` now strictly validates file existence and raises errors instead of silently skipping invalid files
|
|
21
27
|
- `Fetcher` now filters glob results to ensure they remain within trusted plugin directories
|
|
22
28
|
|
|
29
|
+
|
|
23
30
|
## [0.5.3] - 2025-11-23
|
|
24
31
|
|
|
25
32
|
### Changed
|
data/caruso.json
ADDED
data/impl.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Implementation Plan - Evolve Caruso for Claude Code Support
|
|
2
|
+
|
|
3
|
+
Refining caruso to provide robust support for Claude Code's Commands, Skills, and Hooks, adapting them specifically for the Cursor environment.
|
|
4
|
+
|
|
5
|
+
## Goal Description
|
|
6
|
+
|
|
7
|
+
The goal is to transform caruso from a simple markdown copier into a smart adapter that bridges the gap between Claude Code's plugin architecture and Cursor's steering mechanisms. This involves:
|
|
8
|
+
|
|
9
|
+
- **Skills**: deeply adapting skills by not just copying the markdown, but also bundling associated scripts/ and making them executable.
|
|
10
|
+
- **Commands**: mapping Claude Code commands to Cursor's new .cursor/commands/ structure.
|
|
11
|
+
- **Hooks**: translating Claude Code's hooks/hooks.json events into Cursor's .cursor/hooks.json format.
|
|
12
|
+
- **Fetcher**: upgrading the fetcher to retrieve auxiliary files (scripts, configs) beyond just `.md` files.
|
|
13
|
+
|
|
14
|
+
## User Review Required
|
|
15
|
+
|
|
16
|
+
> [!NOTE]
|
|
17
|
+
> **Script Execution Security:** We will trust the "trusted workspace" model of Cursor. This means we will automatically fetch scripts and make them executable (`chmod +x`). Users relying on Caruso are expected to audit the plugins they install, similar to how they would audit an npm package.
|
|
18
|
+
|
|
19
|
+
## Proposed Changes
|
|
20
|
+
|
|
21
|
+
### 1. Fetcher Upgrades (`lib/caruso/fetcher.rb`)
|
|
22
|
+
|
|
23
|
+
The current fetcher is overly focused on *.md files. We need to broaden it to fetch associated resources using an Additive Strategy.
|
|
24
|
+
|
|
25
|
+
#### [MODIFY] `fetcher.rb`
|
|
26
|
+
|
|
27
|
+
- **Additive Discovery**: In `fetch_plugin`:
|
|
28
|
+
- **Logic**:
|
|
29
|
+
1. **Check Manifest**: Look for `skills` field in `plugin` object (string or array).
|
|
30
|
+
2. **If Present**: Recursively fetch all files in those specific paths.
|
|
31
|
+
3. **If Absent**: Fallback to scanning the default `skills/` directory (recursively).
|
|
32
|
+
4. **Other Components**: Continue scanning for `commands/`, `agents/`, `hooks/` as before.
|
|
33
|
+
|
|
34
|
+
- **Support Resource Types**:
|
|
35
|
+
- `skills`: Fetch `SKILL.md` AND recursively fetch `scripts/` directories if found within the skill path.
|
|
36
|
+
|
|
37
|
+
- `hooks`: Fetch the `hooks/hooks.json` file (or inline config).
|
|
38
|
+
- `commands`: Fetch markdown files.
|
|
39
|
+
- `agents`: Fetch markdown files.
|
|
40
|
+
|
|
41
|
+
### 2. Adapter Architecture Refactor (`lib/caruso/`)
|
|
42
|
+
|
|
43
|
+
Refactor the monolithic Adapter class into a dispatcher with specialized strategies.
|
|
44
|
+
|
|
45
|
+
#### [MODIFY] `adapter.rb`
|
|
46
|
+
Change `Adapter#adapt` to identify the component type and delegate to the appropriate sub-adapter.
|
|
47
|
+
|
|
48
|
+
#### [NEW] `adapters/base.rb`
|
|
49
|
+
Shared logic for file writing, frontmatter injection, and path sanitization.
|
|
50
|
+
|
|
51
|
+
#### [NEW] `adapters/skill_adapter.rb`
|
|
52
|
+
**Input:** `skills/<name>/SKILL.md` + `skills/<name>/scripts/*`
|
|
53
|
+
**Output:**
|
|
54
|
+
- `.cursor/rules/caruso/<marketplace>/<skill>/<skill>.mdc` (The rule)
|
|
55
|
+
- `.cursor/scripts/caruso/<marketplace>/<skill>/*` (The scripts)
|
|
56
|
+
|
|
57
|
+
**Logic:**
|
|
58
|
+
1. Copy scripts to `.cursor/scripts/caruso/<marketplace>/<skill>/`.
|
|
59
|
+
2. Ensure scripts are executable (`chmod +x`).
|
|
60
|
+
3. **Paths:** Do NOT rewrite paths in the markdown (to avoid messiness).
|
|
61
|
+
4. **Context:** Inject a location hint into the Rule's frontmatter `description` or prepended content:
|
|
62
|
+
```yaml
|
|
63
|
+
description: Imported from <skill>. Scripts located at: .cursor/scripts/caruso/<marketplace>/<skill>/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### [NEW] `adapters/agent_adapter.rb`
|
|
67
|
+
**Input:** `agents/<name>.md`
|
|
68
|
+
**Output:** `.cursor/rules/caruso/<marketplace>/agents/<name>.mdc`
|
|
69
|
+
**Logic:**
|
|
70
|
+
- Copy content.
|
|
71
|
+
- Wrap as a "Persona" rule if needed, or simple markdown rule.
|
|
72
|
+
|
|
73
|
+
#### [NEW] `adapters/command_adapter.rb`
|
|
74
|
+
**Input:** `commands/<name>.md`
|
|
75
|
+
**Output:** `.cursor/commands/<name>.md`
|
|
76
|
+
**Logic:**
|
|
77
|
+
- Basic markdown copy.
|
|
78
|
+
- Frontmatter cleanup (remove Claude-specific fields if necessary).
|
|
79
|
+
|
|
80
|
+
#### [NEW] `adapters/hook_adapter.rb`
|
|
81
|
+
**Input:** `hooks/hooks.json`
|
|
82
|
+
**Output:** Merged/Updated `.cursor/hooks.json`
|
|
83
|
+
**Logic:**
|
|
84
|
+
- Parse source JSON.
|
|
85
|
+
- Map events (e.g., `PostToolUse` -> `afterFileEdit`).
|
|
86
|
+
- Write/Merge into project's `hooks.json`.
|
|
87
|
+
|
|
88
|
+
### 3. CLI Updates (`lib/caruso/cli.rb`)
|
|
89
|
+
|
|
90
|
+
#### [MODIFY] `cli.rb`
|
|
91
|
+
- Update `install` command to handle multiple file types returned by the upgraded fetcher.
|
|
92
|
+
- Ensure `uninstall` cleans up the directories (scripts, etc) correctly.
|
|
93
|
+
|
|
94
|
+
## Verification Plan
|
|
95
|
+
|
|
96
|
+
### Automated Tests
|
|
97
|
+
- **Fetcher Tests**: Mock a plugin structure with `scripts/` and verify they are returned in the file list.
|
|
98
|
+
- **Adapter Tests**:
|
|
99
|
+
- Feed a `SKILL.md` + script to `SkillAdapter` and verify output structure and chmod.
|
|
100
|
+
- Feed a `hooks.json` and verify the event translation.
|
|
101
|
+
|
|
102
|
+
### Manual Verification
|
|
103
|
+
- **Skills**: Install a plugin with a script (e.g., a "linter" skill).
|
|
104
|
+
- Verify file exists at `.cursor/scripts/.../lint.sh`.
|
|
105
|
+
- Verify it is executable.
|
|
106
|
+
- Verify the Rule markdown is present.
|
|
107
|
+
- **Commands**: Install a command plugin.
|
|
108
|
+
- Verify file exists at `.cursor/commands/...`.
|
|
109
|
+
- Test running the command in Cursor (Cmd+K `/command`).
|
|
110
|
+
- **Hooks**: Install a hook plugin.
|
|
111
|
+
- Check `.cursor/hooks.json` contains the mapped event.
|
data/lib/caruso/adapter.rb
CHANGED
|
@@ -1,113 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "yaml"
|
|
5
|
-
require_relative "safe_file"
|
|
6
|
-
require_relative "path_sanitizer"
|
|
3
|
+
require_relative "adapters/dispatcher"
|
|
7
4
|
|
|
8
5
|
module Caruso
|
|
9
6
|
class Adapter
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
# Preserving the interface for CLI compatibility
|
|
12
8
|
def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
13
9
|
@files = files
|
|
14
10
|
@target_dir = target_dir
|
|
15
|
-
@agent = agent
|
|
16
11
|
@marketplace_name = marketplace_name
|
|
17
12
|
@plugin_name = plugin_name
|
|
18
|
-
|
|
13
|
+
@agent = agent
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
def adapt
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
30
|
-
created_files
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def inject_metadata(content, file_path)
|
|
36
|
-
# Check if frontmatter exists
|
|
37
|
-
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
38
|
-
# If it exists, we might need to append to it or modify it
|
|
39
|
-
# For now, we assume existing frontmatter is "good enough" but might need 'globs' for Cursor
|
|
40
|
-
ensure_cursor_globs(content) if agent == :cursor
|
|
41
|
-
else
|
|
42
|
-
# No frontmatter, prepend it
|
|
43
|
-
create_frontmatter(file_path) + content
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def ensure_cursor_globs(content)
|
|
48
|
-
# Add required Cursor metadata fields if missing
|
|
49
|
-
# globs: [] enables semantic search (Apply Intelligently)
|
|
50
|
-
# alwaysApply: false means it won't apply to every chat session
|
|
51
|
-
|
|
52
|
-
unless content.include?("globs:")
|
|
53
|
-
content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
unless content.include?("alwaysApply:")
|
|
57
|
-
# Add after the first line of frontmatter
|
|
58
|
-
content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
content
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def create_frontmatter(file_path)
|
|
65
|
-
filename = File.basename(file_path)
|
|
66
|
-
<<~YAML
|
|
67
|
-
---
|
|
68
|
-
description: Imported rule from #{filename}
|
|
69
|
-
globs: []
|
|
70
|
-
alwaysApply: false
|
|
71
|
-
---
|
|
72
|
-
YAML
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def save_file(original_path, content)
|
|
76
|
-
filename = File.basename(original_path, ".*")
|
|
77
|
-
|
|
78
|
-
# Rename SKILL.md to the skill name (parent directory) to avoid collisions
|
|
79
|
-
if filename.casecmp("skill").zero?
|
|
80
|
-
filename = File.basename(File.dirname(original_path))
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
extension = agent == :cursor ? ".mdc" : ".md"
|
|
84
|
-
output_filename = "#{filename}#{extension}"
|
|
85
|
-
|
|
86
|
-
# Extract component type from original path (commands/agents/skills)
|
|
87
|
-
component_type = extract_component_type(original_path)
|
|
88
|
-
|
|
89
|
-
# Build nested directory structure for Cursor
|
|
90
|
-
# Build nested directory structure for Cursor
|
|
91
|
-
# Structure: .cursor/rules/caruso/marketplace/plugin/component-type/file.mdc
|
|
92
|
-
subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
|
|
93
|
-
output_dir = File.join(@target_dir, subdirs)
|
|
94
|
-
FileUtils.mkdir_p(output_dir)
|
|
95
|
-
target_path = File.join(output_dir, output_filename)
|
|
96
|
-
|
|
97
|
-
File.write(target_path, content)
|
|
98
|
-
puts "Saved: #{target_path}"
|
|
99
|
-
|
|
100
|
-
# Return relative path from target_dir
|
|
101
|
-
File.join(subdirs, output_filename)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def extract_component_type(file_path)
|
|
105
|
-
# Extract component type (commands/agents/skills) from path
|
|
106
|
-
return "commands" if file_path.include?("/commands/")
|
|
107
|
-
return "agents" if file_path.include?("/agents/")
|
|
108
|
-
return "skills" if file_path.include?("/skills/")
|
|
109
|
-
|
|
110
|
-
raise Caruso::Error, "Cannot determine component type from path: #{file_path}"
|
|
17
|
+
Caruso::Adapters::Dispatcher.adapt(
|
|
18
|
+
@files,
|
|
19
|
+
target_dir: @target_dir,
|
|
20
|
+
marketplace_name: @marketplace_name,
|
|
21
|
+
plugin_name: @plugin_name,
|
|
22
|
+
agent: @agent
|
|
23
|
+
)
|
|
111
24
|
end
|
|
112
25
|
end
|
|
113
26
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "../safe_file"
|
|
6
|
+
require_relative "../path_sanitizer"
|
|
7
|
+
|
|
8
|
+
module Caruso
|
|
9
|
+
module Adapters
|
|
10
|
+
class Base
|
|
11
|
+
attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
|
|
12
|
+
|
|
13
|
+
def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
14
|
+
@files = files
|
|
15
|
+
@target_dir = target_dir
|
|
16
|
+
@agent = agent
|
|
17
|
+
@marketplace_name = marketplace_name
|
|
18
|
+
@plugin_name = plugin_name
|
|
19
|
+
FileUtils.mkdir_p(@target_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def adapt
|
|
23
|
+
raise NotImplementedError, "#{self.class.name}#adapt must be implemented"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def save_file(relative_path, content, extension: nil)
|
|
29
|
+
filename = File.basename(relative_path, ".*")
|
|
30
|
+
|
|
31
|
+
# Preserve original extension if none provided
|
|
32
|
+
ext = extension || File.extname(relative_path)
|
|
33
|
+
|
|
34
|
+
# Rename SKILL.md to the skill name (parent directory) to avoid collisions
|
|
35
|
+
# This is specific to Skills, might move to SkillAdapter later, but keeping behavior for now
|
|
36
|
+
if filename.casecmp("skill").zero?
|
|
37
|
+
filename = File.basename(File.dirname(relative_path))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
output_filename = "#{filename}#{ext}"
|
|
41
|
+
|
|
42
|
+
# Build nested directory structure: .cursor/rules/caruso/marketplace/plugin/component/file
|
|
43
|
+
# Component type is derived from the class name or passed in?
|
|
44
|
+
# For base, we might need a way to determine output path more flexibly.
|
|
45
|
+
# But sticking to current behavior:
|
|
46
|
+
|
|
47
|
+
component_type = extract_component_type(relative_path)
|
|
48
|
+
subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
|
|
49
|
+
output_dir = File.join(target_dir, subdirs)
|
|
50
|
+
|
|
51
|
+
FileUtils.mkdir_p(output_dir)
|
|
52
|
+
target_path = File.join(output_dir, output_filename)
|
|
53
|
+
|
|
54
|
+
File.write(target_path, content)
|
|
55
|
+
puts "Saved: #{target_path}"
|
|
56
|
+
|
|
57
|
+
File.join(subdirs, output_filename)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_component_type(file_path)
|
|
61
|
+
# Extract component type (commands/agents/skills) from path
|
|
62
|
+
return "commands" if file_path.include?("/commands/")
|
|
63
|
+
return "agents" if file_path.include?("/agents/")
|
|
64
|
+
return "skills" if file_path.include?("/skills/")
|
|
65
|
+
|
|
66
|
+
# Fallback or specific handling for other types
|
|
67
|
+
"misc"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inject_metadata(content, file_path)
|
|
71
|
+
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
72
|
+
ensure_cursor_globs(content) if agent == :cursor
|
|
73
|
+
else
|
|
74
|
+
create_frontmatter(file_path) + content
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ensure_cursor_globs(content)
|
|
79
|
+
unless content.include?("globs:")
|
|
80
|
+
content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
unless content.include?("alwaysApply:")
|
|
84
|
+
content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
content
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def create_frontmatter(file_path)
|
|
91
|
+
filename = File.basename(file_path)
|
|
92
|
+
<<~YAML
|
|
93
|
+
---
|
|
94
|
+
description: Imported rule from #{filename}
|
|
95
|
+
globs: []
|
|
96
|
+
alwaysApply: false
|
|
97
|
+
---
|
|
98
|
+
YAML
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "markdown_adapter"
|
|
4
|
+
require_relative "skill_adapter"
|
|
5
|
+
|
|
6
|
+
module Caruso
|
|
7
|
+
module Adapters
|
|
8
|
+
class Dispatcher
|
|
9
|
+
def self.adapt(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
10
|
+
created_files = []
|
|
11
|
+
remaining_files = files.dup
|
|
12
|
+
|
|
13
|
+
# 1. Identify and Process Skill Clusters
|
|
14
|
+
# Find all SKILL.md files to serve as anchors
|
|
15
|
+
skill_anchors = remaining_files.select { |f| File.basename(f).casecmp("skill.md").zero? }
|
|
16
|
+
|
|
17
|
+
skill_anchors.each do |anchor|
|
|
18
|
+
skill_dir = File.dirname(anchor)
|
|
19
|
+
|
|
20
|
+
# Find all files that belong to this skill's directory (recursive)
|
|
21
|
+
skill_cluster = remaining_files.select { |f| f.start_with?(skill_dir) }
|
|
22
|
+
|
|
23
|
+
# Use SkillAdapter for this cluster
|
|
24
|
+
adapter = SkillAdapter.new(
|
|
25
|
+
skill_cluster,
|
|
26
|
+
target_dir: target_dir,
|
|
27
|
+
marketplace_name: marketplace_name,
|
|
28
|
+
plugin_name: plugin_name,
|
|
29
|
+
agent: agent
|
|
30
|
+
)
|
|
31
|
+
created_files.concat(adapter.adapt)
|
|
32
|
+
|
|
33
|
+
# Remove processed files
|
|
34
|
+
remaining_files -= skill_cluster
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 2. Process Remaining Files (Commands, Agents, etc.) via MarkdownAdapter
|
|
38
|
+
if remaining_files.any?
|
|
39
|
+
adapter = MarkdownAdapter.new(
|
|
40
|
+
remaining_files,
|
|
41
|
+
target_dir: target_dir,
|
|
42
|
+
marketplace_name: marketplace_name,
|
|
43
|
+
plugin_name: plugin_name,
|
|
44
|
+
agent: agent
|
|
45
|
+
)
|
|
46
|
+
created_files.concat(adapter.adapt)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
created_files
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Caruso
|
|
6
|
+
module Adapters
|
|
7
|
+
class MarkdownAdapter < Base
|
|
8
|
+
def adapt
|
|
9
|
+
created_files = []
|
|
10
|
+
files.each do |file_path|
|
|
11
|
+
content = SafeFile.read(file_path)
|
|
12
|
+
adapted_content = inject_metadata(content, file_path)
|
|
13
|
+
|
|
14
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
15
|
+
created_file = save_file(file_path, adapted_content, extension: extension)
|
|
16
|
+
|
|
17
|
+
created_files << created_file
|
|
18
|
+
end
|
|
19
|
+
created_files
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Caruso
|
|
6
|
+
module Adapters
|
|
7
|
+
class SkillAdapter < Base
|
|
8
|
+
def adapt
|
|
9
|
+
created_files = []
|
|
10
|
+
|
|
11
|
+
# Separate SKILL.md from other files (scripts, etc.)
|
|
12
|
+
skill_file = files.find { |f| File.basename(f).casecmp("skill.md").zero? }
|
|
13
|
+
other_files = files - [skill_file]
|
|
14
|
+
|
|
15
|
+
if skill_file
|
|
16
|
+
skill_root = File.dirname(skill_file)
|
|
17
|
+
skill_name = File.basename(skill_root)
|
|
18
|
+
|
|
19
|
+
# Inject script location hint
|
|
20
|
+
# Targeted script location: .cursor/scripts/caruso/<market>/<plugin>/<skill_name>/
|
|
21
|
+
script_location = ".cursor/scripts/caruso/#{marketplace_name}/#{plugin_name}/#{skill_name}/"
|
|
22
|
+
|
|
23
|
+
content = SafeFile.read(skill_file)
|
|
24
|
+
adapted_content = inject_skill_metadata(content, skill_file, script_location)
|
|
25
|
+
|
|
26
|
+
# Save as Rule (.mdc)
|
|
27
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
28
|
+
created_file = save_file(skill_file, adapted_content, extension: extension)
|
|
29
|
+
created_files << created_file
|
|
30
|
+
|
|
31
|
+
# Process Scripts/Assets -> .cursor/scripts/...
|
|
32
|
+
other_files.each do |file_path|
|
|
33
|
+
# Calculate path relative to the skill root
|
|
34
|
+
# e.g. source: .../skills/auth/scripts/login.sh
|
|
35
|
+
# root: .../skills/auth
|
|
36
|
+
# rel: scripts/login.sh
|
|
37
|
+
relative_path_from_root = file_path.sub(skill_root + "/", "")
|
|
38
|
+
|
|
39
|
+
created_script = save_script(file_path, skill_name, relative_path_from_root)
|
|
40
|
+
created_files << created_script
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
created_files
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def inject_skill_metadata(content, file_path, script_location)
|
|
50
|
+
# Inject script location into description
|
|
51
|
+
hint = "Scripts located at: #{script_location}"
|
|
52
|
+
|
|
53
|
+
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
54
|
+
# Update existing frontmatter
|
|
55
|
+
content.sub!(/^description: (.*)$/, "description: \\1. #{hint}")
|
|
56
|
+
ensure_cursor_globs(content)
|
|
57
|
+
else
|
|
58
|
+
# Create new frontmatter with hint
|
|
59
|
+
create_skill_frontmatter(file_path, hint) + content
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_skill_frontmatter(file_path, hint)
|
|
64
|
+
filename = File.basename(file_path)
|
|
65
|
+
<<~YAML
|
|
66
|
+
---
|
|
67
|
+
description: Imported skill from #{filename}. #{hint}
|
|
68
|
+
globs: []
|
|
69
|
+
alwaysApply: false
|
|
70
|
+
---
|
|
71
|
+
YAML
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save_script(source_path, skill_name, relative_sub_path)
|
|
75
|
+
# Construct target path in .cursor/scripts
|
|
76
|
+
# .cursor/scripts/caruso/<marketplace>/<plugin>/<skill_name>/<relative_sub_path>
|
|
77
|
+
|
|
78
|
+
scripts_root = File.join(target_dir, "..", "scripts", "caruso", marketplace_name, plugin_name, skill_name)
|
|
79
|
+
# Note: target_dir passed to adapter is usually .cursor/rules.
|
|
80
|
+
# So .. -> .cursor -> scripts
|
|
81
|
+
|
|
82
|
+
target_path = File.join(scripts_root, relative_sub_path)
|
|
83
|
+
output_dir = File.dirname(target_path)
|
|
84
|
+
|
|
85
|
+
FileUtils.mkdir_p(output_dir)
|
|
86
|
+
FileUtils.cp(source_path, target_path)
|
|
87
|
+
|
|
88
|
+
# Make executable
|
|
89
|
+
File.chmod(0755, target_path)
|
|
90
|
+
|
|
91
|
+
puts "Saved script: #{target_path}"
|
|
92
|
+
|
|
93
|
+
# Return relative path for tracking/reporting
|
|
94
|
+
# We start from .cursor (parent of target_dir) ideally?
|
|
95
|
+
# Or just return the absolute path for now?
|
|
96
|
+
target_path
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/caruso/cli.rb
CHANGED
|
@@ -51,27 +51,30 @@ module Caruso
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
desc "remove
|
|
55
|
-
def remove(
|
|
54
|
+
desc "remove NAME_OR_URL", "Remove a marketplace"
|
|
55
|
+
def remove(name_or_url)
|
|
56
56
|
config_manager = load_config
|
|
57
|
+
marketplaces = config_manager.list_marketplaces
|
|
57
58
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
puts "Cache directory still exists at: #{cache_dir}"
|
|
71
|
-
puts "Run 'rm -rf #{cache_dir}' to delete it if desired."
|
|
59
|
+
# Try to find by name first
|
|
60
|
+
if marketplaces.key?(name_or_url)
|
|
61
|
+
name = name_or_url
|
|
62
|
+
else
|
|
63
|
+
# Try to find by URL
|
|
64
|
+
# We need to check exact match or maybe normalized match
|
|
65
|
+
match = marketplaces.find { |_, details| details["url"] == name_or_url || details["url"].chomp(".git") == name_or_url }
|
|
66
|
+
if match
|
|
67
|
+
name = match[0]
|
|
68
|
+
else
|
|
69
|
+
puts "Marketplace '#{name_or_url}' not found."
|
|
70
|
+
return
|
|
72
71
|
end
|
|
73
72
|
end
|
|
74
73
|
|
|
74
|
+
# Use Remover to handle cleanup
|
|
75
|
+
remover = Caruso::Remover.new(config_manager)
|
|
76
|
+
remover.remove_marketplace(name)
|
|
77
|
+
|
|
75
78
|
puts "Removed marketplace '#{name}'"
|
|
76
79
|
end
|
|
77
80
|
|
|
@@ -260,15 +263,9 @@ module Caruso
|
|
|
260
263
|
end
|
|
261
264
|
|
|
262
265
|
puts "Removing #{plugin_key}..."
|
|
263
|
-
files_to_remove = config_manager.remove_plugin(plugin_key)
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if File.exist?(full_path)
|
|
268
|
-
File.delete(full_path)
|
|
269
|
-
puts " Deleted #{file}"
|
|
270
|
-
end
|
|
271
|
-
end
|
|
267
|
+
remover = Caruso::Remover.new(config_manager)
|
|
268
|
+
remover.remove_plugin(plugin_key)
|
|
272
269
|
|
|
273
270
|
puts "Uninstalled #{plugin_key}."
|
|
274
271
|
end
|
|
@@ -150,6 +150,23 @@ module Caruso
|
|
|
150
150
|
save_project_config(data)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
def remove_marketplace_with_plugins(marketplace_name)
|
|
154
|
+
files_to_remove = []
|
|
155
|
+
|
|
156
|
+
# Find and remove all plugins associated with this marketplace
|
|
157
|
+
installed_plugins = list_plugins
|
|
158
|
+
installed_plugins.each do |plugin_key, details|
|
|
159
|
+
if details["marketplace"] == marketplace_name
|
|
160
|
+
files_to_remove.concat(remove_plugin(plugin_key))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Remove the marketplace itself
|
|
165
|
+
remove_marketplace(marketplace_name)
|
|
166
|
+
|
|
167
|
+
files_to_remove.uniq
|
|
168
|
+
end
|
|
169
|
+
|
|
153
170
|
def list_marketplaces
|
|
154
171
|
load_project_config["marketplaces"] || {}
|
|
155
172
|
end
|