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/lib/caruso/cli.rb ADDED
@@ -0,0 +1,532 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../caruso"
5
+
6
+ module Caruso
7
+ class Marketplace < Thor
8
+ desc "add URL", "Add a marketplace"
9
+ method_option :ref, type: :string, desc: "Git branch or tag to checkout"
10
+ def add(url)
11
+ config_manager = load_config
12
+
13
+ # Determine source type
14
+ source = "git"
15
+ if url.match?(%r{\Ahttps://github\.com/[^/]+/[^/]+}) || url.match?(%r{\A[^/]+/[^/]+\z})
16
+ source = "github"
17
+ end
18
+
19
+ # Initialize fetcher and clone repository (cache dir is URL-based)
20
+ fetcher = Caruso::Fetcher.new(url, ref: options[:ref])
21
+
22
+ # For Git repos, clone/update the cache (skip in test mode to allow fake URLs)
23
+ if (source == "github" || url.match?(/\Ahttps?:/) || url.match?(%r{[^/]+/[^/]+})) && !ENV["CARUSO_TESTING_SKIP_CLONE"]
24
+ fetcher.clone_git_repo({"url" => url, "source" => source})
25
+ end
26
+
27
+ # Read marketplace name from marketplace.json
28
+ marketplace_name = fetcher.extract_marketplace_name
29
+
30
+ config_manager.add_marketplace(marketplace_name, url, source: source, ref: options[:ref])
31
+
32
+ puts "Added marketplace '#{marketplace_name}' from #{url}"
33
+ puts " Cached at: #{fetcher.cache_dir}"
34
+ puts " Ref: #{options[:ref]}" if options[:ref]
35
+ end
36
+
37
+ desc "list", "List configured marketplaces"
38
+ def list
39
+ config_manager = load_config
40
+ marketplaces = config_manager.list_marketplaces
41
+
42
+ if marketplaces.empty?
43
+ puts "No marketplaces configured."
44
+ else
45
+ puts "Configured Marketplaces:"
46
+ marketplaces.each do |name, details|
47
+ puts " - #{name}: #{details['url']} (Ref: #{details['ref'] || 'HEAD'})"
48
+ end
49
+ end
50
+ end
51
+
52
+ desc "remove NAME", "Remove a marketplace"
53
+ def remove(name)
54
+ config_manager = load_config
55
+
56
+ # Remove from config
57
+ config_manager.remove_marketplace(name)
58
+
59
+ # Remove from registry
60
+ registry = Caruso::MarketplaceRegistry.new
61
+ marketplace = registry.get_marketplace(name)
62
+ if marketplace
63
+ cache_dir = marketplace["install_location"]
64
+ registry.remove_marketplace(name)
65
+
66
+ # Inform about cache directory
67
+ if Dir.exist?(cache_dir)
68
+ puts "Cache directory still exists at: #{cache_dir}"
69
+ puts "Run 'rm -rf #{cache_dir}' to delete it if desired."
70
+ end
71
+ end
72
+
73
+ puts "Removed marketplace '#{name}'"
74
+ end
75
+
76
+ desc "info NAME", "Show marketplace information"
77
+ def info(name)
78
+ registry = Caruso::MarketplaceRegistry.new
79
+ marketplace = registry.get_marketplace(name)
80
+
81
+ unless marketplace
82
+ puts "Error: Marketplace '#{name}' not found in registry."
83
+ available = registry.list_marketplaces.keys
84
+ puts "Available marketplaces: #{available.join(', ')}" unless available.empty?
85
+ return
86
+ end
87
+
88
+ puts "Marketplace: #{name}"
89
+ puts " Source: #{marketplace['source']}" if marketplace['source']
90
+ puts " URL: #{marketplace['url']}"
91
+ puts " Location: #{marketplace['install_location']}"
92
+ puts " Last Updated: #{marketplace['last_updated']}"
93
+ puts " Ref: #{marketplace['ref']}" if marketplace['ref']
94
+
95
+ # Check if directory actually exists
96
+ if Dir.exist?(marketplace['install_location'])
97
+ puts " Status: ✓ Cached locally"
98
+ else
99
+ puts " Status: ✗ Cache directory missing"
100
+ end
101
+ end
102
+
103
+ desc "update [NAME]", "Update marketplace metadata (updates all if no name given)"
104
+ def update(name = nil)
105
+ config_manager = load_config
106
+ marketplaces = config_manager.list_marketplaces
107
+
108
+ if name
109
+ # Update specific marketplace
110
+ if marketplaces.empty?
111
+ puts "No marketplaces configured. Use 'caruso marketplace add <url>' to get started."
112
+ return
113
+ end
114
+
115
+ marketplace_details = config_manager.get_marketplace_details(name)
116
+ unless marketplace_details
117
+ puts "Error: Marketplace '#{name}' not found."
118
+ puts "Available marketplaces: #{marketplaces.keys.join(', ')}"
119
+ return
120
+ end
121
+
122
+ puts "Updating marketplace '#{name}'..."
123
+ begin
124
+ fetcher = Caruso::Fetcher.new(marketplace_details["url"], marketplace_name: name, ref: marketplace_details["ref"])
125
+ fetcher.update_cache
126
+ puts "Updated marketplace '#{name}'"
127
+ rescue StandardError => e
128
+ puts "Error updating marketplace: #{e.message}"
129
+ end
130
+ else
131
+ # Update all marketplaces
132
+ if marketplaces.empty?
133
+ puts "No marketplaces configured. Use 'caruso marketplace add <url>' to get started."
134
+ return
135
+ end
136
+
137
+ puts "Updating all marketplaces..."
138
+ success_count = 0
139
+ error_count = 0
140
+
141
+ marketplaces.each do |marketplace_name, details|
142
+ begin
143
+ puts " Updating #{marketplace_name}..."
144
+ fetcher = Caruso::Fetcher.new(details["url"], marketplace_name: marketplace_name, ref: details["ref"])
145
+ fetcher.update_cache
146
+ success_count += 1
147
+ rescue StandardError => e
148
+ puts " Error updating #{marketplace_name}: #{e.message}"
149
+ error_count += 1
150
+ end
151
+ end
152
+
153
+ puts "\nUpdated #{success_count} marketplace(s)" + (error_count.positive? ? " (#{error_count} failed)" : "")
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def load_config
160
+ manager = Caruso::ConfigManager.new
161
+ manager.load
162
+ manager
163
+ rescue Caruso::Error => e
164
+ puts "Error: #{e.message}"
165
+ exit 1
166
+ end
167
+ end
168
+
169
+ class Plugin < Thor
170
+ desc "install PLUGIN_NAME", "Install a plugin (format: plugin@marketplace or just plugin)"
171
+ def install(plugin_ref)
172
+ config_manager = load_config
173
+ target_dir = config_manager.full_target_path
174
+ ide = config_manager.ide
175
+
176
+ plugin_name, marketplace_name = plugin_ref.split("@")
177
+
178
+ marketplaces = config_manager.list_marketplaces
179
+
180
+ marketplace_url = nil
181
+
182
+ if marketplace_name
183
+ marketplace_details = config_manager.get_marketplace_details(marketplace_name)
184
+ unless marketplace_details
185
+ puts "Error: Marketplace '#{marketplace_name}' not found. Add it with 'caruso marketplace add <url>'."
186
+ puts "Available marketplaces: #{marketplaces.keys.join(', ')}" unless marketplaces.empty?
187
+ return
188
+ end
189
+ marketplace_url = marketplace_details["url"]
190
+ elsif marketplaces.empty?
191
+ # Try to find plugin in any configured marketplace
192
+ # Or default to the first one if only one exists
193
+ puts "Error: No marketplaces configured. Add one with 'caruso marketplace add <url>'."
194
+ return
195
+ elsif marketplaces.size == 1
196
+ marketplace_name = marketplaces.keys.first
197
+ marketplace_url = marketplaces.values.first["url"]
198
+ puts "Using default marketplace: #{marketplace_name}"
199
+ else
200
+ puts "Error: Multiple marketplaces configured. Please specify which one to use: plugin@marketplace"
201
+ puts "Available marketplaces: #{marketplaces.keys.join(', ')}"
202
+ return
203
+ end
204
+
205
+ puts "Installing #{plugin_name} from #{marketplace_name}..."
206
+
207
+ begin
208
+ fetcher = Caruso::Fetcher.new(marketplace_url, marketplace_name: marketplace_name)
209
+ files = fetcher.fetch(plugin_name)
210
+ rescue Caruso::PluginNotFoundError => e
211
+ puts "Error: #{e.message}"
212
+ puts "Available plugins: #{e.available_plugins.join(', ')}" unless e.available_plugins.empty?
213
+ return
214
+ end
215
+
216
+ if files.empty?
217
+ puts "No steering files found for #{plugin_name}."
218
+ return
219
+ end
220
+
221
+ adapter = Caruso::Adapter.new(
222
+ files,
223
+ target_dir: target_dir,
224
+ agent: ide.to_sym,
225
+ marketplace_name: marketplace_name,
226
+ plugin_name: plugin_name
227
+ )
228
+ created_filenames = adapter.adapt
229
+
230
+ # Convert filenames to relative paths from project root
231
+ created_files = created_filenames.map { |f| File.join(config_manager.target_dir, f) }
232
+
233
+ # Use composite key for uniqueness
234
+ plugin_key = "#{plugin_name}@#{marketplace_name}"
235
+ config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name)
236
+ puts "Installed #{plugin_name}!"
237
+ end
238
+
239
+ desc "uninstall PLUGIN_NAME", "Uninstall a plugin"
240
+ def uninstall(plugin_ref)
241
+ config_manager = load_config
242
+
243
+ # Handle both "plugin" and "plugin@marketplace" formats
244
+ # If just "plugin", we need to find the full key
245
+ plugin_key = plugin_ref
246
+ unless plugin_ref.include?("@")
247
+ installed = config_manager.list_plugins
248
+ matches = installed.keys.select { |k| k.start_with?("#{plugin_ref}@") }
249
+ if matches.size == 1
250
+ plugin_key = matches.first
251
+ elsif matches.size > 1
252
+ puts "Error: Multiple plugins match '#{plugin_ref}'. Please specify marketplace: #{matches.join(', ')}"
253
+ return
254
+ elsif !installed.key?(plugin_ref) # Check exact match just in case
255
+ puts "Plugin #{plugin_ref} is not installed."
256
+ return
257
+ end
258
+ end
259
+
260
+ unless config_manager.plugin_installed?(plugin_key)
261
+ puts "Plugin #{plugin_key} is not installed."
262
+ return
263
+ end
264
+
265
+ puts "Removing #{plugin_key}..."
266
+ files_to_remove = config_manager.remove_plugin(plugin_key)
267
+
268
+ files_to_remove.each do |file|
269
+ full_path = File.join(config_manager.project_dir, file)
270
+ if File.exist?(full_path)
271
+ File.delete(full_path)
272
+ puts " Deleted #{file}"
273
+ end
274
+ end
275
+
276
+ puts "Uninstalled #{plugin_key}."
277
+ end
278
+
279
+ desc "list", "List available and installed plugins"
280
+ def list
281
+ config_manager = load_config
282
+ marketplaces = config_manager.list_marketplaces
283
+ installed = config_manager.list_plugins
284
+
285
+ if marketplaces.empty?
286
+ puts "No marketplaces configured. Use 'caruso marketplace add <url>' to get started."
287
+ return
288
+ end
289
+
290
+ marketplaces.each do |name, details|
291
+ puts "\nMarketplace: #{name} (#{details['url']})"
292
+ begin
293
+ fetcher = Caruso::Fetcher.new(details["url"], marketplace_name: name, ref: details["ref"])
294
+ available = fetcher.list_available_plugins
295
+
296
+ available.each do |plugin|
297
+ plugin_key = "#{plugin[:name]}@#{name}"
298
+ status = installed.key?(plugin_key) ? "[Installed]" : ""
299
+ puts " - #{plugin[:name]} #{status}"
300
+ puts " #{plugin[:description]}"
301
+ end
302
+ rescue StandardError => e
303
+ puts " Error fetching marketplace: #{e.message}"
304
+ end
305
+ end
306
+ end
307
+
308
+ desc "update PLUGIN_NAME", "Update a plugin to the latest version"
309
+ method_option :all, type: :boolean, aliases: "-a", desc: "Update all installed plugins"
310
+ def update(plugin_ref = nil)
311
+ config_manager = load_config
312
+ installed_plugins = config_manager.list_plugins
313
+
314
+ if options[:all]
315
+ # Update all plugins
316
+ if installed_plugins.empty?
317
+ puts "No plugins installed."
318
+ return
319
+ end
320
+
321
+ puts "Updating all plugins..."
322
+ success_count = 0
323
+ error_count = 0
324
+
325
+ installed_plugins.each do |key, plugin_data|
326
+ begin
327
+ puts " Updating #{key}..."
328
+ update_single_plugin(key, plugin_data, config_manager)
329
+ success_count += 1
330
+ rescue StandardError => e
331
+ puts " Error updating #{key}: #{e.message}"
332
+ error_count += 1
333
+ end
334
+ end
335
+
336
+ puts "\nUpdated #{success_count} plugin(s)" + (error_count.positive? ? " (#{error_count} failed)" : "")
337
+ else
338
+ # Update single plugin
339
+ unless plugin_ref
340
+ puts "Error: Please specify a plugin name (plugin@marketplace) or use --all to update all plugins."
341
+ return
342
+ end
343
+
344
+ # Resolve key
345
+ plugin_key = plugin_ref
346
+ unless plugin_ref.include?("@")
347
+ matches = installed_plugins.keys.select { |k| k.start_with?("#{plugin_ref}@") }
348
+ if matches.size == 1
349
+ plugin_key = matches.first
350
+ elsif matches.size > 1
351
+ puts "Error: Multiple plugins match '#{plugin_ref}'. Please specify marketplace: #{matches.join(', ')}"
352
+ return
353
+ elsif !installed_plugins.key?(plugin_ref)
354
+ puts "Error: Plugin '#{plugin_ref}' is not installed."
355
+ puts "Use 'caruso plugin install #{plugin_ref}' to install it."
356
+ return
357
+ end
358
+ end
359
+
360
+ plugin_data = installed_plugins[plugin_key]
361
+ unless plugin_data
362
+ puts "Error: Plugin '#{plugin_key}' is not installed."
363
+ return
364
+ end
365
+
366
+ puts "Updating #{plugin_key}..."
367
+ begin
368
+ update_single_plugin(plugin_key, plugin_data, config_manager)
369
+ puts "Updated #{plugin_key}!"
370
+ rescue StandardError => e
371
+ puts "Error updating plugin: #{e.message}"
372
+ exit 1
373
+ end
374
+ end
375
+ end
376
+
377
+ desc "outdated", "Show plugins with available updates"
378
+ def outdated
379
+ config_manager = load_config
380
+ target_dir = config_manager.full_target_path
381
+
382
+ installed_plugins = config_manager.list_plugins
383
+
384
+ if installed_plugins.empty?
385
+ puts "No plugins installed."
386
+ return
387
+ end
388
+
389
+ puts "Checking for updates..."
390
+ outdated_plugins = []
391
+
392
+ marketplaces = config_manager.list_marketplaces
393
+
394
+ installed_plugins.each do |key, plugin_data|
395
+ marketplace_name = plugin_data["marketplace"]
396
+ next unless marketplace_name
397
+
398
+ marketplace_details = config_manager.get_marketplace_details(marketplace_name)
399
+ next unless marketplace_details
400
+
401
+ begin
402
+ fetcher = Caruso::Fetcher.new(marketplace_details["url"], marketplace_name: marketplace_name, ref: marketplace_details["ref"])
403
+ # For now, we'll just report that updates might be available
404
+ # Full version comparison would require version tracking in marketplace.json
405
+ outdated_plugins << {
406
+ name: key,
407
+ current_version: "unknown", # Version tracking not fully implemented yet
408
+ marketplace: marketplace_name
409
+ }
410
+ rescue StandardError
411
+ # Skip plugins with inaccessible marketplaces
412
+ next
413
+ end
414
+ end
415
+
416
+ if outdated_plugins.empty?
417
+ puts "All plugins are up to date."
418
+ else
419
+ puts "\nPlugins installed:"
420
+ outdated_plugins.each do |plugin|
421
+ puts " - #{plugin[:name]} (version: #{plugin[:current_version]})"
422
+ end
423
+ puts "\nRun 'caruso plugin update --all' to update all plugins."
424
+ end
425
+ end
426
+
427
+ private
428
+
429
+ def update_single_plugin(plugin_key, plugin_data, config_manager)
430
+ marketplace_name = plugin_data["marketplace"]
431
+ unless marketplace_name
432
+ raise "No marketplace information found for #{plugin_key}"
433
+ end
434
+
435
+ marketplace_details = config_manager.get_marketplace_details(marketplace_name)
436
+ unless marketplace_details
437
+ raise "Marketplace '#{marketplace_name}' not found in config"
438
+ end
439
+
440
+ # Update marketplace cache first
441
+ fetcher = Caruso::Fetcher.new(marketplace_details["url"], marketplace_name: marketplace_name, ref: marketplace_details["ref"])
442
+ fetcher.update_cache
443
+
444
+ # Parse plugin name from key (plugin@marketplace)
445
+ plugin_name = plugin_key.split("@").first
446
+
447
+ # Fetch latest plugin files
448
+ files = fetcher.fetch(plugin_name)
449
+
450
+ if files.empty?
451
+ raise "No steering files found for #{plugin_name}"
452
+ end
453
+
454
+ # Adapt files to target IDE
455
+ adapter = Caruso::Adapter.new(
456
+ files,
457
+ target_dir: config_manager.full_target_path,
458
+ agent: config_manager.ide.to_sym,
459
+ marketplace_name: marketplace_name,
460
+ plugin_name: plugin_name
461
+ )
462
+ created_filenames = adapter.adapt
463
+
464
+ # Convert filenames to relative paths from project root
465
+ created_files = created_filenames.map { |f| File.join(config_manager.target_dir, f) }
466
+
467
+ # Cleanup: Delete files that are no longer present
468
+ old_files = config_manager.get_installed_files(plugin_key)
469
+ files_to_delete = old_files - created_files
470
+ files_to_delete.each do |file|
471
+ full_path = File.join(config_manager.project_dir, file)
472
+ if File.exist?(full_path)
473
+ File.delete(full_path)
474
+ puts " Deleted obsolete file: #{file}"
475
+ end
476
+ end
477
+
478
+ # Update plugin in config
479
+ config_manager.add_plugin(plugin_key, created_files, marketplace_name: marketplace_name)
480
+ end
481
+
482
+ def load_config
483
+ manager = Caruso::ConfigManager.new
484
+ manager.load
485
+ manager
486
+ rescue Caruso::Error => e
487
+ puts "Error: #{e.message}"
488
+ exit 1
489
+ end
490
+ end
491
+
492
+ class CLI < Thor
493
+ desc "init [PATH]", "Initialize Caruso in a directory"
494
+ method_option :ide, required: true, desc: "Target IDE (currently: cursor)"
495
+ def init(path = ".")
496
+ config_manager = Caruso::ConfigManager.new(path)
497
+
498
+ begin
499
+ config = config_manager.init(ide: options[:ide])
500
+
501
+ puts "✓ Initialized Caruso for #{config['ide']}"
502
+ puts " Project directory: #{config_manager.project_dir}"
503
+ puts " Target directory: #{config['target_dir']}"
504
+ puts ""
505
+ puts "Created files:"
506
+ puts " ✓ caruso.json (commit this)"
507
+ puts " ✓ .caruso.local.json (add to .gitignore)"
508
+ puts ""
509
+ puts "Recommended .gitignore entries:"
510
+ puts " .caruso.local.json"
511
+ puts " #{config['target_dir']}/caruso/"
512
+ rescue ArgumentError => e
513
+ puts "Error: #{e.message}"
514
+ exit 1
515
+ rescue Caruso::Error => e
516
+ puts "Error: #{e.message}"
517
+ exit 1
518
+ end
519
+ end
520
+
521
+ desc "marketplace SUBCOMMAND", "Manage marketplaces"
522
+ subcommand "marketplace", Marketplace
523
+
524
+ desc "plugin SUBCOMMAND", "Manage plugins"
525
+ subcommand "plugin", Plugin
526
+
527
+ desc "version", "Print version"
528
+ def version
529
+ puts "Caruso v#{Caruso::VERSION}"
530
+ end
531
+ end
532
+ end