fix: show built-in distributions in llama stack list

This fixes issue #3922 where `llama stack list` only showed distributions
after they were run. Now the command shows both:
- Built-in distributions from the source code (e.g., starter, nvidia, dell)
- Built distributions from ~/.llama/distributions

Changes:
- Updated _get_distribution_dirs() to discover built-in distributions
- Added a "Source" column to distinguish "built-in" vs "built" distributions
- Built distributions override built-in ones with the same name
- Added comprehensive unit tests to verify the fix

Closes #3922

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Roy Belio 2025-11-03 12:23:07 +02:00
parent d45137a399
commit 650d2fd9e3
2 changed files with 267 additions and 17 deletions

View file

@ -9,48 +9,69 @@ from pathlib import Path
from llama_stack.cli.subcommand import Subcommand
from llama_stack.cli.table import print_table
from llama_stack.core.utils.config_dirs import DISTRIBS_BASE_DIR
class StackListBuilds(Subcommand):
"""List built stacks in .llama/distributions directory"""
"""List available distributions (both built-in and built)"""
def __init__(self, subparsers: argparse._SubParsersAction):
super().__init__()
self.parser = subparsers.add_parser(
"list",
prog="llama stack list",
description="list the build stacks",
description="list available distributions",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
self._add_arguments()
self.parser.set_defaults(func=self._list_stack_command)
def _get_distribution_dirs(self) -> dict[str, Path]:
"""Return a dictionary of distribution names and their paths"""
distributions = {}
dist_dir = Path.home() / ".llama" / "distributions"
def _get_distribution_dirs(self) -> dict[str, tuple[Path, str]]:
"""Return a dictionary of distribution names and their paths with source type
Returns:
dict mapping distro name to (path, source_type) where source_type is 'built-in' or 'built'
"""
distributions = {}
# Get built-in distributions from source code
distro_dir = Path(__file__).parent.parent.parent / "distributions"
if distro_dir.exists():
for stack_dir in distro_dir.iterdir():
if stack_dir.is_dir() and not stack_dir.name.startswith(".") and not stack_dir.name.startswith("__"):
distributions[stack_dir.name] = (stack_dir, "built-in")
# Get built/run distributions from ~/.llama/distributions
# These override built-in ones if they have the same name
if DISTRIBS_BASE_DIR.exists():
for stack_dir in DISTRIBS_BASE_DIR.iterdir():
if stack_dir.is_dir() and not stack_dir.name.startswith("."):
# Clean up the name (remove llamastack- prefix if present)
name = stack_dir.name.replace("llamastack-", "")
distributions[name] = (stack_dir, "built")
if dist_dir.exists():
for stack_dir in dist_dir.iterdir():
if stack_dir.is_dir():
distributions[stack_dir.name] = stack_dir
return distributions
def _list_stack_command(self, args: argparse.Namespace) -> None:
distributions = self._get_distribution_dirs()
if not distributions:
print("No stacks found in ~/.llama/distributions")
print("No distributions found")
return
headers = ["Stack Name", "Path"]
headers.extend(["Build Config", "Run Config"])
headers = ["Stack Name", "Source", "Path", "Build Config", "Run Config"]
rows = []
for name, path in distributions.items():
row = [name, str(path)]
for name, (path, source_type) in sorted(distributions.items()):
row = [name, source_type, str(path)]
# Check for build and run config files
build_config = "Yes" if (path / f"{name}-build.yaml").exists() else "No"
run_config = "Yes" if (path / f"{name}-run.yaml").exists() else "No"
# For built-in distributions, configs are named build.yaml and run.yaml
# For built distributions, configs are named {name}-build.yaml and {name}-run.yaml
if source_type == "built-in":
build_config = "Yes" if (path / "build.yaml").exists() else "No"
run_config = "Yes" if (path / "run.yaml").exists() else "No"
else:
build_config = "Yes" if (path / f"{name}-build.yaml").exists() else "No"
run_config = "Yes" if (path / f"{name}-run.yaml").exists() else "No"
row.extend([build_config, run_config])
rows.append(row)
print_table(rows, headers, separate_rows=True)

View file

@ -0,0 +1,229 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
"""
Tests for the llama stack list command.
These tests verify the fix for issue #3922 where `llama stack list` only showed
distributions after they were run.
"""
import argparse
from unittest.mock import MagicMock, patch
import pytest
from llama_stack.cli.stack.list_stacks import StackListBuilds
@pytest.fixture
def list_stacks_command():
"""Create a StackListBuilds instance for testing."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
return StackListBuilds(subparsers)
@pytest.fixture
def mock_distribs_base_dir(tmp_path):
"""Create a mock DISTRIBS_BASE_DIR with some built distributions."""
built_dir = tmp_path / "distributions"
built_dir.mkdir(parents=True, exist_ok=True)
# Create a built distribution
starter_built = built_dir / "starter"
starter_built.mkdir()
(starter_built / "starter-build.yaml").write_text("# build config")
(starter_built / "starter-run.yaml").write_text("# run config")
return built_dir
@pytest.fixture
def mock_distro_dir(tmp_path):
"""Create a mock distributions directory with built-in distributions."""
distro_dir = tmp_path / "src" / "llama_stack" / "distributions"
distro_dir.mkdir(parents=True, exist_ok=True)
# Create some built-in distributions
for distro_name in ["starter", "nvidia", "dell"]:
distro_path = distro_dir / distro_name
distro_path.mkdir()
(distro_path / "build.yaml").write_text("# build config")
(distro_path / "run.yaml").write_text("# run config")
return distro_dir
def create_path_mock(builtin_dist_dir):
"""Create a properly mocked Path object that returns builtin_dist_dir for the distributions path."""
mock_parent_parent_parent = MagicMock()
mock_parent_parent_parent.__truediv__ = lambda self, other: builtin_dist_dir if other == "distributions" else MagicMock()
mock_path = MagicMock()
mock_path.parent.parent.parent = mock_parent_parent_parent
return mock_path
class TestStackList:
"""Test suite for llama stack list command."""
def test_builtin_distros_shown_without_running(self, list_stacks_command, mock_distro_dir, tmp_path):
"""Test that built-in distributions are shown even before running them.
This verifies the fix for issue #3922 where `llama stack list` only showed
distributions after they were run.
"""
mock_path = create_path_mock(mock_distro_dir)
# Mock DISTRIBS_BASE_DIR to be a non-existent directory (no built distributions)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", tmp_path / "nonexistent"):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
# Verify built-in distributions are found
assert len(distributions) > 0, "Should find built-in distributions"
assert all(source_type == "built-in" for _, source_type in distributions.values()), "All should be built-in"
# Check specific distributions we created
assert "starter" in distributions
assert "nvidia" in distributions
assert "dell" in distributions
def test_builtin_and_built_distros_shown_together(self, list_stacks_command, mock_distro_dir, mock_distribs_base_dir):
"""Test that both built-in and built distributions are shown together."""
mock_path = create_path_mock(mock_distro_dir)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", mock_distribs_base_dir):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
# Should have built-in distributions
builtin_count = sum(1 for _, source_type in distributions.values() if source_type == "built-in")
# Should have built distributions
built_count = sum(1 for _, source_type in distributions.values() if source_type == "built")
assert builtin_count > 0, "Should have built-in distributions"
assert built_count > 0, "Should have built distributions"
def test_built_distribution_overrides_builtin(self, list_stacks_command, mock_distro_dir, mock_distribs_base_dir):
"""Test that built distributions override built-in ones with the same name."""
mock_path = create_path_mock(mock_distro_dir)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", mock_distribs_base_dir):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
# "starter" should exist and be marked as "built" (not "built-in")
# because the built version overrides the built-in one
assert "starter" in distributions
_, source_type = distributions["starter"]
assert source_type == "built", "Built distribution should override built-in"
def test_empty_distributions(self, list_stacks_command, tmp_path):
"""Test behavior when no distributions exist."""
nonexistent = tmp_path / "nonexistent"
mock_path = create_path_mock(nonexistent)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", nonexistent):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
assert len(distributions) == 0, "Should return empty dict when no distributions exist"
def test_config_files_detection_builtin(self, list_stacks_command, mock_distro_dir, tmp_path):
"""Test that config files are correctly detected for built-in distributions."""
mock_path = create_path_mock(mock_distro_dir)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", tmp_path / "nonexistent"):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
# Check that starter has both config files
if "starter" in distributions:
path, source_type = distributions["starter"]
if source_type == "built-in":
assert (path / "build.yaml").exists()
assert (path / "run.yaml").exists()
def test_config_files_detection_built(self, list_stacks_command, tmp_path):
"""Test that config files are correctly detected for built distributions."""
# Create a built distribution
built_dir = tmp_path / "distributions"
built_dir.mkdir(parents=True)
test_distro = built_dir / "test-distro"
test_distro.mkdir()
(test_distro / "test-distro-build.yaml").write_text("# build")
(test_distro / "test-distro-run.yaml").write_text("# run")
nonexistent = tmp_path / "nonexistent"
mock_path = create_path_mock(nonexistent)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", built_dir):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
assert "test-distro" in distributions
path, source_type = distributions["test-distro"]
assert source_type == "built"
assert (path / "test-distro-build.yaml").exists()
assert (path / "test-distro-run.yaml").exists()
def test_llamastack_prefix_stripped(self, list_stacks_command, tmp_path):
"""Test that llamastack- prefix is stripped from built distribution names."""
# Create a built distribution with llamastack- prefix
built_dir = tmp_path / "distributions"
built_dir.mkdir(parents=True)
distro_with_prefix = built_dir / "llamastack-mystack"
distro_with_prefix.mkdir()
nonexistent = tmp_path / "nonexistent"
mock_path = create_path_mock(nonexistent)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", built_dir):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
# Should be listed as "mystack" not "llamastack-mystack"
assert "mystack" in distributions
assert "llamastack-mystack" not in distributions
def test_hidden_directories_ignored(self, list_stacks_command, mock_distro_dir, tmp_path):
"""Test that hidden directories (starting with .) are ignored."""
# Add a hidden directory
hidden_dir = mock_distro_dir / ".hidden"
hidden_dir.mkdir()
(hidden_dir / "build.yaml").write_text("# build")
# Add a __pycache__ directory
pycache_dir = mock_distro_dir / "__pycache__"
pycache_dir.mkdir()
mock_path = create_path_mock(mock_distro_dir)
with patch("llama_stack.cli.stack.list_stacks.DISTRIBS_BASE_DIR", tmp_path / "nonexistent"):
with patch("llama_stack.cli.stack.list_stacks.Path") as mock_path_class:
mock_path_class.return_value = mock_path
distributions = list_stacks_command._get_distribution_dirs()
assert ".hidden" not in distributions
assert "__pycache__" not in distributions