Skip to content

ledger.mkdocs.adr.cli

CLI commands for creating and managing ADRs/MADRs.

This module provides command-line tools for managing Architecture Decision Records (ADRs) following the MADR 4.0 template specification.

See: - https://adr.github.io/ for ADR documentation - https://adr.github.io/madr/ for MADR template documentation

Classes:

  • ADRDict

    Dictionary representation of an ADR/MADR document.

  • Context

    Common context object for CLI commands containing shared parameters.

  • ThemedConsole

Functions:

  • adr_to_dict

    Convert an ADRDocument to a normalized dictionary.

  • cli

    ADR/MADR management helpers

  • get_jinja_env

    Get a configured Jinja2 environment.

  • get_next_adr_index

    Get the next ADR/MADR index by scanning existing files.

  • list_adrs

    List ADR/MADR documents in a directory.

  • new_adr

    Create a new ADR/MADR document.

  • slugify

    Convert title to a URL-friendly slug.

Attributes:

ALLOWED_STATUSES module-attribute

ALLOWED_STATUSES = ['proposed', 'accepted', 'rejected', 'superseded', 'deprecated', 'amended']

DEFAULT_ADR_DIR module-attribute

DEFAULT_ADR_DIR = 'docs/adr'

LEDGER_THEME module-attribute

LEDGER_THEME = Theme({'success': 'bold green', 'warning': 'bold yellow', 'error': 'bold red', 'info': 'bold blue', 'muted': 'dim', 'prompt': 'bold cyan', 'adr.index': 'cyan', 'adr.title': 'bold', 'adr.status': 'green', 'adr.date': 'dim'})

pass_ctx module-attribute

pass_ctx = make_pass_decorator(Context, ensure=True)

ADRDict

Bases: TypedDict

Dictionary representation of an ADR/MADR document.

Attributes:

date instance-attribute

date: str | None

index instance-attribute

index: int

path instance-attribute

path: str

status instance-attribute

status: str

title instance-attribute

title: str

Context dataclass

Context(directory: Path, out: ThemedConsole, err: Console)

Common context object for CLI commands containing shared parameters.

Attributes:

directory instance-attribute

directory: Path

err instance-attribute

err: Console

out instance-attribute

ThemedConsole

Bases: Console

Methods:

  • ask

    Ask with style

  • ask_multiple

    Ask user repeatedly for multiple values until empty input.

ask

ask(prompt: str, **kwargs) -> str

Ask with style

Source code in ledger/mkdocs/adr/cli.py
def ask(self, prompt: str, **kwargs) -> str:
    """Ask with style"""
    return Prompt.ask(f"[prompt]{prompt}[/prompt]", console=self, **kwargs)

ask_multiple

ask_multiple(question: str) -> list[str]

Ask user repeatedly for multiple values until empty input.

Source code in ledger/mkdocs/adr/cli.py
def ask_multiple(self, question: str) -> list[str]:
    """Ask user repeatedly for multiple values until empty input."""
    prompt = f"{question} [muted](empty to finish)[/]"
    values = []
    try:
        while response := self.ask(prompt):
            values.append(response)
    except (EOFError, KeyboardInterrupt):
        pass

    return values

adr_to_dict

adr_to_dict(doc: ADRDocument) -> ADRDict

Convert an ADRDocument to a normalized dictionary.

Parameters:

  • doc

    (ADRDocument) –

    ADRDocument to convert.

Returns: Normalized ADR dictionary with defaults.

Source code in ledger/mkdocs/adr/cli.py
def adr_to_dict(doc: ADRDocument) -> ADRDict:
    """
    Convert an ADRDocument to a normalized dictionary.

    Args:
        doc: ADRDocument to convert.

    Returns: Normalized ADR dictionary with defaults.
    """
    return {
        "index": doc.document_id or 0,
        "title": doc.title or "",
        "status": doc.status.lower() if doc.status else "proposed",
        "date": doc.date.strftime("%Y-%m-%d") if doc.date else None,
        "path": Path(doc.file_path).name,
    }

cli

cli(ctx: Context, directory: Path)

ADR/MADR management helpers

Source code in ledger/mkdocs/adr/cli.py
@click.group()
@click.option(
    "--directory",
    default=DEFAULT_ADR_DIR,
    help="Directory path for ADRs",
    type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path),
)
@click.pass_context
def cli(ctx: click.Context, directory: Path):
    """ADR/MADR management helpers"""
    ctx.obj = Context(
        directory=directory,
        out=ThemedConsole(theme=LEDGER_THEME),
        err=Console(stderr=True, theme=LEDGER_THEME),
    )

