diff --git a/src/chutes-miner-cli/chutes_miner_cli/cli.py b/src/chutes-miner-cli/chutes_miner_cli/cli.py index 71cf9df1..7588f0d3 100644 --- a/src/chutes-miner-cli/chutes_miner_cli/cli.py +++ b/src/chutes-miner-cli/chutes_miner_cli/cli.py @@ -22,7 +22,7 @@ from chutes_miner_cli import tee_images from chutes_miner_cli import tee_maintenance from chutes_miner_cli import tee_status -from chutes_miner_cli.util import sign_request +from chutes_miner_cli.util import sign_request, sort_servers, filter_server from loguru import logger import yaml @@ -340,6 +340,12 @@ def display_remote_inventory(servers): def local_inventory( raw_json: bool = typer.Option(False, help="Display raw JSON output"), + name: Optional[str] = typer.Option( + None, + "--name", + "-n", + help="Show only the server matching this name or ID", + ), hotkey: str = typer.Option( ..., help="Path to the hotkey file for your miner", @@ -356,7 +362,7 @@ def local_inventory( """ async def _local_inventory(): - nonlocal hotkey, miner_api, raw_json + nonlocal hotkey, miner_api, raw_json, name async with aiohttp.ClientSession(raise_for_status=True) as session: headers, _ = sign_request(hotkey, purpose="management") async with session.get( @@ -365,6 +371,12 @@ async def _local_inventory(): timeout=30, ) as resp: inventory = await resp.json() + inventory = sort_servers(filter_server(inventory, name)) + if name and not inventory: + typer.echo( + f"No server matching '{name}' found in local inventory.", err=True + ) + raise typer.Exit(1) if raw_json: print(json.dumps(inventory, indent=2)) else: @@ -375,6 +387,12 @@ async def _local_inventory(): def remote_inventory( raw_json: bool = typer.Option(False, help="Display raw JSON output"), + name: Optional[str] = typer.Option( + None, + "--name", + "-n", + help="Show only the server matching this name or ID", + ), hotkey: str = typer.Option( ..., help="Path to the hotkey file for your miner", @@ -391,7 +409,7 @@ def remote_inventory( """ async def _remote_inventory(): - nonlocal hotkey, validator_api, raw_json + nonlocal hotkey, validator_api, raw_json, name async with aiohttp.ClientSession(raise_for_status=True) as session: headers, _ = sign_request(hotkey, purpose="miner", remote=True) async with session.get( @@ -420,6 +438,12 @@ async def _remote_inventory(): "inst_verified_at": item["last_verified_at"], } ) + servers = sort_servers(filter_server(servers, name)) + if name and not servers: + typer.echo( + f"No server matching '{name}' found in remote inventory.", err=True + ) + raise typer.Exit(1) if raw_json: print(json.dumps({"servers": servers}, indent=2)) else: diff --git a/src/chutes-miner-cli/chutes_miner_cli/tee_maintenance.py b/src/chutes-miner-cli/chutes_miner_cli/tee_maintenance.py index c9462d07..14f643fb 100644 --- a/src/chutes-miner-cli/chutes_miner_cli/tee_maintenance.py +++ b/src/chutes-miner-cli/chutes_miner_cli/tee_maintenance.py @@ -7,7 +7,7 @@ import asyncio import json -from typing import Any +from typing import Any, Optional import aiohttp import typer @@ -16,7 +16,7 @@ from rich import box from chutes_miner_cli.constants import HOTKEY_ENVVAR, MINER_API_ENVVAR, VALIDATOR_API_ENVVAR -from chutes_miner_cli.util import sign_request +from chutes_miner_cli.util import sign_request, sort_servers, filter_server console = Console() @@ -141,6 +141,12 @@ def maintenance_status( raw_json: bool = typer.Option( False, "--raw-json", help="Output raw JSON for programmatic use" ), + name: Optional[str] = typer.Option( + None, + "--name", + "-n", + help="Show only the server matching this name or ID", + ), hotkey: str = typer.Option( ..., help="Path to the hotkey file for your miner", envvar=HOTKEY_ENVVAR ), @@ -161,6 +167,13 @@ async def _run(): raise typer.Exit(1) data = await resp.json() + data["servers"] = sort_servers(filter_server(data.get("servers"), name)) + if name and not data["servers"]: + typer.echo( + f"No server matching '{name}' found in maintenance status.", err=True + ) + raise typer.Exit(1) + if raw_json: print(json.dumps(data, indent=2)) else: diff --git a/src/chutes-miner-cli/chutes_miner_cli/util.py b/src/chutes-miner-cli/chutes_miner_cli/util.py index e9125cc1..cba12075 100644 --- a/src/chutes-miner-cli/chutes_miner_cli/util.py +++ b/src/chutes-miner-cli/chutes_miner_cli/util.py @@ -36,6 +36,33 @@ def get_signing_message( raise ValueError("Either payload_str or purpose must be provided") +def sort_servers(servers: list | None) -> list: + """ + Sort a list of server dicts consistently by name (case-insensitive), + falling back to server_id as a tiebreaker. Used across local-inventory, + remote-inventory, and tee maintenance-status so ordering is stable. + """ + return sorted( + servers or [], + key=lambda s: ((s.get("name") or "").lower(), s.get("server_id") or ""), + ) + + +def filter_server(servers: list | None, name_or_id: str | None) -> list: + """ + Filter server dicts down to those matching name_or_id exactly, by either + name or server_id (same semantics as the miner API lock/unlock/delete + routes). Returns the list unchanged when name_or_id is falsy. + """ + if not name_or_id: + return servers or [] + return [ + s + for s in (servers or []) + if s.get("name") == name_or_id or s.get("server_id") == name_or_id + ] + + def sign_request( hotkey: str, payload: Dict[str, Any] | str | None = None,