diff --git a/lib/classifier/cli.rb b/lib/classifier/cli.rb index 6c673942..400ac36d 100644 --- a/lib/classifier/cli.rb +++ b/lib/classifier/cli.rb @@ -370,8 +370,9 @@ 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 @@ -381,6 +382,28 @@ def list_remote_models return end + if model + name, info = index['models'].find { |name, _| name == model } + if name.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: name, desc: desc.slice(0, 40), type: type, categories: categories, + version: version, author: author, size: size + ) + return + end + index['models'].each do |name, info| type = info['type'] || 'unknown' size = info['size'] || 'unknown' @@ -390,6 +413,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) @@ -418,6 +443,27 @@ def list_local_models return 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] + info = load_model_info(model[:path]) + type = info['type'] || 'unknown' + version = info['version'] + categories = (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 + models.each do |model| info = load_model_info(model[:path]) type = info['type'] || 'unknown' @@ -800,6 +846,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(':') 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 613d8c3d..dc8c8f28 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 + # # Pull Command #