mcquack/mcquack.py
Erich Blume 34e63c41c4 Add quack easter egg and development docs
Add a hidden 'quack' command that displays DuckTales ASCII art of
Launchpad McQuack. Also document the development workflow in README,
including how to run tests with uv and the optional venv setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:09:07 -08:00

319 lines
9.8 KiB
Python
Executable file

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = ["typer>=0.9.0"]
# ///
"""mcquack - A simple macOS LaunchAgent manager for executable scripts."""
import os
import plistlib
import subprocess
import sys
from pathlib import Path
from typing import Annotated
import typer
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(script_path: Path) -> Path:
"""Get the plist path for a given script."""
label = f"{PLIST_PREFIX}.{script_path.stem}"
return LAUNCH_AGENTS_DIR / f"{label}.plist"
def get_label(script_path: Path) -> str:
"""Get the launchd label for a given script."""
return f"{PLIST_PREFIX}.{script_path.stem}"
def create_plist_dict(script_path: Path, args: list[str]) -> dict:
"""Create a plist dictionary for a script."""
label = get_label(script_path)
program_args = [str(script_path.resolve())] + args
return {
"Label": label,
"ProgramArguments": program_args,
"RunAtLoad": True,
"KeepAlive": True,
"StandardOutPath": str(LOGS_DIR / f"mcquack.{script_path.stem}.out.log"),
"StandardErrorPath": str(LOGS_DIR / f"mcquack.{script_path.stem}.err.log"),
}
@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
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", [])
script = program_args[0] if program_args else "unknown"
typer.echo(f"{label}: {script}")
except Exception as e:
typer.echo(f"{plist_path.name}: error reading ({e})")
@app.command()
def create(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
script_args: Annotated[
list[str] | None,
typer.Argument(help="Arguments to pass to the script (must come after '--')"),
] = None,
) -> None:
"""Create and load a LaunchAgent for a script.
To pass arguments to your script, use '--' to separate mcquack options from
script 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 script
"""
script_path = script.resolve()
if not script_path.exists():
typer.echo(f"Error: {script_path} does not exist.", err=True)
raise typer.Exit(1)
if not os.access(script_path, os.X_OK):
typer.echo(f"Error: {script_path} is not executable.", err=True)
raise typer.Exit(1)
plist_path = get_plist_path(script_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)
# Ensure directories exist
LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
plist = create_plist_dict(script_path, script_args or [])
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(script_path)}")
@app.command()
def launch(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
) -> None:
"""Kickstart a LaunchAgent."""
script_path = script.resolve()
plist_path = get_plist_path(script_path)
if not plist_path.exists():
typer.echo(f"Error: {plist_path} does not exist. Use 'create' first.", err=True)
raise typer.Exit(1)
label = get_label(script_path)
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(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
) -> None:
"""Show the arguments configured in a plist."""
script_path = script.resolve()
plist_path = get_plist_path(script_path)
if not plist_path.exists():
typer.echo(f"Error: {plist_path} does not exist.", err=True)
raise typer.Exit(1)
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)}")
typer.echo(f"Stdout: {plist.get('StandardOutPath', 'N/A')}")
typer.echo(f"Stderr: {plist.get('StandardErrorPath', 'N/A')}")
@app.command()
def edit(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
script_args: Annotated[
list[str] | None,
typer.Argument(help="New arguments for the script (must come after '--')"),
] = None,
) -> None:
"""Edit the arguments in a plist.
To update script arguments, use '--' to separate mcquack options from
script arguments. For example:
mcquack edit my_script.sh -- --verbose --debug
This replaces all existing script arguments. To clear arguments, omit them:
mcquack edit my_script.sh
"""
script_path = script.resolve()
plist_path = get_plist_path(script_path)
if not plist_path.exists():
typer.echo(f"Error: {plist_path} does not exist. Use 'create' first.", err=True)
raise typer.Exit(1)
with open(plist_path, "rb") as f:
plist = plistlib.load(f)
program_args = plist.get("ProgramArguments", [])
script_in_plist = program_args[0] if program_args else str(script_path)
# Update arguments
plist["ProgramArguments"] = [script_in_plist] + (script_args or [])
# Unload first
subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
with open(plist_path, "wb") as f:
plistlib.dump(plist, f)
typer.echo(f"Updated: {plist_path}")
# 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: {get_label(script_path)}")
@app.command()
def unload(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
) -> None:
"""Unload a LaunchAgent."""
script_path = script.resolve()
plist_path = get_plist_path(script_path)
if not plist_path.exists():
typer.echo(f"Error: {plist_path} does not exist.", err=True)
raise typer.Exit(1)
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: {get_label(script_path)}")
@app.command()
def delete(
script: Annotated[Path, typer.Argument(help="Path to the executable script")],
) -> None:
"""Delete a LaunchAgent plist."""
script_path = script.resolve()
plist_path = get_plist_path(script_path)
if not plist_path.exists():
typer.echo(f"Error: {plist_path} does not exist.", err=True)
raise typer.Exit(1)
# 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 we're not back by dawn... call the press!"
- Launchpad McQuack
"""
typer.echo(art)
if __name__ == "__main__":
app()