generated from coulomb/repo-seed
CLI and API polish
This commit is contained in:
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import yaml
|
||||
from click.shell_completion import get_completion_class
|
||||
|
||||
from markitect_tool.cache import (
|
||||
build_cache,
|
||||
@@ -43,7 +45,12 @@ from markitect_tool.document_function import (
|
||||
render_document_functions,
|
||||
validate_document_functions,
|
||||
)
|
||||
from markitect_tool.extension import ProcessingContext
|
||||
from markitect_tool.cli.extensions import collect_cli_command_specs
|
||||
from markitect_tool.extension import (
|
||||
ExtensionRegistryError,
|
||||
ProcessingContext,
|
||||
builtin_extension_registry,
|
||||
)
|
||||
from markitect_tool.explode import (
|
||||
ExplodeError,
|
||||
explode_markdown_file,
|
||||
@@ -107,6 +114,118 @@ def main() -> None:
|
||||
"""Markdown-native toolkit for structured knowledge artifacts."""
|
||||
|
||||
|
||||
@main.command("completion")
|
||||
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False))
|
||||
@click.option(
|
||||
"--program-name",
|
||||
default="mkt",
|
||||
show_default=True,
|
||||
help="Installed executable name used by the shell completion script.",
|
||||
)
|
||||
@click.option(
|
||||
"--instructions",
|
||||
is_flag=True,
|
||||
help="Print installation instructions instead of the completion script.",
|
||||
)
|
||||
def completion(shell: str, program_name: str, instructions: bool) -> None:
|
||||
"""Generate shell completion for Bash, Zsh, or Fish."""
|
||||
|
||||
normalized_shell = shell.lower()
|
||||
if instructions:
|
||||
click.echo(_completion_instructions(normalized_shell, program_name))
|
||||
return
|
||||
completion_class = get_completion_class(normalized_shell)
|
||||
if completion_class is None:
|
||||
raise click.ClickException(f"Unsupported completion shell `{shell}`")
|
||||
complete = completion_class(main, {}, program_name, "_MKT_COMPLETE")
|
||||
click.echo(complete.source())
|
||||
|
||||
|
||||
@main.group("extension")
|
||||
def extension_group() -> None:
|
||||
"""Inspect built-in extension descriptors and CLI affordances."""
|
||||
|
||||
|
||||
@extension_group.command("list")
|
||||
@click.option("--kind", help="Only show extensions of this kind.")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def extension_list(kind: str | None, output_format: str) -> None:
|
||||
"""List built-in extension descriptors."""
|
||||
|
||||
registry = builtin_extension_registry()
|
||||
extensions = [descriptor.to_dict() for descriptor in registry.list(kind=kind)]
|
||||
_emit_extension_catalog({"count": len(extensions), "extensions": extensions}, output_format)
|
||||
|
||||
|
||||
@extension_group.command("inspect")
|
||||
@click.argument("extension_id")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def extension_inspect(extension_id: str, output_format: str) -> None:
|
||||
"""Inspect one built-in extension descriptor."""
|
||||
|
||||
try:
|
||||
descriptor = builtin_extension_registry().get(extension_id)
|
||||
except ExtensionRegistryError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_extension_catalog({"extension": descriptor.to_dict()}, output_format)
|
||||
|
||||
|
||||
@extension_group.command("commands")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def extension_commands(output_format: str) -> None:
|
||||
"""List CLI commands declared by built-in extension descriptors."""
|
||||
|
||||
specs = [spec.to_dict() for spec in collect_cli_command_specs(builtin_extension_registry())]
|
||||
_emit_extension_catalog({"count": len(specs), "commands": specs}, output_format)
|
||||
|
||||
|
||||
@main.group("docs")
|
||||
def docs_group() -> None:
|
||||
"""Generate CLI and API reference documentation."""
|
||||
|
||||
|
||||
@docs_group.command("cli")
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write generated Markdown reference to this file.",
|
||||
)
|
||||
def docs_cli(output: Path | None) -> None:
|
||||
"""Generate a Markdown command reference from the Click command tree."""
|
||||
|
||||
_emit_or_write_text(_generate_cli_reference(), output)
|
||||
|
||||
|
||||
@docs_group.command("api")
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write generated Markdown reference to this file.",
|
||||
)
|
||||
def docs_api(output: Path | None) -> None:
|
||||
"""Generate a compact Markdown API reference from public exports."""
|
||||
|
||||
_emit_or_write_text(_generate_api_reference(), output)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
@@ -2743,6 +2862,36 @@ def _emit_tangle_result(data: dict, output_format: str) -> None:
|
||||
click.echo(f"written: {written}")
|
||||
|
||||
|
||||
def _emit_extension_catalog(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
elif "extension" in data:
|
||||
extension = data["extension"]
|
||||
click.echo(f"{extension['id']} ({extension['kind']})")
|
||||
if extension.get("summary"):
|
||||
click.echo(extension["summary"])
|
||||
commands = extension.get("cli", {}).get("commands", [])
|
||||
if commands:
|
||||
click.echo("commands:")
|
||||
for command in commands:
|
||||
click.echo(f"- {command}")
|
||||
docs = extension.get("docs", [])
|
||||
if docs:
|
||||
click.echo("docs:")
|
||||
for doc in docs:
|
||||
click.echo(f"- {doc}")
|
||||
elif "commands" in data:
|
||||
click.echo(f"command specs: {data['count']}")
|
||||
for spec in data["commands"]:
|
||||
click.echo(f"- {spec['command']} [{spec['extension_id']}]")
|
||||
else:
|
||||
click.echo(f"extensions: {data['count']}")
|
||||
for extension in data["extensions"]:
|
||||
click.echo(f"- {extension['id']} ({extension['kind']})")
|
||||
|
||||
|
||||
def _emit_jsonish(data: dict, output_format: str) -> None:
|
||||
if output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
@@ -2764,6 +2913,115 @@ def _emit_template_analysis(data: dict, output_format: str) -> None:
|
||||
click.echo(f"! {error}")
|
||||
|
||||
|
||||
def _emit_or_write_text(text: str, output: Path | None) -> None:
|
||||
if output:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(text, encoding="utf-8")
|
||||
else:
|
||||
click.echo(text)
|
||||
|
||||
|
||||
def _completion_instructions(shell: str, program_name: str) -> str:
|
||||
command = f"_MKT_COMPLETE={shell}_source {program_name}"
|
||||
if shell == "fish":
|
||||
target = f"~/.config/fish/completions/{program_name}.fish"
|
||||
return (
|
||||
f"Generate completion script:\n{command} > {target}\n\n"
|
||||
f"Or load it for the current shell:\n{command} | source"
|
||||
)
|
||||
target = f"~/.{program_name}-complete.{shell}"
|
||||
rc_file = "~/.bashrc" if shell == "bash" else "~/.zshrc"
|
||||
return (
|
||||
f"Generate completion script:\n{command} > {target}\n\n"
|
||||
f"Then add this to {rc_file}:\n. {target}"
|
||||
)
|
||||
|
||||
|
||||
def _generate_cli_reference() -> str:
|
||||
lines = [
|
||||
"# Markitect CLI Reference",
|
||||
"",
|
||||
"Generated from the Click command tree.",
|
||||
"",
|
||||
]
|
||||
_append_command_reference(lines, main, "mkt")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def _append_command_reference(lines: list[str], command: click.Command, path: str) -> None:
|
||||
lines.extend([f"## `{path}`", ""])
|
||||
if command.help:
|
||||
lines.extend([command.help, ""])
|
||||
with click.Context(command, info_name=path.split()[-1]) as ctx:
|
||||
usage = command.get_usage(ctx).replace("Usage: ", "", 1)
|
||||
lines.extend(["```text", usage, "```", ""])
|
||||
params = [
|
||||
param
|
||||
for param in command.params
|
||||
if not getattr(param, "hidden", False) and getattr(param, "name", None)
|
||||
]
|
||||
if params:
|
||||
lines.extend(["Parameters:", ""])
|
||||
for param in params:
|
||||
if isinstance(param, click.Option):
|
||||
names = ", ".join(param.opts + param.secondary_opts)
|
||||
else:
|
||||
names = param.human_readable_name
|
||||
help_text = getattr(param, "help", None) or ""
|
||||
if param.required:
|
||||
help_text = (help_text + " Required.").strip()
|
||||
lines.append(f"- `{names}` - {help_text}".rstrip())
|
||||
lines.append("")
|
||||
if isinstance(command, click.Group):
|
||||
for name, subcommand in sorted(command.commands.items()):
|
||||
_append_command_reference(lines, subcommand, f"{path} {name}")
|
||||
|
||||
|
||||
def _generate_api_reference() -> str:
|
||||
import markitect_tool as api
|
||||
|
||||
lines = [
|
||||
"# Markitect API Reference",
|
||||
"",
|
||||
"Generated from `markitect_tool.__all__`.",
|
||||
"",
|
||||
]
|
||||
grouped: dict[str, list[str]] = {}
|
||||
for name in sorted(api.__all__):
|
||||
obj = getattr(api, name, None)
|
||||
module = getattr(obj, "__module__", "markitect_tool")
|
||||
grouped.setdefault(module, []).append(name)
|
||||
for module in sorted(grouped):
|
||||
lines.extend([f"## `{module}`", ""])
|
||||
for name in grouped[module]:
|
||||
obj = getattr(api, name, None)
|
||||
lines.append(_api_reference_line(name, obj))
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def _api_reference_line(name: str, obj: object) -> str:
|
||||
kind = "object"
|
||||
if inspect.isclass(obj):
|
||||
kind = "class"
|
||||
elif inspect.isfunction(obj):
|
||||
kind = "function"
|
||||
elif inspect.isbuiltin(obj):
|
||||
kind = "builtin"
|
||||
signature = ""
|
||||
if callable(obj):
|
||||
try:
|
||||
signature = str(inspect.signature(obj))
|
||||
except (TypeError, ValueError):
|
||||
signature = ""
|
||||
summary = ""
|
||||
doc = inspect.getdoc(obj)
|
||||
if doc:
|
||||
summary = doc.splitlines()[0]
|
||||
display = f"`{name}{signature}`" if signature else f"`{name}`"
|
||||
return f"- {display} - {kind}. {summary}".rstrip()
|
||||
|
||||
|
||||
def _parse_key_value_options(items: tuple[str, ...]) -> dict[str, object]:
|
||||
values: dict[str, object] = {}
|
||||
for item in items:
|
||||
|
||||
Reference in New Issue
Block a user