mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Module moves (with imports, paths, argv literals, scripts, docs, and
parents[N] formulas updated in lockstep):
- cli/deploy -> cli/administration/deploy
- cli/validate/inventory -> cli/administration/inventory/validate
- cli/vault -> cli/administration/secrets/vault
- cli/administration/inventory/encrypt -> cli/administration/secrets/seal
- cli/mirror -> cli/contributing/mirror
The new cli/administration/secrets cluster collocates the whole-file
ansible-vault wrapper next to the inventory-aware per-field sealer.
The user-facing CLI subcommand paths follow the package layout, e.g.
"infinito administration deploy dedicated <inv>" and
"infinito administration inventory validate <inv>".
Help system redesign in cli/core/{discovery,help,app}.py:
- Discovery now distinguishes runnable commands (directories with
__main__.py) from category folders (directories that only group
further commands). Empty support packages are filtered out.
- Each category folder MAY carry a README.md; its H1 heading is used
as the title and its first paragraph as the one-line description in
the help output.
- "infinito" / "infinito --help" prints only the top-level entries
(one level deep). "infinito <category>" navigates into a folder and
shows its sub-folders and commands one level deep, with the README
title as the page heading and the invocation as a dim subtitle.
- New "--tree [N]" flag walks the tree under the (optional) path
prefix recursively. N caps the depth (1-based); omit N for an
unbounded walk. The output uses Unicode tree connectors, an emoji
per entry (folder for categories, gear for commands), a globally
aligned description column, and multi-line wrapped descriptions
with tree-continuity bars on continuation lines.
- Category and command listings use the full invocation
("infinito meta callorder") rather than the bare leaf name, so the
output is copy-pasteable.
Convention doc + 17 CLI READMEs:
- New docs/contributing/artefact/files/cli/readme_md.md describes the
H1+paragraph layout the help system reads, and how it differs from
role/docs READMEs.
- README.md added for cli/{administration, build, contributing, meta}
and their immediate sub-folders, plus cli/meta/roles/applications
and its resolution sub-folder. Each follows the "title with trailing
emoji + single descriptive paragraph" convention.
Mirror resolver/sync description strings (the help system reads them
from argparse --help output) added so they no longer surface as
"options:" in the global index. cli/mirror/cleanup already had a
description and is unchanged.
Fixes uncovered by the move:
- import unittest.mock added to four dedicated-deploy tests so
unittest.mock.patch resolves under stdlib unittest discovery
(Python 3.13 no longer surfaces unittest.mock through "import
unittest" alone).
- parents[N] index in the new
cli/administration/inventory/validate/__init__.py corrected from
3 to 4 to match the deeper layout.
- test_paths.INVENTORY_VALIDATOR_PATH assertion and test_runner
substring assertions updated to match the new validator path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
import argparse
|
|
|
|
MODE_LINE_RE = re.compile(
|
|
r"""^\s*(?P<key>[A-Z0-9_]+)\s*:\s*(?P<value>.+?)\s*(?:#\s*(?P<cmt>.*))?\s*$"""
|
|
)
|
|
|
|
|
|
def _parse_bool_literal(text: str) -> bool | None:
|
|
"""Convert simple true/false/yes/no/on/off into boolean."""
|
|
t = text.strip().lower()
|
|
if t in ("true", "yes", "on"):
|
|
return True
|
|
if t in ("false", "no", "off"):
|
|
return False
|
|
return None
|
|
|
|
|
|
def load_modes_from_yaml(modes_yaml_path: str) -> list[dict[str, Any]]:
|
|
"""
|
|
Load MODE_* definitions from a YAML-like key/value file.
|
|
|
|
Expected lines:
|
|
MODE_FOO: true # comment
|
|
MODE_BAR: false # comment
|
|
MODE_BAZ: null # comment
|
|
"""
|
|
if not Path(modes_yaml_path).exists():
|
|
raise FileNotFoundError(f"Modes file not found: {modes_yaml_path}")
|
|
|
|
modes: list[dict[str, Any]] = []
|
|
|
|
with Path(modes_yaml_path).open(encoding="utf-8") as fh:
|
|
for raw in fh:
|
|
line = raw.rstrip()
|
|
if not line or line.lstrip().startswith("#"):
|
|
continue
|
|
|
|
m = MODE_LINE_RE.match(line)
|
|
if not m:
|
|
continue
|
|
|
|
key = m.group("key")
|
|
val = m.group("value").strip()
|
|
cmt = (m.group("cmt") or "").strip()
|
|
|
|
if not key.startswith("MODE_"):
|
|
continue
|
|
|
|
default_bool = _parse_bool_literal(val)
|
|
|
|
modes.append(
|
|
{
|
|
"name": key,
|
|
"default": default_bool, # True/False/None
|
|
"help": cmt or f"Toggle {key}",
|
|
}
|
|
)
|
|
|
|
return modes
|
|
|
|
|
|
def add_dynamic_mode_args(
|
|
parser: argparse.ArgumentParser, modes_meta: list[dict[str, Any]]
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""
|
|
Add command-line args dynamically based on MODE_* metadata.
|
|
|
|
Returns a spec mapping MODE_NAME -> info used by build_modes_from_args().
|
|
"""
|
|
spec: dict[str, dict[str, Any]] = {}
|
|
|
|
for meta in modes_meta:
|
|
name = meta["name"]
|
|
default = meta["default"]
|
|
desc = meta["help"]
|
|
short = name.replace("MODE_", "").lower()
|
|
|
|
if default is True:
|
|
# MODE_FOO: true → --skip-foo disables it
|
|
opt = f"--skip-{short}"
|
|
dest = f"skip_{short}"
|
|
parser.add_argument(opt, action="store_true", dest=dest, help=desc)
|
|
spec[name] = {"dest": dest, "default": True, "kind": "bool_true"}
|
|
|
|
elif default is False:
|
|
# MODE_BAR: false → --bar enables it
|
|
opt = f"--{short}"
|
|
dest = short
|
|
parser.add_argument(opt, action="store_true", dest=dest, help=desc)
|
|
spec[name] = {"dest": dest, "default": False, "kind": "bool_false"}
|
|
|
|
else:
|
|
# MODE_XYZ: null → --xyz true|false
|
|
opt = f"--{short}"
|
|
dest = short
|
|
parser.add_argument(opt, choices=["true", "false"], dest=dest, help=desc)
|
|
spec[name] = {"dest": dest, "default": None, "kind": "explicit"}
|
|
|
|
return spec
|
|
|
|
|
|
def build_modes_from_args(
|
|
spec: dict[str, dict[str, Any]], args_namespace: argparse.Namespace
|
|
) -> dict[str, Any]:
|
|
"""Resolve CLI arguments into a MODE_* dictionary."""
|
|
modes: dict[str, Any] = {}
|
|
|
|
for mode_name, info in spec.items():
|
|
dest = info["dest"]
|
|
kind = info["kind"]
|
|
value = getattr(args_namespace, dest, None)
|
|
|
|
if kind == "bool_true":
|
|
# if user passed --skip-foo => disable => False, otherwise True
|
|
modes[mode_name] = not value
|
|
|
|
elif kind == "bool_false":
|
|
# if user passed --bar => enable => True, otherwise False
|
|
modes[mode_name] = bool(value)
|
|
|
|
elif value is not None:
|
|
modes[mode_name] = value == "true"
|
|
|
|
return modes
|