From e46e5f62be2ae1de00d1ca4eb5b6687982473234 Mon Sep 17 00:00:00 2001 From: Artem Yegorov Date: Thu, 18 Jun 2026 15:16:27 +0300 Subject: [PATCH 1/3] feat: add model detail view (#129) --- lib/classifier/cli.rb | 68 ++++++++++++++++++++++++++--- test/cli/registry_commands_test.rb | 69 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/lib/classifier/cli.rb b/lib/classifier/cli.rb index 6c673942..dd428ce7 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,15 +382,38 @@ def list_remote_models return end - index['models'].each do |name, info| + 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'] || '' - @output << format('%-20s %s (%s, %s)', name: name, desc: desc.slice(0, 40), type: type, size: size) + 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 + ) + else + index['models'].each do |name, info| + type = info['type'] || 'unknown' + size = info['size'] || 'unknown' + desc = info['description'] || '' + @output << format('%-20s %s (%s, %s)', name: name, desc: desc.slice(0, 40), type: type, size: size) + end end end def list_local_models + model_arg = @args.shift + models_dir = File.join(CACHE_DIR, 'models') unless Dir.exist?(models_dir) @@ -418,12 +442,32 @@ def list_local_models return end - models.each do |model| + 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' - display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name] + version = info['version'] + categories = info['categories'].keys.map(&:downcase).join(', ') size = File.size(model[:path]) - @output << format('%-30s (%s, %s)', name: display_name, type: type, size: human_size(size)) + @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) + ) + else + models.each do |model| + info = load_model_info(model[:path]) + type = info['type'] || 'unknown' + display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name] + size = File.size(model[:path]) + @output << format('%-30s (%s, %s)', name: display_name, type: type, size: human_size(size)) + end end end @@ -800,6 +844,16 @@ def show_getting_started @output << 'Run "classifier --help" for full usage.' end + # Detect registry and model + # @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..fdf37d44 100644 --- a/test/cli/registry_commands_test.rb +++ b/test/cli/registry_commands_test.rb @@ -106,6 +106,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_match('Categories: positive, negative, neutral', 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_match('Categories: spam, ham', 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') @@ -151,6 +190,36 @@ 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 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) + + 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 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) + + 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 # From 8679ed392a84f634c530a0bcfe2ea32e5c60dbcf Mon Sep 17 00:00:00 2001 From: Artem Yegorov Date: Thu, 18 Jun 2026 15:26:37 +0300 Subject: [PATCH 2/3] fix: greptile comments (#129) --- lib/classifier/cli.rb | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/classifier/cli.rb b/lib/classifier/cli.rb index dd428ce7..400ac36d 100644 --- a/lib/classifier/cli.rb +++ b/lib/classifier/cli.rb @@ -392,7 +392,7 @@ def list_remote_models type = info['type'] || 'unknown' size = info['size'] || 'unknown' desc = info['description'] || '' - categories = info['categories'].map(&:downcase).join(', ') || '' + categories = (info['categories'] || []).map(&:downcase).join(', ') version = info['version'] || '' author = info['author'] || '' @output << format( @@ -401,13 +401,14 @@ def list_remote_models name: name, desc: desc.slice(0, 40), type: type, categories: categories, version: version, author: author, size: size ) - else - index['models'].each do |name, info| - type = info['type'] || 'unknown' - size = info['size'] || 'unknown' - desc = info['description'] || '' - @output << format('%-20s %s (%s, %s)', name: name, desc: desc.slice(0, 40), type: type, size: size) - end + return + end + + index['models'].each do |name, info| + type = info['type'] || 'unknown' + size = info['size'] || 'unknown' + desc = info['description'] || '' + @output << format('%-20s %s (%s, %s)', name: name, desc: desc.slice(0, 40), type: type, size: size) end end @@ -452,7 +453,7 @@ def list_local_models info = load_model_info(model[:path]) type = info['type'] || 'unknown' version = info['version'] - categories = info['categories'].keys.map(&:downcase).join(', ') + categories = (info['categories'] || {}).keys.map(&:downcase).join(', ') size = File.size(model[:path]) @output << format( "Name: %s\nType: %s\n" \ @@ -460,14 +461,15 @@ def list_local_models name: display_name, type: type, categories: categories, version: version, size: human_size(size) ) - else - models.each do |model| - info = load_model_info(model[:path]) - type = info['type'] || 'unknown' - display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name] - size = File.size(model[:path]) - @output << format('%-30s (%s, %s)', name: display_name, type: type, size: human_size(size)) - end + return + end + + models.each do |model| + info = load_model_info(model[:path]) + type = info['type'] || 'unknown' + display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name] + size = File.size(model[:path]) + @output << format('%-30s (%s, %s)', name: display_name, type: type, size: human_size(size)) end end @@ -844,7 +846,6 @@ def show_getting_started @output << 'Run "classifier --help" for full usage.' end - # Detect registry and model # @rbs (String?) -> [String?, String?] def detect_registry_and_model(arg) return nil, nil if arg.nil? From edc78a344c421bbcdd5e2c070ca6c03b1c1f38d8 Mon Sep 17 00:00:00 2001 From: Artem Yegorov Date: Thu, 18 Jun 2026 15:43:32 +0300 Subject: [PATCH 3/3] fix: houndci-bot comments (#129) --- test/cli/registry_commands_test.rb | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/test/cli/registry_commands_test.rb b/test/cli/registry_commands_test.rb index fdf37d44..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 # @@ -116,7 +124,6 @@ def test_models_model_detail_view assert_match('Name: sentiment', result[:output]) assert_match('Description: Sentiment analysis', result[:output]) assert_match('Type: bayes', result[:output]) - assert_match('Categories: positive, negative, neutral', result[:output]) assert_empty result[:error] end @@ -141,16 +148,11 @@ def test_models_model_detail_view_with_custom_registry assert_match('Name: spam-filter', result[:output]) assert_match('Description: Email spam detection', result[:output]) assert_match('Type: bayes', result[:output]) - assert_match('Categories: spam, ham', 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') @@ -191,11 +193,7 @@ def test_models_local_shows_no_models_when_cache_dir_missing end def test_models_local_model_detail_view - # 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', 'spam-filter') @@ -207,11 +205,7 @@ def test_models_local_model_detail_view end def test_models_local_model_detail_view_if_not_found - # 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', 'no-model')