caruso 0.6.2 → 0.7.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 +4 -4
- data/CHANGELOG.md +41 -0
- data/impl.md +111 -0
- data/lib/caruso/adapter.rb +16 -96
- data/lib/caruso/adapters/base.rb +102 -0
- data/lib/caruso/adapters/command_adapter.rb +88 -0
- data/lib/caruso/adapters/dispatcher.rb +83 -0
- data/lib/caruso/adapters/hook_adapter.rb +222 -0
- data/lib/caruso/adapters/markdown_adapter.rb +23 -0
- data/lib/caruso/adapters/skill_adapter.rb +87 -0
- data/lib/caruso/cli.rb +22 -6
- data/lib/caruso/config_manager.rb +30 -28
- data/lib/caruso/fetcher.rb +137 -29
- data/lib/caruso/remover.rb +73 -13
- data/lib/caruso/version.rb +1 -1
- data/package-lock.json +6 -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 +20 -3
- data/reference/plugins_reference.md +0 -376
- /data/reference/{marketplace.md → claude_code_marketplaces.md} +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Caruso
|
|
8
|
+
module Adapters
|
|
9
|
+
class HookAdapter < Base
|
|
10
|
+
# Claude Code events that map to Cursor events.
|
|
11
|
+
# Each CC event maps to a single Cursor event name.
|
|
12
|
+
# Matchers are lost in translation (Cursor has no matcher concept).
|
|
13
|
+
EVENT_MAP = {
|
|
14
|
+
"PreToolUse" => "beforeShellExecution",
|
|
15
|
+
"PostToolUse" => "afterShellExecution",
|
|
16
|
+
"UserPromptSubmit" => "beforeSubmitPrompt",
|
|
17
|
+
"Stop" => "stop"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# PostToolUse with Write|Edit matchers should map to afterFileEdit instead.
|
|
21
|
+
# We detect this via the matcher pattern.
|
|
22
|
+
FILE_EDIT_MATCHERS = /\A(Write|Edit|Write\|Edit|Edit\|Write|Notebook.*)\z/i
|
|
23
|
+
|
|
24
|
+
# Events that have no Cursor equivalent and must be skipped.
|
|
25
|
+
UNSUPPORTED_EVENTS = %w[
|
|
26
|
+
SessionStart
|
|
27
|
+
SessionEnd
|
|
28
|
+
SubagentStop
|
|
29
|
+
PreCompact
|
|
30
|
+
Notification
|
|
31
|
+
PermissionRequest
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# After adapt(), contains the translated hook commands keyed by event.
|
|
35
|
+
# Used by callers to track which hooks were installed for clean uninstall.
|
|
36
|
+
attr_reader :translated_hooks
|
|
37
|
+
|
|
38
|
+
def adapt
|
|
39
|
+
@translated_hooks = {}
|
|
40
|
+
|
|
41
|
+
hooks_file = find_hooks_file
|
|
42
|
+
return [] unless hooks_file
|
|
43
|
+
|
|
44
|
+
plugin_hooks = parse_hooks_file(hooks_file)
|
|
45
|
+
return [] if plugin_hooks.nil? || plugin_hooks.empty?
|
|
46
|
+
|
|
47
|
+
cursor_hooks = translate_hooks(plugin_hooks)
|
|
48
|
+
return [] if cursor_hooks.empty?
|
|
49
|
+
|
|
50
|
+
# Copy any referenced scripts
|
|
51
|
+
copied_scripts = copy_hook_scripts(cursor_hooks, hooks_file)
|
|
52
|
+
|
|
53
|
+
# Merge into existing .cursor/hooks.json
|
|
54
|
+
merge_hooks(cursor_hooks)
|
|
55
|
+
|
|
56
|
+
# Store for tracking
|
|
57
|
+
@translated_hooks = cursor_hooks
|
|
58
|
+
|
|
59
|
+
# Return list of created/modified files for tracking
|
|
60
|
+
created = [".cursor/hooks.json"]
|
|
61
|
+
created += copied_scripts
|
|
62
|
+
created
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def find_hooks_file
|
|
68
|
+
# files array contains the hooks.json path passed in by the Dispatcher.
|
|
69
|
+
# Also matches .caruso_inline_hooks.json written by Fetcher for inline plugin.json hooks.
|
|
70
|
+
files.find { |f| File.basename(f) =~ /hooks\.json\z/ }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_hooks_file(hooks_file)
|
|
74
|
+
content = SafeFile.read(hooks_file)
|
|
75
|
+
data = JSON.parse(content)
|
|
76
|
+
data["hooks"]
|
|
77
|
+
rescue JSON::ParserError => e
|
|
78
|
+
puts "Warning: Could not parse hooks.json: #{e.message}"
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def translate_hooks(plugin_hooks)
|
|
83
|
+
cursor_hooks = {}
|
|
84
|
+
skipped_events = []
|
|
85
|
+
skipped_prompts = 0
|
|
86
|
+
|
|
87
|
+
plugin_hooks.each do |event_name, matchers|
|
|
88
|
+
if UNSUPPORTED_EVENTS.include?(event_name)
|
|
89
|
+
skipped_events << event_name
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
result = translate_event_hooks(event_name, matchers)
|
|
94
|
+
result[:hooks].each { |event, entries| (cursor_hooks[event] ||= []).concat(entries) }
|
|
95
|
+
skipped_prompts += result[:skipped_prompts]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
warn_skipped(skipped_events, skipped_prompts)
|
|
99
|
+
cursor_hooks
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def translate_event_hooks(event_name, matchers)
|
|
103
|
+
hooks = {}
|
|
104
|
+
skipped = 0
|
|
105
|
+
|
|
106
|
+
matchers.each do |matcher_entry|
|
|
107
|
+
matcher = matcher_entry["matcher"]
|
|
108
|
+
(matcher_entry["hooks"] || []).each do |hook|
|
|
109
|
+
if hook["type"] == "prompt"
|
|
110
|
+
skipped += 1
|
|
111
|
+
next
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
command = hook["command"]
|
|
115
|
+
next unless command
|
|
116
|
+
|
|
117
|
+
cursor_event = resolve_cursor_event(event_name, matcher)
|
|
118
|
+
cursor_hook = { "command" => command }
|
|
119
|
+
cursor_hook["timeout"] = hook["timeout"] if hook["timeout"]
|
|
120
|
+
(hooks[cursor_event] ||= []) << cursor_hook
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
{ hooks: hooks, skipped_prompts: skipped }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def warn_skipped(skipped_events, skipped_prompts)
|
|
128
|
+
if skipped_events.any?
|
|
129
|
+
unique_skipped = skipped_events.uniq
|
|
130
|
+
puts "Skipping #{unique_skipped.size} unsupported hook event(s): #{unique_skipped.join(', ')}"
|
|
131
|
+
puts " (Cursor has no equivalent for these Claude Code lifecycle events)"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return unless skipped_prompts.positive?
|
|
135
|
+
|
|
136
|
+
puts "Skipping #{skipped_prompts} prompt-based hook(s): Cursor does not support LLM evaluation in hooks"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def resolve_cursor_event(cc_event, matcher)
|
|
140
|
+
# PostToolUse with file-related matchers maps to afterFileEdit
|
|
141
|
+
if cc_event == "PostToolUse" && matcher && FILE_EDIT_MATCHERS.match?(matcher)
|
|
142
|
+
return "afterFileEdit"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
EVENT_MAP[cc_event] || "afterShellExecution"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def rewrite_script_path(command)
|
|
149
|
+
plugin_script_dir = File.join("hooks", "caruso", marketplace_name, plugin_name)
|
|
150
|
+
command.gsub("${CLAUDE_PLUGIN_ROOT}", plugin_script_dir)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def plugin_root_from_hooks_file(hooks_file)
|
|
154
|
+
# ${CLAUDE_PLUGIN_ROOT} refers to the plugin root directory.
|
|
155
|
+
# hooks.json is typically at <plugin_root>/hooks/hooks.json,
|
|
156
|
+
# so the plugin root is the parent of the hooks/ directory.
|
|
157
|
+
hooks_dir = File.dirname(hooks_file)
|
|
158
|
+
if File.basename(hooks_dir) == "hooks"
|
|
159
|
+
File.dirname(hooks_dir)
|
|
160
|
+
else
|
|
161
|
+
hooks_dir
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def copy_hook_scripts(cursor_hooks, hooks_file)
|
|
166
|
+
plugin_root = plugin_root_from_hooks_file(hooks_file)
|
|
167
|
+
|
|
168
|
+
cursor_hooks.each_value.flat_map do |hook_entries|
|
|
169
|
+
hook_entries.filter_map { |hook| copy_single_script(hook, plugin_root) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def copy_single_script(hook, plugin_root)
|
|
174
|
+
command = hook["command"]
|
|
175
|
+
return unless command&.include?("${CLAUDE_PLUGIN_ROOT}")
|
|
176
|
+
|
|
177
|
+
relative_path = command.sub("${CLAUDE_PLUGIN_ROOT}/", "")
|
|
178
|
+
source_path = File.join(plugin_root, relative_path)
|
|
179
|
+
return unless File.exist?(source_path)
|
|
180
|
+
|
|
181
|
+
target_path = File.join(".cursor", "hooks", "caruso", marketplace_name, plugin_name, relative_path)
|
|
182
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
183
|
+
FileUtils.cp(source_path, target_path)
|
|
184
|
+
File.chmod(0o755, target_path)
|
|
185
|
+
puts "Copied hook script: #{target_path}"
|
|
186
|
+
|
|
187
|
+
hook["command"] = rewrite_script_path(command)
|
|
188
|
+
target_path
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def merge_hooks(new_hooks)
|
|
192
|
+
hooks_path = File.join(".cursor", "hooks.json")
|
|
193
|
+
existing = read_existing_hooks(hooks_path)
|
|
194
|
+
|
|
195
|
+
# Merge: append new hook entries to existing event arrays
|
|
196
|
+
new_hooks.each do |event, entries|
|
|
197
|
+
existing[event] ||= []
|
|
198
|
+
entries.each do |entry|
|
|
199
|
+
# Deduplicate: don't add if an identical command already exists
|
|
200
|
+
unless existing[event].any? { |e| e["command"] == entry["command"] }
|
|
201
|
+
existing[event] << entry
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
FileUtils.mkdir_p(".cursor")
|
|
207
|
+
File.write(hooks_path, JSON.pretty_generate({ "version" => 1, "hooks" => existing }))
|
|
208
|
+
puts "Merged hooks into #{hooks_path}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def read_existing_hooks(hooks_path)
|
|
212
|
+
return {} unless File.exist?(hooks_path)
|
|
213
|
+
|
|
214
|
+
data = JSON.parse(File.read(hooks_path))
|
|
215
|
+
data["hooks"] || {}
|
|
216
|
+
rescue JSON::ParserError
|
|
217
|
+
puts "Warning: Existing hooks.json is malformed, starting fresh"
|
|
218
|
+
{}
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
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,87 @@
|
|
|
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
|
+
skill_file = files.find { |f| File.basename(f).casecmp("skill.md").zero? }
|
|
10
|
+
return [] unless skill_file
|
|
11
|
+
|
|
12
|
+
skill_root = File.dirname(skill_file)
|
|
13
|
+
skill_name = File.basename(skill_root)
|
|
14
|
+
other_files = files - [skill_file]
|
|
15
|
+
|
|
16
|
+
[adapt_skill(skill_file, skill_name)] + copy_assets(other_files, skill_root, skill_name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def adapt_skill(skill_file, skill_name)
|
|
22
|
+
script_location = ".cursor/scripts/caruso/#{marketplace_name}/#{plugin_name}/#{skill_name}/"
|
|
23
|
+
content = SafeFile.read(skill_file)
|
|
24
|
+
adapted_content = inject_skill_metadata(content, skill_file, script_location)
|
|
25
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
26
|
+
save_file(skill_file, adapted_content, extension: extension)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inject_skill_metadata(content, file_path, script_location)
|
|
30
|
+
# Inject script location into description
|
|
31
|
+
hint = "Scripts located at: #{script_location}"
|
|
32
|
+
|
|
33
|
+
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
34
|
+
# Update existing frontmatter
|
|
35
|
+
content.sub!(/^description: (.*)$/, "description: \\1. #{hint}")
|
|
36
|
+
ensure_cursor_globs(content)
|
|
37
|
+
else
|
|
38
|
+
# Create new frontmatter with hint
|
|
39
|
+
create_skill_frontmatter(file_path, hint) + content
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_skill_frontmatter(file_path, hint)
|
|
44
|
+
filename = File.basename(file_path)
|
|
45
|
+
<<~YAML
|
|
46
|
+
---
|
|
47
|
+
description: Imported skill from #{filename}. #{hint}
|
|
48
|
+
globs: []
|
|
49
|
+
alwaysApply: false
|
|
50
|
+
---
|
|
51
|
+
YAML
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def copy_assets(asset_files, skill_root, skill_name)
|
|
55
|
+
asset_files.map do |file_path|
|
|
56
|
+
relative_path = file_path.sub("#{skill_root}/", "")
|
|
57
|
+
save_script(file_path, skill_name, relative_path)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def save_script(source_path, skill_name, relative_sub_path)
|
|
62
|
+
# Construct target path in .cursor/scripts
|
|
63
|
+
# .cursor/scripts/caruso/<marketplace>/<plugin>/<skill_name>/<relative_sub_path>
|
|
64
|
+
|
|
65
|
+
scripts_root = File.join(target_dir, "..", "scripts", "caruso", marketplace_name, plugin_name, skill_name)
|
|
66
|
+
# NOTE: target_dir passed to adapter is usually .cursor/rules.
|
|
67
|
+
# So .. -> .cursor -> scripts
|
|
68
|
+
|
|
69
|
+
target_path = File.join(scripts_root, relative_sub_path)
|
|
70
|
+
output_dir = File.dirname(target_path)
|
|
71
|
+
|
|
72
|
+
FileUtils.mkdir_p(output_dir)
|
|
73
|
+
FileUtils.cp(source_path, target_path)
|
|
74
|
+
|
|
75
|
+
# Make executable
|
|
76
|
+
File.chmod(0o755, target_path)
|
|
77
|
+
|
|
78
|
+
puts "Saved script: #{target_path}"
|
|
79
|
+
|
|
80
|
+
# Return relative path for tracking/reporting
|
|
81
|
+
# We start from .cursor (parent of target_dir) ideally?
|
|
82
|
+
# Or just return the absolute path for now?
|
|
83
|
+
target_path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/caruso/cli.rb
CHANGED
|
@@ -227,12 +227,21 @@ module Caruso
|
|
|
227
227
|
)
|
|
228
228
|
created_filenames = adapter.adapt
|
|
229
229
|
|
|
230
|
-
# Convert filenames to relative paths
|
|
231
|
-
|
|
230
|
+
# Convert filenames to project-relative paths.
|
|
231
|
+
# Paths already starting with .cursor/ (commands, hooks) are already project-relative.
|
|
232
|
+
# Others (rules from SkillAdapter/MarkdownAdapter) are relative to target_dir.
|
|
233
|
+
created_files = created_filenames.map do |f|
|
|
234
|
+
if f.start_with?(".cursor/") || File.absolute_path?(f)
|
|
235
|
+
f
|
|
236
|
+
else
|
|
237
|
+
File.join(config_manager.target_dir, f)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
232
240
|
|
|
233
241
|
# Use composite key for uniqueness
|
|
234
242
|
plugin_key = "#{plugin_name}@#{marketplace_name}"
|
|
235
|
-
config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name
|
|
243
|
+
config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name,
|
|
244
|
+
hooks: adapter.installed_hooks)
|
|
236
245
|
puts "Installed #{plugin_name}!"
|
|
237
246
|
end
|
|
238
247
|
|
|
@@ -455,8 +464,14 @@ module Caruso
|
|
|
455
464
|
)
|
|
456
465
|
created_filenames = adapter.adapt
|
|
457
466
|
|
|
458
|
-
# Convert filenames to relative paths
|
|
459
|
-
created_files = created_filenames.map
|
|
467
|
+
# Convert filenames to project-relative paths (same logic as install)
|
|
468
|
+
created_files = created_filenames.map do |f|
|
|
469
|
+
if f.start_with?(".cursor/") || File.absolute_path?(f)
|
|
470
|
+
f
|
|
471
|
+
else
|
|
472
|
+
File.join(config_manager.target_dir, f)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
460
475
|
|
|
461
476
|
# Cleanup: Delete files that are no longer present
|
|
462
477
|
old_files = config_manager.get_installed_files(plugin_key)
|
|
@@ -470,7 +485,8 @@ module Caruso
|
|
|
470
485
|
end
|
|
471
486
|
|
|
472
487
|
# Update plugin in config
|
|
473
|
-
config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name
|
|
488
|
+
config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name,
|
|
489
|
+
hooks: adapter.installed_hooks)
|
|
474
490
|
end
|
|
475
491
|
|
|
476
492
|
def load_config
|
|
@@ -54,11 +54,7 @@ module Caruso
|
|
|
54
54
|
raise Error, "Caruso not initialized. Run 'caruso init --ide=cursor' first."
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
local_data = load_local_config
|
|
59
|
-
|
|
60
|
-
# Merge data for easier access, but keep them conceptually separate
|
|
61
|
-
project_data.merge(local_data)
|
|
57
|
+
load_project_config.merge(load_local_config)
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
def config_exists?
|
|
@@ -77,9 +73,7 @@ module Caruso
|
|
|
77
73
|
load["ide"]
|
|
78
74
|
end
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def add_plugin(name, files, marketplace_name:)
|
|
76
|
+
def add_plugin(name, files, marketplace_name:, hooks: {})
|
|
83
77
|
# Update project config (Intent)
|
|
84
78
|
project_data = load_project_config
|
|
85
79
|
project_data["plugins"] ||= {}
|
|
@@ -88,23 +82,32 @@ module Caruso
|
|
|
88
82
|
}
|
|
89
83
|
save_project_config(project_data)
|
|
90
84
|
|
|
91
|
-
# Update local config (Files)
|
|
85
|
+
# Update local config (Files and Hooks)
|
|
92
86
|
local_data = load_local_config
|
|
93
87
|
local_data["installed_files"] ||= {}
|
|
94
88
|
local_data["installed_files"][name] = files
|
|
89
|
+
|
|
90
|
+
# Track installed hook commands for clean uninstall
|
|
91
|
+
local_data["installed_hooks"] ||= {}
|
|
92
|
+
if hooks.empty?
|
|
93
|
+
local_data["installed_hooks"].delete(name)
|
|
94
|
+
else
|
|
95
|
+
local_data["installed_hooks"][name] = hooks
|
|
96
|
+
end
|
|
97
|
+
|
|
95
98
|
save_local_config(local_data)
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
def remove_plugin(name)
|
|
99
|
-
# Get files to remove from local config
|
|
102
|
+
# Get files and hooks to remove from local config
|
|
100
103
|
local_data = load_local_config
|
|
101
104
|
files = local_data.dig("installed_files", name) || []
|
|
105
|
+
hooks = local_data.dig("installed_hooks", name) || {}
|
|
102
106
|
|
|
103
107
|
# Remove from local config
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
end
|
|
108
|
+
local_data["installed_files"]&.delete(name)
|
|
109
|
+
local_data["installed_hooks"]&.delete(name)
|
|
110
|
+
save_local_config(local_data)
|
|
108
111
|
|
|
109
112
|
# Remove from project config
|
|
110
113
|
project_data = load_project_config
|
|
@@ -113,7 +116,7 @@ module Caruso
|
|
|
113
116
|
save_project_config(project_data)
|
|
114
117
|
end
|
|
115
118
|
|
|
116
|
-
files
|
|
119
|
+
{ files: files, hooks: hooks }
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
def list_plugins
|
|
@@ -121,15 +124,16 @@ module Caruso
|
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
def plugin_installed?(name)
|
|
124
|
-
|
|
125
|
-
plugins.key?(name)
|
|
127
|
+
list_plugins.key?(name)
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def get_installed_files(name)
|
|
129
131
|
load_local_config.dig("installed_files", name) || []
|
|
130
132
|
end
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
def get_installed_hooks(name)
|
|
135
|
+
load_local_config.dig("installed_hooks", name) || {}
|
|
136
|
+
end
|
|
133
137
|
|
|
134
138
|
def add_marketplace(name, url, source: "git", ref: nil)
|
|
135
139
|
data = load_project_config
|
|
@@ -151,20 +155,23 @@ module Caruso
|
|
|
151
155
|
end
|
|
152
156
|
|
|
153
157
|
def remove_marketplace_with_plugins(marketplace_name)
|
|
154
|
-
|
|
158
|
+
result = { files: [], hooks: {} }
|
|
155
159
|
|
|
156
160
|
# Find and remove all plugins associated with this marketplace
|
|
157
161
|
installed_plugins = list_plugins
|
|
158
162
|
installed_plugins.each do |plugin_key, details|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
next unless details["marketplace"] == marketplace_name
|
|
164
|
+
|
|
165
|
+
plugin_result = remove_plugin(plugin_key)
|
|
166
|
+
result[:files].concat(plugin_result[:files])
|
|
167
|
+
result[:hooks].merge!(plugin_result[:hooks])
|
|
162
168
|
end
|
|
163
169
|
|
|
164
170
|
# Remove the marketplace itself
|
|
165
171
|
remove_marketplace(marketplace_name)
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
result[:files] = result[:files].uniq
|
|
174
|
+
result
|
|
168
175
|
end
|
|
169
176
|
|
|
170
177
|
def list_marketplaces
|
|
@@ -175,11 +182,6 @@ module Caruso
|
|
|
175
182
|
load_project_config.dig("marketplaces", name)
|
|
176
183
|
end
|
|
177
184
|
|
|
178
|
-
def get_marketplace_url(name)
|
|
179
|
-
details = get_marketplace_details(name)
|
|
180
|
-
details ? details["url"] : nil
|
|
181
|
-
end
|
|
182
|
-
|
|
183
185
|
private
|
|
184
186
|
|
|
185
187
|
def load_project_config
|