Add row borders and detail item separators to improve readability of the rich table output. Each agent row now displays borders across all columns, and detail items are separated by short horizontal bars. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
639 lines
21 KiB
Python
Executable file
639 lines
21 KiB
Python
Executable file
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# requires-python = ">=3.14"
|
|
# dependencies = ["typer>=0.9.0", "rich>=13.0.0"]
|
|
# ///
|
|
"""mcquack - A simple macOS LaunchAgent manager for executable scripts."""
|
|
|
|
import os
|
|
import plistlib
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
app = typer.Typer(help="mcquack - A simple macOS LaunchAgent manager")
|
|
|
|
LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
|
|
LOGS_DIR = Path.home() / "Library" / "Logs"
|
|
PLIST_PREFIX = "mcquack.eblume"
|
|
|
|
|
|
def get_plist_path(command_path: Path) -> Path:
|
|
"""Get the plist path for a given command."""
|
|
label = f"{PLIST_PREFIX}.{command_path.stem}"
|
|
return LAUNCH_AGENTS_DIR / f"{label}.plist"
|
|
|
|
|
|
def get_label(command_path: Path) -> str:
|
|
"""Get the launchd label for a given command."""
|
|
return f"{PLIST_PREFIX}.{command_path.stem}"
|
|
|
|
|
|
def resolve_command(command: str) -> tuple[Path, bool]:
|
|
"""Resolve a command to a Path and indicate if it should be kept as command name.
|
|
|
|
Returns:
|
|
tuple: (resolved_path, keep_as_command_name)
|
|
- resolved_path: The Path to the executable
|
|
- keep_as_command_name: True if command should remain as-is (not expanded to absolute)
|
|
|
|
If command is a path (contains / or starts with ~ or .):
|
|
- Expands ~ and resolves to absolute path
|
|
- Validates existence and executability
|
|
- Returns (resolved_path, False) to use absolute path in plist
|
|
|
|
If command is not a path (no /):
|
|
- Searches $PATH using shutil.which()
|
|
- Returns (path_to_command, True) to keep command name as-is
|
|
- Raises error if not found on PATH
|
|
"""
|
|
# Check if this is a path (contains / or starts with ~ or .)
|
|
is_path = "/" in command or command.startswith("~") or command.startswith(".")
|
|
|
|
if is_path:
|
|
# Treat as path: expand and resolve
|
|
path = Path(command).expanduser().resolve()
|
|
|
|
if not path.exists():
|
|
typer.echo(f"Error: {path} does not exist.", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
if not os.access(path, os.X_OK):
|
|
typer.echo(f"Error: {path} is not executable.", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
return (path, False)
|
|
else:
|
|
# Not a path: search $PATH
|
|
which_result = shutil.which(command)
|
|
|
|
if which_result is None:
|
|
typer.echo(f"Error: command '{command}' not found in PATH.", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
# Verify the found command is executable
|
|
if not os.access(which_result, os.X_OK):
|
|
typer.echo(f"Error: {which_result} is not executable.", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
# Return the command name as-is (don't expand to absolute path)
|
|
return (Path(command), True)
|
|
|
|
|
|
def resolve_plist_name(plist_identifier: str) -> Path:
|
|
"""Resolve a plist identifier to an actual plist path.
|
|
|
|
Accepts:
|
|
- Full filename: 'mcquack.eblume.mycommand.plist'
|
|
- Label: 'mcquack.eblume.mycommand'
|
|
- Stem: 'mycommand'
|
|
- Full path: '/Users/.../Library/LaunchAgents/mcquack.eblume.mycommand.plist'
|
|
|
|
Returns the resolved plist path, or raises an error if ambiguous/not found.
|
|
"""
|
|
# If it's already a full path to an existing file, use it
|
|
as_path = Path(plist_identifier)
|
|
if as_path.is_absolute() and as_path.exists():
|
|
return as_path
|
|
|
|
# Normalize the identifier (remove .plist extension if present)
|
|
identifier = plist_identifier
|
|
if identifier.endswith('.plist'):
|
|
identifier = identifier[:-6]
|
|
|
|
# Get all mcquack plists
|
|
if not LAUNCH_AGENTS_DIR.exists():
|
|
typer.echo(f"Error: No LaunchAgents directory found at {LAUNCH_AGENTS_DIR}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
all_plists = list(LAUNCH_AGENTS_DIR.glob(f"{PLIST_PREFIX}.*.plist"))
|
|
|
|
if not all_plists:
|
|
typer.echo("Error: No mcquack-managed LaunchAgents found.", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
# Try exact match on label first
|
|
exact_match = LAUNCH_AGENTS_DIR / f"{identifier}.plist"
|
|
if exact_match.exists():
|
|
return exact_match
|
|
|
|
# Try with prefix if not already prefixed
|
|
if not identifier.startswith(f"{PLIST_PREFIX}."):
|
|
with_prefix = LAUNCH_AGENTS_DIR / f"{PLIST_PREFIX}.{identifier}.plist"
|
|
if with_prefix.exists():
|
|
return with_prefix
|
|
|
|
# Try fuzzy matching on stem
|
|
matches = []
|
|
for plist_path in all_plists:
|
|
# Extract the stem (everything after the prefix)
|
|
plist_label = plist_path.stem
|
|
if plist_label.startswith(f"{PLIST_PREFIX}."):
|
|
stem = plist_label[len(f"{PLIST_PREFIX}."):]
|
|
if stem == identifier:
|
|
matches.append(plist_path)
|
|
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
elif len(matches) > 1:
|
|
typer.echo(f"Error: Ambiguous plist identifier '{plist_identifier}'. Did you mean:", err=True)
|
|
for match in matches:
|
|
typer.echo(f" - {match.stem}", err=True)
|
|
raise typer.Exit(1)
|
|
else:
|
|
typer.echo(f"Error: No plist found matching '{plist_identifier}'.", err=True)
|
|
# Show suggestions
|
|
typer.echo("Available plists:", err=True)
|
|
for plist_path in all_plists:
|
|
typer.echo(f" - {plist_path.stem}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
|
|
def parse_calendar(calendar_str: str) -> dict[str, int]:
|
|
"""Parse a calendar string like 'Hour=2,Minute=0' into a dictionary.
|
|
|
|
Valid keys: Month (1-12), Day (1-31), Weekday (0-7), Hour (0-23), Minute (0-59)
|
|
"""
|
|
valid_keys = {"Month", "Day", "Weekday", "Hour", "Minute"}
|
|
ranges = {
|
|
"Month": (1, 12),
|
|
"Day": (1, 31),
|
|
"Weekday": (0, 7),
|
|
"Hour": (0, 23),
|
|
"Minute": (0, 59),
|
|
}
|
|
|
|
result = {}
|
|
parts = calendar_str.split(",")
|
|
|
|
for part in parts:
|
|
part = part.strip()
|
|
if "=" not in part:
|
|
raise ValueError(f"Invalid calendar format: '{part}' (missing '=')")
|
|
|
|
key, value = part.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
|
|
if key not in valid_keys:
|
|
raise ValueError(
|
|
f"Invalid calendar key: '{key}' (valid keys: {', '.join(sorted(valid_keys))})"
|
|
)
|
|
|
|
try:
|
|
int_value = int(value)
|
|
except ValueError:
|
|
raise ValueError(f"Invalid calendar value for {key}: '{value}' (must be an integer)")
|
|
|
|
min_val, max_val = ranges[key]
|
|
if not (min_val <= int_value <= max_val):
|
|
raise ValueError(
|
|
f"Invalid calendar value for {key}: {int_value} (must be {min_val}-{max_val})"
|
|
)
|
|
|
|
result[key] = int_value
|
|
|
|
return result
|
|
|
|
|
|
def create_plist_dict(
|
|
command_path: Path,
|
|
args: list[str],
|
|
program_arg: str | None = None,
|
|
interval: int | None = None,
|
|
throttle: int | None = None,
|
|
calendar: list[dict[str, int]] | None = None,
|
|
) -> dict:
|
|
"""Create a plist dictionary for a command.
|
|
|
|
Args:
|
|
command_path: Path used for label generation (stem)
|
|
args: Additional arguments to pass to the program
|
|
program_arg: The actual program path/command to execute (if None, uses command_path.resolve())
|
|
interval: StartInterval value
|
|
throttle: ThrottleInterval value
|
|
calendar: StartCalendarInterval value
|
|
"""
|
|
label = get_label(command_path)
|
|
# Use program_arg if provided, otherwise fall back to resolved command_path
|
|
program = program_arg if program_arg is not None else str(command_path.resolve())
|
|
program_args = [program] + args
|
|
|
|
plist = {
|
|
"Label": label,
|
|
"ProgramArguments": program_args,
|
|
"RunAtLoad": True,
|
|
"StandardOutPath": str(LOGS_DIR / f"mcquack.{command_path.stem}.out.log"),
|
|
"StandardErrorPath": str(LOGS_DIR / f"mcquack.{command_path.stem}.err.log"),
|
|
}
|
|
|
|
# If scheduling is used, don't keep alive (let the schedule handle it)
|
|
if interval is not None:
|
|
plist["KeepAlive"] = False
|
|
plist["StartInterval"] = interval
|
|
elif calendar is not None:
|
|
plist["KeepAlive"] = False
|
|
# Single calendar entry is a dict, multiple entries are a list
|
|
if len(calendar) == 1:
|
|
plist["StartCalendarInterval"] = calendar[0]
|
|
else:
|
|
plist["StartCalendarInterval"] = calendar
|
|
else:
|
|
plist["KeepAlive"] = True
|
|
|
|
if throttle is not None:
|
|
plist["ThrottleInterval"] = throttle
|
|
|
|
return plist
|
|
|
|
|
|
@app.command("list")
|
|
def list_agents() -> None:
|
|
"""List all mcquack-managed LaunchAgents."""
|
|
if not LAUNCH_AGENTS_DIR.exists():
|
|
typer.echo("No LaunchAgents directory found.")
|
|
return
|
|
|
|
plists = sorted(LAUNCH_AGENTS_DIR.glob(f"{PLIST_PREFIX}.*.plist"))
|
|
if not plists:
|
|
typer.echo("No mcquack-managed LaunchAgents found.")
|
|
return
|
|
|
|
console = Console()
|
|
table = Table(show_header=True, header_style="bold cyan", show_lines=True)
|
|
table.add_column("Label", style="green", no_wrap=False)
|
|
table.add_column("Command", style="yellow", no_wrap=False, overflow="fold")
|
|
table.add_column("Details", style="dim", no_wrap=False, overflow="fold")
|
|
|
|
for plist_path in plists:
|
|
try:
|
|
with open(plist_path, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
|
|
label = plist.get("Label", "unknown")
|
|
program_args = plist.get("ProgramArguments", [])
|
|
command = program_args[0] if program_args else "unknown"
|
|
|
|
# Build details column with multiple lines
|
|
details = []
|
|
if len(program_args) > 1:
|
|
args_str = ' '.join(program_args[1:])
|
|
details.append(f"Args: {args_str}")
|
|
|
|
if "StartInterval" in plist:
|
|
details.append(f"StartInterval: {plist['StartInterval']}s")
|
|
|
|
if "ThrottleInterval" in plist:
|
|
details.append(f"ThrottleInterval: {plist['ThrottleInterval']}s")
|
|
|
|
if "StartCalendarInterval" in plist:
|
|
calendar = plist["StartCalendarInterval"]
|
|
if isinstance(calendar, list):
|
|
details.append(f"StartCalendarInterval: {len(calendar)} entries")
|
|
else:
|
|
calendar_str = ', '.join(f"{k}={v}" for k, v in calendar.items())
|
|
details.append(f"StartCalendarInterval: {calendar_str}")
|
|
|
|
# Join details with a short horizontal separator
|
|
separator = "─" * 8
|
|
details_str = f'\n{separator}\n'.join(details) if details else "-"
|
|
table.add_row(label, command, details_str)
|
|
|
|
except Exception as e:
|
|
table.add_row(plist_path.name, f"error: {e}", "-")
|
|
|
|
console.print(table)
|
|
|
|
|
|
@app.command()
|
|
def create(
|
|
command: Annotated[str, typer.Argument(help="Command or path to the executable")],
|
|
command_args: Annotated[
|
|
list[str] | None,
|
|
typer.Argument(help="Arguments to pass to the command (must come after '--')"),
|
|
] = None,
|
|
interval: Annotated[
|
|
int | None,
|
|
typer.Option(
|
|
"--interval",
|
|
help="Run every N seconds (StartInterval)",
|
|
min=1,
|
|
),
|
|
] = None,
|
|
throttle: Annotated[
|
|
int | None,
|
|
typer.Option(
|
|
"--throttle",
|
|
help="Minimum seconds between runs (ThrottleInterval)",
|
|
min=1,
|
|
),
|
|
] = None,
|
|
calendar: Annotated[
|
|
list[str] | None,
|
|
typer.Option(
|
|
"--calendar",
|
|
help="Schedule with calendar interval (e.g., 'Hour=2,Minute=0'). Can be specified multiple times.",
|
|
),
|
|
] = None,
|
|
) -> None:
|
|
"""Create and load a LaunchAgent for a script.
|
|
|
|
The command argument can be:
|
|
- A path (absolute or relative, with / or ~ or .): will be expanded and validated
|
|
- A command name (no /): will be searched on $PATH
|
|
|
|
To pass arguments to your command, use '--' to separate mcquack options from
|
|
command arguments. For example:
|
|
|
|
mcquack create my_script.sh -- --config /path/to/config
|
|
|
|
Without '--', options like --help apply to mcquack itself:
|
|
|
|
mcquack create my_script.sh --help # Shows this help
|
|
mcquack create my_script.sh -- --help # Passes --help to the command
|
|
|
|
Scheduling options:
|
|
|
|
mcquack create script.sh --interval 300 # Run every 5 minutes
|
|
mcquack create script.sh --interval 300 --throttle 60 # Run every 5 min, throttle 60s
|
|
mcquack create script.sh --calendar "Hour=2,Minute=0" # Every day at 2 AM
|
|
mcquack create script.sh --calendar "Weekday=1,Hour=9,Minute=0" # Mondays at 9 AM
|
|
"""
|
|
# Resolve command to path and determine if it should be kept as command name
|
|
resolved_path, keep_as_command_name = resolve_command(command)
|
|
|
|
# For label generation, use the original command name (stem of the resolved path)
|
|
command_path = Path(command).expanduser() if ("/" in command or command.startswith("~") or command.startswith(".")) else resolved_path
|
|
|
|
# Validate mutually exclusive options
|
|
if interval is not None and calendar is not None:
|
|
typer.echo(
|
|
"Error: --interval and --calendar are mutually exclusive. Use one or the other.",
|
|
err=True,
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
plist_path = get_plist_path(command_path)
|
|
|
|
if plist_path.exists():
|
|
typer.echo(
|
|
f"Error: {plist_path} already exists. Use 'edit' to modify or 'delete' first.",
|
|
err=True,
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
# Parse calendar strings if provided
|
|
parsed_calendar = None
|
|
if calendar is not None:
|
|
try:
|
|
parsed_calendar = [parse_calendar(cal_str) for cal_str in calendar]
|
|
except ValueError as e:
|
|
typer.echo(f"Error: {e}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
# Ensure directories exist
|
|
LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Determine what to use in ProgramArguments
|
|
program_arg = command if keep_as_command_name else str(resolved_path)
|
|
|
|
plist = create_plist_dict(
|
|
command_path,
|
|
command_args or [],
|
|
program_arg=program_arg,
|
|
interval=interval,
|
|
throttle=throttle,
|
|
calendar=parsed_calendar,
|
|
)
|
|
|
|
with open(plist_path, "wb") as f:
|
|
plistlib.dump(plist, f)
|
|
|
|
typer.echo(f"Created: {plist_path}")
|
|
|
|
# Load the plist
|
|
result = subprocess.run(
|
|
["launchctl", "load", str(plist_path)], capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
typer.echo(f"Warning: launchctl load failed: {result.stderr}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
typer.echo(f"Loaded: {get_label(command_path)}")
|
|
|
|
|
|
@app.command()
|
|
def launch(
|
|
plist_identifier: Annotated[str, typer.Argument(help="Plist name, label, stem, or full path")],
|
|
) -> None:
|
|
"""Kickstart a LaunchAgent.
|
|
|
|
Examples:
|
|
mcquack launch mycommand
|
|
mcquack launch mcquack.eblume.mycommand
|
|
mcquack launch mcquack.eblume.mycommand.plist
|
|
"""
|
|
plist_path = resolve_plist_name(plist_identifier)
|
|
|
|
# Get label from plist
|
|
with open(plist_path, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
label = plist.get("Label", "unknown")
|
|
|
|
uid = os.getuid()
|
|
|
|
result = subprocess.run(
|
|
["launchctl", "kickstart", f"gui/{uid}/{label}"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
typer.echo(f"Error: launchctl kickstart failed: {result.stderr}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
typer.echo(f"Kickstarted: {label}")
|
|
|
|
|
|
@app.command()
|
|
def show(
|
|
plist_identifier: Annotated[str, typer.Argument(help="Plist name, label, stem, or full path")],
|
|
) -> None:
|
|
"""Show the arguments configured in a plist.
|
|
|
|
Examples:
|
|
mcquack show mycommand
|
|
mcquack show mcquack.eblume.mycommand
|
|
mcquack show mcquack.eblume.mycommand.plist
|
|
"""
|
|
plist_path = resolve_plist_name(plist_identifier)
|
|
|
|
with open(plist_path, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
|
|
program_args = plist.get("ProgramArguments", [])
|
|
|
|
typer.echo(f"Label: {plist.get('Label', 'unknown')}")
|
|
typer.echo(f"Script: {program_args[0] if program_args else 'unknown'}")
|
|
typer.echo(
|
|
f"Arguments: {' '.join(program_args[1:]) if len(program_args) > 1 else '(none)'}"
|
|
)
|
|
typer.echo(f"RunAtLoad: {plist.get('RunAtLoad', False)}")
|
|
typer.echo(f"KeepAlive: {plist.get('KeepAlive', False)}")
|
|
|
|
# Display scheduling information
|
|
if "StartInterval" in plist:
|
|
typer.echo(f"StartInterval: {plist['StartInterval']}")
|
|
if "ThrottleInterval" in plist:
|
|
typer.echo(f"ThrottleInterval: {plist['ThrottleInterval']}")
|
|
if "StartCalendarInterval" in plist:
|
|
typer.echo(f"StartCalendarInterval: {plist['StartCalendarInterval']}")
|
|
|
|
typer.echo(f"Stdout: {plist.get('StandardOutPath', 'N/A')}")
|
|
typer.echo(f"Stderr: {plist.get('StandardErrorPath', 'N/A')}")
|
|
|
|
|
|
@app.command()
|
|
def edit(
|
|
plist_identifier: Annotated[str, typer.Argument(help="Plist name, label, stem, or full path")],
|
|
) -> None:
|
|
"""Edit a plist in $EDITOR.
|
|
|
|
Opens the plist file in your default editor.
|
|
After editing, the agent is automatically reloaded.
|
|
|
|
Examples:
|
|
mcquack edit mycommand
|
|
mcquack edit mcquack.eblume.mycommand
|
|
mcquack edit mcquack.eblume.mycommand.plist
|
|
"""
|
|
plist_path = resolve_plist_name(plist_identifier)
|
|
|
|
editor = os.environ.get("EDITOR", "vi")
|
|
|
|
# Unload first
|
|
subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
|
|
|
|
# Open in editor
|
|
result = subprocess.run([editor, str(plist_path)])
|
|
if result.returncode != 0:
|
|
typer.echo(f"Error: editor exited with code {result.returncode}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
typer.echo(f"Edited: {plist_path}")
|
|
|
|
# Get label from plist for reload message
|
|
with open(plist_path, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
label = plist.get("Label", "unknown")
|
|
|
|
# Reload
|
|
result = subprocess.run(
|
|
["launchctl", "load", str(plist_path)], capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
typer.echo(f"Warning: launchctl load failed: {result.stderr}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
typer.echo(f"Reloaded: {label}")
|
|
|
|
|
|
@app.command()
|
|
def unload(
|
|
plist_identifier: Annotated[str, typer.Argument(help="Plist name, label, stem, or full path")],
|
|
) -> None:
|
|
"""Unload a LaunchAgent.
|
|
|
|
Examples:
|
|
mcquack unload mycommand
|
|
mcquack unload mcquack.eblume.mycommand
|
|
mcquack unload mcquack.eblume.mycommand.plist
|
|
"""
|
|
plist_path = resolve_plist_name(plist_identifier)
|
|
|
|
# Get label from plist
|
|
with open(plist_path, "rb") as f:
|
|
plist = plistlib.load(f)
|
|
label = plist.get("Label", "unknown")
|
|
|
|
result = subprocess.run(
|
|
["launchctl", "unload", str(plist_path)], capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
typer.echo(f"Error: launchctl unload failed: {result.stderr}", err=True)
|
|
raise typer.Exit(1)
|
|
|
|
typer.echo(f"Unloaded: {label}")
|
|
|
|
|
|
@app.command()
|
|
def delete(
|
|
plist_identifier: Annotated[str, typer.Argument(help="Plist name, label, stem, or full path")],
|
|
) -> None:
|
|
"""Delete a LaunchAgent plist.
|
|
|
|
Examples:
|
|
mcquack delete mycommand
|
|
mcquack delete mcquack.eblume.mycommand
|
|
mcquack delete mcquack.eblume.mycommand.plist
|
|
"""
|
|
plist_path = resolve_plist_name(plist_identifier)
|
|
|
|
# Unload first (ignore errors)
|
|
subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
|
|
|
|
plist_path.unlink()
|
|
typer.echo(f"Deleted: {plist_path}")
|
|
|
|
|
|
@app.command(hidden=True)
|
|
def quack() -> None:
|
|
"""Quack!"""
|
|
art = r"""
|
|
___ _ _____ _
|
|
| \ _ _ __| |_|_ _|_ _| |___ ___
|
|
| |) | || / _| / / | |/ _` | / -_|_-<
|
|
|___/ \_,_\__|_\_\ |_|\__,_|_\___/__/
|
|
|
|
'' .``,l"
|
|
"[}ll1)f/1I
|
|
!__YpQm+:]",
|
|
~>. ,i,i<nxnav(,:" .
|
|
;/Zj, ::'::[trcx(i<-" `i``umv~'
|
|
[do| .":]z]<<[; :fzLwddmw_
|
|
cWr xxdvzr\fr\_:-~' "QOL0b\;
|
|
X*> `!>~1cXUXvcLbccL_{1! . +YM)
|
|
:U1. <fUmpZUxXmphkqwv-]?+ .i~-}>.
|
|
";|XmkodUf{?_?{xCCZw\[{[~>">{}{]{i
|
|
`>]11[][[)UmYrz+fx1}{{}<_]{{{{{?.
|
|
"I<___]+^Qo**h)}}]}{{}1+}1{}{{{<
|
|
`+}{{{{}]<|8Mkou?{{{{{}1?,~][[[?l
|
|
;{{}}}[[]?v0r?|{{{{{{{1?` `^`
|
|
![}{{}[]i .?{{[]}}~'
|
|
.,;;,` i]_fC|>
|
|
"{vxfXQcv{,
|
|
.>/zJJJCUXXXJz]
|
|
'?vYCCUXcvvvcXJJU>
|
|
!XJJYv\?>l::,I[Yu'
|
|
)C/l. <n,.
|
|
`/\. ;{+
|
|
<{~ ![}[i:"^`^`
|
|
'"I+}}[I .-{}}}}[[[}-`
|
|
;--][[}{}}}l 'i][}}}[]]+^
|
|
;[[[}}}}[_, .";II" .
|
|
.```:!!;^
|
|
|
|
"If it has wings, I can crash it."
|
|
- Launchpad McQuack
|
|
"""
|
|
typer.echo(art)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|