Skip to content

cli_types

cli_types

Shared vocabulary for the command registry — argument and command definitions.

Every per-subsystem command module in a sibling terok package imports from here. Out-of-tree consumers (terok, terok-executor, terok-shield, terok-clearance) build their CLI frontends against these types without depending on any handler implementation.

A CommandDef is either a leaf (handler set, children empty) or a group (handler is None, children holds subverbs). Groups can nest arbitrarily — vault passphrase seal is a leaf inside the passphrase group inside the vault group. A package's whole CLI is a forest of these, wrapped in a CommandTree for composition + argparse wiring.

__all__ = ['ArgDef', 'CommandDef', 'CommandTree', 'KeyRow'] module-attribute

ArgDef(name, help='', type=None, default=None, action=None, dest=None, nargs=None, required=False) dataclass

Definition of a single CLI argument.

name instance-attribute

help = '' class-attribute instance-attribute

type = None class-attribute instance-attribute

default = None class-attribute instance-attribute

action = None class-attribute instance-attribute

dest = None class-attribute instance-attribute

nargs = None class-attribute instance-attribute

required = False class-attribute instance-attribute

CommandDef(name, help='', handler=None, args=(), children=(), group='', epilog='', extras=dict()) dataclass

One node in a command tree — a leaf verb or a group of verbs.

Attributes:

Name Type Description
name str

Verb name as it appears on the CLI.

help str

One-line help string.

handler Callable[..., Any] | None

Callable implementing the verb. None for groups.

args tuple[ArgDef, ...]

Argument definitions parsed by argparse.

children tuple[CommandDef, ...]

Sub-verbs. Non-empty makes this node a group.

group str

Free-form tag used by per-subsystem grouping (unrelated to the children structural nesting).

epilog str

Optional long-form text rendered after the argparse argument list in --help output.

extras Mapping[str, Any]

