claude-configs/skills/major-domo/cli.py
Cal Corum 8a1d15911f Initial commit: Claude Code configuration backup
Version control Claude Code configuration including:
- Global instructions (CLAUDE.md)
- User settings (settings.json)
- Custom agents (architect, designer, engineer, etc.)
- Custom skills (create-skill templates and workflows)

Excludes session data, secrets, cache, and temporary files per .gitignore.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:34:21 -06:00

520 lines
19 KiB
Python
Executable File

#!/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.
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
Environment:
API_TOKEN: Required. Bearer token for API authentication.
"""
import json
import os
import sys
from typing import Annotated, List, Optional
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
# Import the existing API client from same directory
sys.path.insert(0, os.path.dirname(__file__))
from api_client import MajorDomoAPI
# ============================================================================
# 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")
console = Console()
class State:
"""Global state for API client and settings"""
api: Optional[MajorDomoAPI] = None
json_output: bool = False
current_season: Optional[int] = None
state = State()
# ============================================================================
# Output Helpers
# ============================================================================
def output_json(data):
"""Output data as formatted JSON"""
console.print_json(json.dumps(data, indent=2, default=str))
def output_table(title: str, columns: List[str], rows: List[List], show_lines: bool = False):
"""Output data as a rich table"""
table = Table(title=title, show_header=True, header_style="bold cyan", show_lines=show_lines)
for col in columns:
table.add_column(col)
for row in rows:
table.add_row(*[str(cell) if cell is not None else "" for cell in row])
console.print(table)
def handle_error(e: Exception, context: str = ""):
"""Graceful error handling with helpful messages"""
error_str = str(e)
if "401" in error_str:
console.print("[red]Error:[/red] Unauthorized. Check your API_TOKEN.")
elif "404" in error_str:
console.print(f"[red]Error:[/red] Not found. {context}")
elif "Connection" in error_str or "ConnectionError" in error_str:
console.print("[red]Error:[/red] Cannot connect to API. Check network and --env setting.")
else:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
def get_season(season: Optional[int]) -> int:
"""Get season, defaulting to current if not specified"""
return season if season is not None else state.current_season
# ============================================================================
# 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'
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]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()