diff --git a/lib/classifier/cli.rb b/lib/classifier/cli.rb index 9962813..d394ffb 100644 --- a/lib/classifier/cli.rb +++ b/lib/classifier/cli.rb @@ -377,14 +377,37 @@ def command_models end def list_remote_models - registry_arg = @args.shift - registry = parse_registry(registry_arg) || DEFAULT_REGISTRY + registry_or_model_arg = @args.shift + registry, model = detect_registry_and_model(registry_or_model_arg) + registry = parse_registry(registry) || DEFAULT_REGISTRY index = fetch_registry_index(registry) return if @exit_code != 0 models = index['models'] + if model + info = models[model] + if info.nil? + @output << "No model #{model.inspect} found in registry" + return + end + + type = info['type'] || 'unknown' + size = info['size'] || 'unknown' + desc = info['description'] || '' + categories = (info['categories'] || []).map(&:downcase).join(', ') + version = info['version'] || '' + author = info['author'] || '' + @output << format( + "Name: %s\nDescription: %s\nType: %s\n" \ + "Categories: %s\nVersion: %s\nAuthor: %s\nSize: %s", + name: model, desc: desc, type: type, categories: categories, + version: version, author: author, size: size + ) + return + end + if @options[:search] models = models.filter do |name, info| [name, info['description']].any?(/#{Regexp.escape(@options[:search])}/i) @@ -405,6 +428,8 @@ def list_remote_models end def list_local_models + model_arg = @args.shift + models_dir = File.join(CACHE_DIR, 'models') unless Dir.exist?(models_dir) @@ -432,6 +457,26 @@ def list_local_models model[:info] = load_model_info(model[:path]) end + if model_arg + model = models.find { |model| model[:name] == model_arg } + if model.nil? + @output << "No local model #{model_arg.inspect} found" + return + end + display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name] + type = model.dig(:info, 'type') || 'unknown' + version = model.dig(:info, 'version') + categories = (model.dig(:info, 'categories') || {}).keys.map(&:downcase).join(', ') + size = File.size(model[:path]) + @output << format( + "Name: %s\nType: %s\n" \ + "Categories: %s\nVersion: %s\nSize: %s", + name: display_name, type: type, categories: categories, + version: version, size: human_size(size) + ) + return + end + if @options[:search] models = models.filter do |model| [model[:name], model.dig(:info, 'description')].any?(/#{Regexp.escape(@options[:search])}/i) @@ -824,6 +869,15 @@ def show_getting_started @output << 'Run "classifier --help" for full usage.' end + # @rbs (String?) -> [String?, String?] + def detect_registry_and_model(arg) + return nil, nil if arg.nil? + return *arg.split(':', 2) if arg.include?(':') + return nil, arg unless arg.start_with?('@') + + [arg, nil] + end + # Parse @user/repo format to extract registry # @rbs (String?) -> String? def parse_registry(arg) diff --git a/test/cli/registry_commands_test.rb b/test/cli/registry_commands_test.rb index a658217..c0a5b4b 100644 --- a/test/cli/registry_commands_test.rb +++ b/test/cli/registry_commands_test.rb @@ -56,6 +56,14 @@ def run_cli(*args, stdin: nil) cli.run end + def create_local_model_fixtures + # Create some cached models + models_dir = File.join(@cache_dir, 'models') + FileUtils.mkdir_p(models_dir) + File.write(File.join(models_dir, 'spam-filter.json'), @model_json) + File.write(File.join(models_dir, 'sentiment.json'), @model_json) + end + # # Models Command # @@ -106,12 +114,45 @@ def test_models_handles_network_error assert_match(/failed to fetch/i, result[:error]) end + def test_models_model_detail_view + stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json') + .to_return(status: 200, body: @models_json) + + result = run_cli('models', 'sentiment') + + assert_equal 0, result[:exit_code] + assert_match('Name: sentiment', result[:output]) + assert_match('Description: Sentiment analysis', result[:output]) + assert_match('Type: bayes', result[:output]) + assert_empty result[:error] + end + + def test_models_model_detail_view_if_not_found + stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json') + .to_return(status: 200, body: @models_json) + + result = run_cli('models', 'no-model') + + assert_equal 0, result[:exit_code] + assert_match('No model "no-model" found in registry', result[:output]) + assert_empty result[:error] + end + + def test_models_model_detail_view_with_custom_registry + stub_request(:get, 'https://raw.githubusercontent.com/someone/models/main/models.json') + .to_return(status: 200, body: @models_json) + + result = run_cli('models', '@someone/models:spam-filter') + + assert_equal 0, result[:exit_code] + assert_match('Name: spam-filter', result[:output]) + assert_match('Description: Email spam detection', result[:output]) + assert_match('Type: bayes', result[:output]) + assert_empty result[:error] + end + def test_models_local_lists_cached_models - # Create some cached models - models_dir = File.join(@cache_dir, 'models') - FileUtils.mkdir_p(models_dir) - File.write(File.join(models_dir, 'spam-filter.json'), @model_json) - File.write(File.join(models_dir, 'sentiment.json'), @model_json) + create_local_model_fixtures result = run_cli('models', '--local') @@ -151,6 +192,28 @@ def test_models_local_shows_no_models_when_cache_dir_missing assert_match(/no local models found/i, result[:output]) end + def test_models_local_model_detail_view + create_local_model_fixtures + + result = run_cli('models', '--local', 'spam-filter') + + assert_equal 0, result[:exit_code] + assert_match('Name: spam-filter', result[:output]) + assert_match('Type: bayes', result[:output]) + assert_match('Categories: spam, ham', result[:output]) + assert_empty result[:error] + end + + def test_models_local_model_detail_view_if_not_found + create_local_model_fixtures + + result = run_cli('models', '--local', 'no-model') + + assert_equal 0, result[:exit_code] + assert_match('No local model "no-model" found', result[:output]) + assert_empty result[:error] + end + def test_models_remote_search_by_name stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json') .to_return(status: 200, body: @models_json) @@ -184,11 +247,7 @@ def test_models_remote_search_no_found end def test_models_local_search_by_name - # Create some cached models - models_dir = File.join(@cache_dir, 'models') - FileUtils.mkdir_p(models_dir) - File.write(File.join(models_dir, 'spam-filter.json'), @model_json) - File.write(File.join(models_dir, 'sentiment.json'), @model_json) + create_local_model_fixtures result = run_cli('models', '--local', '--search', 'spam-filter')