Bag of package-specific metadata downstream consumers ignore (shield's needs_container / standalone_only would live here on a unified shape).

A frozen-dataclass + structural sharing is the load-bearing part of the wrap-once-share-everywhere story: when a consumer overlays a handler at one path, the modified CommandDef is referenced from every shortcut that also points at that path. Identity is what makes the overlay propagate.

name instance-attribute

help = '' class-attribute instance-attribute

handler = None class-attribute instance-attribute

args = () class-attribute instance-attribute

children = () class-attribute instance-attribute

group = '' class-attribute instance-attribute

epilog = '' class-attribute instance-attribute

extras = field(default_factory=dict) class-attribute instance-attribute

is_group property

Whether this node carries children (i.e. is a verb group).

with_handler(handler)

Return a copy with handler replaced — pure leaf-rewrap.

Source code in src/terok_util/cli_types.py
def with_handler(self, handler: Callable[..., Any]) -> CommandDef:
    """Return a copy with ``handler`` replaced — pure leaf-rewrap."""
    return replace(self, handler=handler)

with_children(children)

Return a copy with children replaced.

Source code in src/terok_util/cli_types.py
def with_children(self, children: tuple[CommandDef, ...]) -> CommandDef:
    """Return a copy with ``children`` replaced."""
    return replace(self, children=children)

KeyRow

Bases: NamedTuple

One registered SSH key, fully resolved for display and matching.

scope instance-attribute

comment instance-attribute

key_type instance-attribute

fingerprint instance-attribute

private_key instance-attribute

public_key instance-attribute

CommandTree(roots)

A forest of CommandDef nodes.

The unit of composition for CLI registries: each package exposes its own CommandTree; consumers walk it structurally, overlay handlers where they wrap a concept, extend with their own verbs, and wire the result into argparse.

Composition is identity-preserving — nodes the consumer doesn't touch share object identity with their pre-overlay counterparts, so a shortcut that splices the same subtree at the consumer's top level reaches the same modified handler. terok shield install and terok executor sandbox shield install resolving to the same wrap is a direct consequence.

Build a tree from an iterable of top-level verbs/groups.

Source code in src/terok_util/cli_types.py
def __init__(self, roots: Iterable[CommandDef]) -> None:
    """Build a tree from an iterable of top-level verbs/groups."""
    self._roots: tuple[CommandDef, ...] = tuple(roots)

roots property

The top-level verbs in this tree, in declaration order.

__iter__()

Yield each root verb.

Source code in src/terok_util/cli_types.py
def __iter__(self) -> Iterator[CommandDef]:
    """Yield each root verb."""
    return iter(self._roots)

__len__()

Number of root verbs.

Source code in src/terok_util/cli_types.py
def __len__(self) -> int:
    """Number of root verbs."""
    return len(self._roots)

__add__(other)

Concatenate forests — other's roots appended to this one's.

Source code in src/terok_util/cli_types.py
def __add__(self, other: CommandTree | Iterable[CommandDef]) -> CommandTree:
    """Concatenate forests — *other*'s roots appended to this one's."""
    other_roots = other.roots if isinstance(other, CommandTree) else tuple(other)
    return CommandTree(self._roots + other_roots)

find_at(path)

Return the CommandDef at path.

path is a sequence of verb names from the root. An empty path is rejected (no synthetic root). KeyError if any segment doesn't match a child name.

Source code in src/terok_util/cli_types.py
def find_at(self, path: Sequence[str]) -> CommandDef:
    """Return the [`CommandDef`][terok_util.cli_types.CommandDef] at *path*.

    *path* is a sequence of verb names from the root.  An empty
    path is rejected (no synthetic root).  ``KeyError`` if any
    segment doesn't match a child name.
    """
    if not path:
        raise KeyError("empty path; specify at least one verb name")
    first, *rest = path
    for root in self._roots:
        if root.name == first:
            return _descend(root, tuple(rest))
    raise KeyError(f"no top-level verb {first!r}")

overlay(overrides)

Return a new tree with handlers replaced at the named paths.

overrides maps verb-name tuples (e.g. ("vault", "status")) to replacement handlers. Each match produces one new CommandDef via replace; ancestors are likewise replaced because their children tuples now hold a new node, but unrelated siblings share identity with the input tree.

Sandbox-vocab paths use the operator-facing verb names — same names you'd type on the CLI — so the override map reads like a routing table.

Source code in src/terok_util/cli_types.py
def overlay(self, overrides: Mapping[tuple[str, ...], Callable[..., Any]]) -> CommandTree:
    """Return a new tree with handlers replaced at the named paths.

    *overrides* maps verb-name tuples (e.g. ``("vault", "status")``)
    to replacement handlers.  Each match produces one new
    [`CommandDef`][terok_util.cli_types.CommandDef] via ``replace``;
    ancestors are likewise replaced because their ``children``
    tuples now hold a new node, but unrelated siblings share
    identity with the input tree.

    Sandbox-vocab paths use the operator-facing verb names — same
    names you'd type on the CLI — so the override map reads like a
    routing table.
    """
    return CommandTree(_overlay_forest(self._roots, dict(overrides), ()))

extend_at(path, additions)

Return a new tree with additions appended at the path's children.

Empty path extends the top-level forest. Otherwise the CommandDef at path must be a group — a leaf (one with handler set and no children) cannot be extended; trying to do so would produce a hybrid node argparse can't represent (handler + subparsers on the same parser). Raises TypeError rather than silently inventing such a node.

Source code in src/terok_util/cli_types.py
def extend_at(self, path: Sequence[str], additions: Iterable[CommandDef]) -> CommandTree:
    """Return a new tree with *additions* appended at the path's children.

    Empty path extends the top-level forest.  Otherwise the
    [`CommandDef`][terok_util.cli_types.CommandDef] at *path*
    must be a **group** — a leaf (one with ``handler`` set and no
    ``children``) cannot be extended; trying to do so would produce
    a hybrid node argparse can't represent (handler + subparsers
    on the same parser).  Raises ``TypeError`` rather than
    silently inventing such a node.
    """
    addition_tuple = tuple(additions)
    if not path:
        return CommandTree(self._roots + addition_tuple)
    target = self.find_at(path)
    if target.handler is not None and not target.children:
        raise TypeError(
            f"cannot extend leaf {'.'.join(path)!r}: extend_at requires a group "
            f"(handler=None with children), but the target has a handler set"
        )
    return CommandTree(_extend_forest(self._roots, tuple(path), addition_tuple, ()))

walk()

Yield (path, command) for every node in the tree, depth-first.

Source code in src/terok_util/cli_types.py
def walk(self) -> Iterator[tuple[tuple[str, ...], CommandDef]]:
    """Yield ``(path, command)`` for every node in the tree, depth-first."""
    for root in self._roots:
        yield from _walk_node(root, ())

wire(target)

Wire this tree's verbs as subparsers under target, recursively.

target may be either an ArgumentParser (a fresh add_subparsers() action is created) or an existing argparse._SubParsersAction (the tree mounts straight under it; the private name has no public docs target). The second form lets a consumer mix legacy register-style subparsers with structural CommandTree ones under the same root parser without colliding on argparse's one-subparsers-per-parser rule.

The same CommandDef wired at multiple positions (deep nesting + shortcuts) yields independent argparse subparser instances, but each subparser's dispatch reads back the same handler object — so concept translations applied via overlay apply uniformly across every entry point that references the modified node.

Source code in src/terok_util/cli_types.py
def wire(self, target: argparse.ArgumentParser | argparse._SubParsersAction) -> None:
    """Wire this tree's verbs as subparsers under *target*, recursively.

    *target* may be either an
    [`ArgumentParser`][argparse.ArgumentParser] (a fresh
    ``add_subparsers()`` action is created) or an existing
    ``argparse._SubParsersAction`` (the tree mounts straight under
    it; the private name has no public docs target).  The second
    form lets a consumer mix legacy register-style subparsers
    with structural
    [`CommandTree`][terok_util.cli_types.CommandTree] ones under
    the same root parser without colliding on argparse's
    one-subparsers-per-parser rule.

    The same [`CommandDef`][terok_util.cli_types.CommandDef]
    wired at multiple positions (deep nesting + shortcuts) yields
    independent argparse subparser instances, but each subparser's
    dispatch reads back the same handler object — so concept
    translations applied via ``overlay`` apply uniformly across
    every entry point that references the modified node.
    """
    if isinstance(target, argparse._SubParsersAction):
        sub = target
    else:
        sub = target.add_subparsers()
    for cmd in self._roots:
        _wire_command(sub, cmd)

dispatch(args) staticmethod

Invoke the handler stored on args by CommandTree.wire.

Bridges argparse's parsed-args namespace to the handler kwargs the CommandDef declared. Async handlers are detected and run via asyncio.run so consumers don't need separate dispatch paths per handler flavour.

Source code in src/terok_util/cli_types.py
@staticmethod
def dispatch(args: argparse.Namespace) -> None:
    """Invoke the handler stored on *args* by [`CommandTree.wire`][terok_util.cli_types.CommandTree.wire].

    Bridges argparse's parsed-args namespace to the handler kwargs
    the [`CommandDef`][terok_util.cli_types.CommandDef] declared.
    Async handlers are detected and run via ``asyncio.run`` so
    consumers don't need separate dispatch paths per handler
    flavour.
    """
    cmd: CommandDef | None = getattr(args, "_cmd", None)
    if cmd is None:
        # Argparse stopped before a leaf command (bare ``terok`` /
        # ``terok shield`` etc.).  Argparse already printed help; exit
        # with the same "no command given" status it would.
        raise SystemExit(2)
    if cmd.handler is None:
        raise SystemExit(f"Command {cmd.name!r} has no handler")
    kwargs = {_arg_dest(arg): getattr(args, _arg_dest(arg), arg.default) for arg in cmd.args}
    # Trailing args after ``--`` (currently only ``run`` consumes them).
    if hasattr(args, "podman_args"):
        kwargs["podman_args"] = args.podman_args
    result = cmd.handler(**kwargs)
    if inspect.iscoroutine(result):
        import asyncio  # noqa: PLC0415

        asyncio.run(result)