get_jinja_env

get_jinja_env() -> Environment

Get a configured Jinja2 environment.

Source code in ledger/mkdocs/adr/cli.py
def get_jinja_env() -> Environment:
    """Get a configured Jinja2 environment."""
    return Environment(
        loader=PackageLoader("ledger.mkdocs.adr", "."),
        autoescape=select_autoescape([]),
    )

get_next_adr_index

get_next_adr_index(directory: Path) -> int

Get the next ADR/MADR index by scanning existing files.

Source code in ledger/mkdocs/adr/cli.py
def get_next_adr_index(directory: Path) -> int:
    """Get the next ADR/MADR index by scanning existing files."""
    ids = (adr.document_id for adr in scan_adrs(directory) if adr.document_id)
    return max(ids, default=0) + 1

list_adrs

list_adrs(ctx: Context, json: bool)

List ADR/MADR documents in a directory.

Source code in ledger/mkdocs/adr/cli.py
@cli.command("list")
@click.option("--json", is_flag=True, help="Output as JSON")
@pass_ctx
def list_adrs(ctx: Context, json: bool):
    """List ADR/MADR documents in a directory."""
    if not ctx.directory.exists():
        if not json:
            ctx.out.print(f"[warning]No ADRs found in {ctx.directory}[/warning]")
        else:
            ctx.out.print_json(data=[])
        return

    adr_list = scan_adrs(ctx.directory)

    if json:
        data = [adr_to_dict(doc) for doc in adr_list]
        ctx.out.print_json(data=data)
        return

    if not adr_list:
        ctx.out.print(f"[warning]No ADRs found in {ctx.directory}[/warning]")
        return

    table = Table(title=f"ADRs in {ctx.directory}")
    table.add_column("Index", style="adr.index", no_wrap=True)
    table.add_column("Title", style="adr.title")
    table.add_column("Status", style="adr.status")
    table.add_column("Date", style="adr.date")

    for doc in adr_list:
        row = adr_to_dict(doc)
        date_display = row["date"] if row["date"] else "-"
        table.add_row(f"{row['index']:04d}", row["title"], row["status"], date_display)

    ctx.out.print(table)

new_adr

new_adr(ctx: Context, title: str | None, status: str, decider: tuple[str, ...], consulted: tuple[str, ...], informed: tuple[str, ...])

Create a new ADR/MADR document.

Source code in ledger/mkdocs/adr/cli.py
@cli.command("new")
@click.option("--title", help="ADR title")
@click.option(
    "--status",
    type=click.Choice(ALLOWED_STATUSES),
    default="proposed",
    help="ADR status",
)
@click.option("--decider", "-d", multiple=True, help="Decider (repeatable)")
@click.option("--consulted", "-C", multiple=True, help="People consulted (repeatable)")
@click.option("--informed", "-I", multiple=True, help="People informed (repeatable)")
@pass_ctx
def new_adr(
    ctx: Context,
    title: str | None,
    status: str,
    decider: tuple[str, ...],
    consulted: tuple[str, ...],
    informed: tuple[str, ...],
):
    """Create a new ADR/MADR document."""
    ctx.directory.mkdir(parents=True, exist_ok=True)

    data = {
        "title": title or ctx.out.ask("Title"),
        "status": status,
        "date": date.today(),
        "deciders": list(decider) or ctx.out.ask_multiple("Decision makers"),
        "consulted": list(consulted) or ctx.out.ask_multiple("People consulted"),
        "informed": list(informed) or ctx.out.ask_multiple("People informed"),
    }

    if not (title := cast(str, data["title"])):
        ctx.err.print("[error]Error: Title is required[/error]")
        raise click.Abort()

    index = get_next_adr_index(ctx.directory)
    slug = slugify(title)
    filename = f"{index:04d}-{slug}.md"
    file_path = ctx.directory / filename

    env = get_jinja_env()
    template = env.get_template("template.md.j2")

    content = template.render(**data)

    file_path.write_text(content, encoding="utf-8")

    ctx.out.print(f"[success]✓ Created ADR: {file_path}[/success]")
    return str(file_path)

slugify

slugify(title: str) -> str

Convert title to a URL-friendly slug.

Source code in ledger/mkdocs/adr/cli.py
def slugify(title: str) -> str:
    """Convert title to a URL-friendly slug."""
    slug = re.sub(r"[^a-zA-Z0-9\s-]", "", title.lower())
    slug = re.sub(r"\s+", "-", slug)
    slug = re.sub(r"-+", "-", slug)
    return slug.strip("-")