#!/usr/bin/env python3 """ Major Domo CLI - SBA League Management A command-line interface for the Major Domo API, primarily for use with Claude Code. Modular architecture: each command group is a separate cli_*.py file. Usage: majordomo status majordomo player get "Mike Trout" majordomo player search "trout" majordomo player move "Mike Trout" CAR majordomo player move --batch "Name1:Team1,Name2:Team2" majordomo team list majordomo team roster CAR majordomo standings --division ALE majordomo transactions --team CLS majordomo injuries --active majordomo stats batting --sort woba --min-pa 100 Environment: API_TOKEN: Required. Bearer token for API authentication. """ import os import sys from typing import Annotated, Optional import typer from rich.panel import Panel # Ensure skill directory is on path for imports sys.path.insert(0, os.path.dirname(__file__)) from api_client import MajorDomoAPI from cli_common import ( console, state, output_json, output_table, handle_error, get_season, ) # ============================================================================ # App Setup # ============================================================================ app = typer.Typer( name="majordomo", help="Major Domo SBA League Management CLI", no_args_is_help=True, ) player_app = typer.Typer(help="Player operations") team_app = typer.Typer(help="Team operations") app.add_typer(player_app, name="player") app.add_typer(team_app, name="team") # Import and mount sub-app modules from cli_transactions import transactions_app from cli_injuries import injuries_app from cli_stats import stats_app from cli_results import results_app from cli_schedule import schedule_app from cli_admin import admin_app app.add_typer(transactions_app, name="transactions") app.add_typer(injuries_app, name="injuries") app.add_typer(stats_app, name="stats") app.add_typer(results_app, name="results") app.add_typer(schedule_app, name="schedule") app.add_typer(admin_app, name="admin") # ============================================================================ # Main Callback (Global Options) # ============================================================================ @app.callback() def main( env: Annotated[ str, typer.Option("--env", help="Environment: prod or dev") ] = "prod", json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False, verbose: Annotated[ bool, typer.Option("--verbose", "-v", help="Verbose output") ] = False, ): """Major Domo SBA League Management CLI""" try: state.api = MajorDomoAPI(environment=env, verbose=verbose) state.json_output = json_output # Cache current season current = state.api.get_current() state.current_season = current["season"] except ValueError as e: console.print(f"[red]Configuration Error:[/red] {e}") console.print("\nSet API_TOKEN environment variable:") console.print(" export API_TOKEN='your-token-here'") raise typer.Exit(1) except Exception as e: handle_error(e) # ============================================================================ # Status & Health Commands # ============================================================================ @app.command() def status(): """Show current season/week status""" try: current = state.api.get_current() if state.json_output: output_json(current) return panel = Panel( f"[bold]Season:[/bold] {current['season']}\n" f"[bold]Week:[/bold] {current['week']}\n" f"[bold]Frozen:[/bold] {'Yes' if current.get('freeze') else 'No'}\n" f"[bold]Trade Deadline:[/bold] Week {current.get('trade_deadline', 'N/A')}\n" f"[bold]Playoffs Begin:[/bold] Week {current.get('playoffs_begin', 'N/A')}", title="SBA League Status", border_style="green", ) console.print(panel) except Exception as e: handle_error(e) @app.command() def health(): """Check API health status""" try: healthy = state.api.health_check() if healthy: console.print(f"[green]API is healthy[/green] ({state.api.base_url})") else: console.print("[red]API is not responding[/red]") raise typer.Exit(1) except Exception as e: handle_error(e) # ============================================================================ # Player Commands # ============================================================================ @player_app.command("get") def player_get( name: Annotated[str, typer.Argument(help="Player name")], season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, ): """Get player information by name""" try: season = get_season(season) player = state.api.get_player(name=name, season=season) if not player: console.print( f"[yellow]Player '{name}' not found in season {season}[/yellow]" ) console.print('Try: majordomo player search "partial name"') raise typer.Exit(1) if state.json_output: output_json(player) return # Extract nested team info team = player.get("team", {}) team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A" # Collect positions positions = [ player.get(f"pos_{i}") for i in range(1, 9) if player.get(f"pos_{i}") ] panel = Panel( f"[bold]ID:[/bold] {player['id']}\n" f"[bold]Name:[/bold] {player['name']}\n" f"[bold]Team:[/bold] {team_abbrev}\n" f"[bold]Position(s):[/bold] {', '.join(positions) if positions else 'N/A'}\n" f"[bold]WARA:[/bold] {player.get('wara', 0):.2f}\n" f"[bold]Strat Code:[/bold] {player.get('strat_code', 'N/A')}\n" f"[bold]IL Return:[/bold] {player.get('il_return') or 'Healthy'}", title=f"Player: {player['name']}", border_style="blue", ) console.print(panel) except typer.Exit: raise except Exception as e: handle_error(e, f"Player '{name}' may not exist.") @player_app.command("search") def player_search( query: Annotated[str, typer.Argument(help="Search query (partial name)")], season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, limit: Annotated[int, typer.Option("--limit", "-n", help="Max results")] = 10, ): """Search players by name (fuzzy match)""" try: season = get_season(season) players = state.api.search_players(query=query, season=season, limit=limit) if state.json_output: output_json(players) return if not players: console.print( f"[yellow]No players found matching '{query}' in season {season}[/yellow]" ) return rows = [] for p in players: team = p.get("team", {}) team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A" rows.append( [ p["id"], p["name"], team_abbrev, p.get("pos_1", ""), f"{p.get('wara', 0):.2f}", ] ) output_table( f"Search Results: '{query}' (Season {season})", ["ID", "Name", "Team", "Pos", "WARA"], rows, ) except Exception as e: handle_error(e) @player_app.command("move") def player_move( name: Annotated[Optional[str], typer.Argument(help="Player name")] = None, team: Annotated[ Optional[str], typer.Argument(help="Target team abbreviation") ] = None, batch: Annotated[ Optional[str], typer.Option("--batch", "-b", help="Batch moves: 'Name1:Team1,Name2:Team2'"), ] = None, season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, dry_run: Annotated[ bool, typer.Option("--dry-run", help="Show what would be done") ] = False, ): """Move player(s) to a new team""" try: season = get_season(season) # Parse moves if batch: moves = [] for move_str in batch.split(","): move_str = move_str.strip() if not move_str: continue parts = move_str.split(":") if len(parts) != 2 or not parts[0].strip() or not parts[1].strip(): console.print(f"[red]Invalid batch format:[/red] '{move_str}'") console.print("Expected format: 'Player Name:TEAM'") raise typer.Exit(1) moves.append((parts[0].strip(), parts[1].strip().upper())) elif name and team: moves = [(name, team.upper())] else: console.print( "[red]Error:[/red] Provide player name and team, or use --batch" ) console.print("\nUsage:") console.print(' majordomo player move "Mike Trout" CAR') console.print( ' majordomo player move --batch "Mike Trout:CAR,Aaron Judge:NYM"' ) raise typer.Exit(1) results = [] for player_name, team_abbrev in moves: # Find player player = state.api.get_player(name=player_name, season=season) if not player: # Try search search_results = state.api.search_players( query=player_name, season=season, limit=1 ) if search_results: player = search_results[0] else: results.append( ( player_name, team_abbrev, "[red]ERROR[/red]", "Player not found", ) ) continue # Get current team current_team = player.get("team", {}) current_abbrev = ( current_team.get("abbrev", "N/A") if isinstance(current_team, dict) else "N/A" ) # Find target team try: target_team = state.api.get_team(abbrev=team_abbrev, season=season) except Exception: results.append( ( player_name, team_abbrev, "[red]ERROR[/red]", f"Team '{team_abbrev}' not found", ) ) continue if dry_run: results.append( ( player["name"], team_abbrev, "[blue]DRY-RUN[/blue]", f"Would move from {current_abbrev}", ) ) continue # Perform update state.api.update_player(player["id"], team_id=target_team["id"]) results.append( ( player["name"], team_abbrev, "[green]SUCCESS[/green]", f"Moved from {current_abbrev}", ) ) if state.json_output: json_results = [ { "player": r[0], "team": r[1], "status": r[2].replace("[", "").replace("]", "").split("/")[0], "message": r[3], } for r in results ] output_json(json_results) return title = "Player Moves" if dry_run: title += " (DRY RUN)" output_table(title, ["Player", "Target", "Status", "Message"], results) except typer.Exit: raise except Exception as e: handle_error(e) # ============================================================================ # Team Commands # ============================================================================ @team_app.command("list") def team_list( season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, active: Annotated[ bool, typer.Option("--active", "-a", help="Exclude IL/MiL teams") ] = False, ): """List all teams""" try: season = get_season(season) teams = state.api.list_teams(season=season, active_only=active) if state.json_output: output_json(teams) return if not teams: console.print(f"[yellow]No teams found in season {season}[/yellow]") return rows = [] for t in teams: manager = t.get("manager1", {}) manager_name = manager.get("name", "") if isinstance(manager, dict) else "" division = t.get("division", {}) div_abbrev = ( division.get("division_abbrev", "") if isinstance(division, dict) else "" ) rows.append( [ t["abbrev"], t.get("lname", t.get("sname", "")), div_abbrev, manager_name, ] ) output_table( f"Teams - Season {season}" + (" (Active Only)" if active else ""), ["Abbrev", "Name", "Division", "Manager"], rows, ) except Exception as e: handle_error(e) @team_app.command("get") def team_get( abbrev: Annotated[str, typer.Argument(help="Team abbreviation")], season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, ): """Get team information""" try: season = get_season(season) team = state.api.get_team(abbrev=abbrev.upper(), season=season) if state.json_output: output_json(team) return manager = team.get("manager1", {}) manager_name = ( manager.get("name", "N/A") if isinstance(manager, dict) else "N/A" ) division = team.get("division", {}) div_name = ( division.get("division_name", "N/A") if isinstance(division, dict) else "N/A" ) salary_cap = team.get("salary_cap") cap_str = f"{salary_cap:.1f}" if salary_cap is not None else "N/A" panel = Panel( f"[bold]ID:[/bold] {team['id']}\n" f"[bold]Abbreviation:[/bold] {team['abbrev']}\n" f"[bold]Short Name:[/bold] {team.get('sname', 'N/A')}\n" f"[bold]Full Name:[/bold] {team.get('lname', 'N/A')}\n" f"[bold]Division:[/bold] {div_name}\n" f"[bold]Manager:[/bold] {manager_name}\n" f"[bold]Salary Cap:[/bold] {cap_str}\n" f"[bold]Stadium:[/bold] {team.get('stadium', 'N/A')}\n" f"[bold]Season:[/bold] {team.get('season', 'N/A')}", title=f"Team: {team.get('lname', abbrev)}", border_style="green", ) console.print(panel) except Exception as e: handle_error(e, f"Team '{abbrev}' may not exist.") @team_app.command("roster") def team_roster( abbrev: Annotated[str, typer.Argument(help="Team abbreviation")], which: Annotated[ str, typer.Option("--which", "-w", help="'current' or 'next'") ] = "current", season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, ): """Show team roster breakdown""" try: season = get_season(season) team = state.api.get_team(abbrev=abbrev.upper(), season=season) roster = state.api.get_team_roster(team_id=team["id"], which=which) if state.json_output: output_json(roster) return console.print( f"\n[bold cyan]{team.get('lname', abbrev)} Roster ({which.title()})[/bold cyan]\n" ) # Active roster active = roster.get("active", {}).get("players", []) if active: active_rows = [ [p["name"], p.get("pos_1", ""), f"{p.get('wara', 0):.2f}"] for p in active ] output_table( f"Active ({len(active)})", ["Name", "Pos", "WARA"], active_rows ) # Short IL short_il = roster.get("shortil", {}).get("players", []) if short_il: console.print() il_rows = [ [p["name"], p.get("pos_1", ""), p.get("il_return", "")] for p in short_il ] output_table( f"Short IL ({len(short_il)})", ["Name", "Pos", "Return"], il_rows ) # Long IL long_il = roster.get("longil", {}).get("players", []) if long_il: console.print() lil_rows = [ [p["name"], p.get("pos_1", ""), p.get("il_return", "")] for p in long_il ] output_table( f"Long IL ({len(long_il)})", ["Name", "Pos", "Return"], lil_rows ) # Summary total = len(active) + len(short_il) + len(long_il) console.print(f"\n[dim]Total: {total} players[/dim]") except Exception as e: handle_error(e, f"Team '{abbrev}' may not exist.") # ============================================================================ # Standings Command # ============================================================================ @app.command() def standings( season: Annotated[ Optional[int], typer.Option("--season", "-s", help="Season number") ] = None, division: Annotated[ Optional[str], typer.Option( "--division", "-d", help="Division (ALE, ALW, ALC, NLE, NLW, NLC)" ), ] = None, league: Annotated[ Optional[str], typer.Option("--league", "-l", help="League (AL or NL)") ] = None, ): """Show league standings""" try: season = get_season(season) standings_data = state.api.get_standings( season=season, division_abbrev=division.upper() if division else None, league_abbrev=league.upper() if league else None, ) if state.json_output: output_json(standings_data) return if not standings_data: console.print(f"[yellow]No standings found for season {season}[/yellow]") return rows = [] for s in standings_data: team = s.get("team", {}) team_abbrev = team.get("abbrev", "N/A") if isinstance(team, dict) else "N/A" team_name = team.get("lname", "N/A") if isinstance(team, dict) else "N/A" wins = s.get("wins", 0) losses = s.get("losses", 0) total = wins + losses pct = f".{int(wins/total*1000):03d}" if total > 0 else ".000" rd = s.get("run_diff", 0) rd_str = f"+{rd}" if rd > 0 else str(rd) rows.append([team_abbrev, team_name, wins, losses, pct, rd_str]) title = f"Standings - Season {season}" if division: title += f" ({division.upper()})" elif league: title += f" ({league.upper()})" output_table(title, ["Team", "Name", "W", "L", "PCT", "RD"], rows) except Exception as e: handle_error(e) # ============================================================================ # Entry Point # ============================================================================ if __name__ == "__main__": app()