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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 718b4ebf40d78b4314d5ba6869ccf65d4e7ba02ada56c0c9b98e5de973d538c7
4
- data.tar.gz: c6fcc0b7f7e45b6cd5e9235e954ea0c1cdc5d49fd729f0c6268e135ef7ac6bce
3
+ metadata.gz: 9f6520caf8c5e8a4946369e9722ca36685a6c79b933b67336bc02b39b0fca359
4
+ data.tar.gz: d1b2f7b52ae5746909e173b75fc31e601417bca1ef55af889a56f802a8ae1d24
5
5
  SHA512:
6
- metadata.gz: 76693951a7604897ad48efaa2e81570165e483ab20e651ff757d009a4c179f21a7738f9912229dd768205736ee076fbf2f865e1b7549385ce4c320132674c08a
7
- data.tar.gz: 691886bb54d63379e80df0663b0825f89b2dd7ece512fc2e859b90c7e09cc830c63c3f6c2cad4fe2dc6f83974eec03d728cb1c150240cf424821cb6948d2b7f4
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
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "marketplaces": {},
4
+ "plugins": {}
5
+ }
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.
@@ -1,113 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
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
- attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
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
- FileUtils.mkdir_p(@target_dir)
13
+ @agent = agent
19
14
  end
20
15
 
21
16
  def adapt
22
- created_files = []
23
- files.each do |file_path|
24
- content = SafeFile.read(file_path)
25
-
26
- adapted_content = inject_metadata(content, file_path)
27
- created_file = save_file(file_path, adapted_content)
28
- created_files << created_file
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 NAME", "Remove a marketplace"
55
- def remove(name)
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
- # Remove from config
59
- config_manager.remove_marketplace(name)
60
-
61
- # Remove from registry
62
- registry = Caruso::MarketplaceRegistry.new
63
- marketplace = registry.get_marketplace(name)
64
- if marketplace
65
- cache_dir = marketplace["install_location"]
66
- registry.remove_marketplace(name)
67
-
68
- # Inform about cache directory
69
- if Dir.exist?(cache_dir)
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
- files_to_remove.each do |file|
266
- full_path = File.join(config_manager.project_dir, file)
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