robot-md 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robot_md/__init__.py +3 -0
- robot_md/__main__.py +133 -0
- robot_md/autodetect.py +584 -0
- robot_md/context.py +65 -0
- robot_md/parser.py +62 -0
- robot_md/render.py +17 -0
- robot_md/schemas/v1/robot.schema.json +250 -0
- robot_md/validate.py +101 -0
- robot_md-0.1.3.dist-info/METADATA +210 -0
- robot_md-0.1.3.dist-info/RECORD +12 -0
- robot_md-0.1.3.dist-info/WHEEL +4 -0
- robot_md-0.1.3.dist-info/entry_points.txt +2 -0
robot_md/__init__.py
ADDED
robot_md/__main__.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""robot-md CLI entry point — typer-based."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from robot_md import __version__
|
|
12
|
+
from robot_md.autodetect import emit_draft, scan_system
|
|
13
|
+
from robot_md.context import emit_context
|
|
14
|
+
from robot_md.parser import ParseError, parse_file
|
|
15
|
+
from robot_md.render import render_yaml
|
|
16
|
+
from robot_md.validate import (
|
|
17
|
+
FILE_ERROR,
|
|
18
|
+
MISSING_BODY_SECTION,
|
|
19
|
+
RCAN_CONFORMANCE_VIOLATION,
|
|
20
|
+
SCHEMA_VIOLATION,
|
|
21
|
+
VALID,
|
|
22
|
+
)
|
|
23
|
+
from robot_md.validate import (
|
|
24
|
+
validate as validate_parsed,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
name="robot-md",
|
|
29
|
+
help="Parse, validate, and render ROBOT.md files.",
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
add_completion=False,
|
|
32
|
+
)
|
|
33
|
+
err_console = Console(stderr=True)
|
|
34
|
+
out_console = Console()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
if value:
|
|
39
|
+
typer.echo(f"robot-md {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def main(
|
|
45
|
+
version: bool = typer.Option(
|
|
46
|
+
False,
|
|
47
|
+
"--version",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
help="Show the version and exit.",
|
|
51
|
+
),
|
|
52
|
+
) -> None:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def validate(path: Path = typer.Argument(..., help="Path to a ROBOT.md file.")) -> None:
|
|
58
|
+
"""Validate a ROBOT.md file against schema + body requirements."""
|
|
59
|
+
try:
|
|
60
|
+
parsed = parse_file(path)
|
|
61
|
+
except ParseError as e:
|
|
62
|
+
err_console.print(f"[red]✗[/red] {e}")
|
|
63
|
+
raise typer.Exit(code=FILE_ERROR) from None
|
|
64
|
+
|
|
65
|
+
result = validate_parsed(parsed)
|
|
66
|
+
if result.code == VALID:
|
|
67
|
+
out_console.print(f"[green]✓[/green] {result.summary}")
|
|
68
|
+
raise typer.Exit(code=VALID)
|
|
69
|
+
|
|
70
|
+
severity = {
|
|
71
|
+
SCHEMA_VIOLATION: ("schema violation", "red"),
|
|
72
|
+
RCAN_CONFORMANCE_VIOLATION: ("RCAN conformance violation", "red"),
|
|
73
|
+
MISSING_BODY_SECTION: ("missing body section", "yellow"),
|
|
74
|
+
}.get(result.code, (f"error (code {result.code})", "red"))
|
|
75
|
+
|
|
76
|
+
err_console.print(f"[{severity[1]}]✗ {severity[0]}[/{severity[1]}]")
|
|
77
|
+
for msg in result.errors:
|
|
78
|
+
err_console.print(f" - {msg}")
|
|
79
|
+
raise typer.Exit(code=result.code)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def render(path: Path = typer.Argument(..., help="Path to a ROBOT.md file.")) -> None:
|
|
84
|
+
"""Strip the prose body and emit the frontmatter as pure YAML to stdout."""
|
|
85
|
+
try:
|
|
86
|
+
parsed = parse_file(path)
|
|
87
|
+
except ParseError as e:
|
|
88
|
+
err_console.print(f"[red]✗[/red] {e}")
|
|
89
|
+
raise typer.Exit(code=FILE_ERROR) from None
|
|
90
|
+
sys.stdout.write(render_yaml(parsed))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def context(path: Path = typer.Argument(..., help="Path to a ROBOT.md file.")) -> None:
|
|
95
|
+
"""Emit a Claude-ready context block (markdown) to stdout."""
|
|
96
|
+
try:
|
|
97
|
+
parsed = parse_file(path)
|
|
98
|
+
except ParseError as e:
|
|
99
|
+
err_console.print(f"[red]✗[/red] {e}")
|
|
100
|
+
raise typer.Exit(code=FILE_ERROR) from None
|
|
101
|
+
sys.stdout.write(emit_context(parsed))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def autodetect(
|
|
106
|
+
write: Path | None = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--write",
|
|
109
|
+
"-w",
|
|
110
|
+
help="Write draft to this path (refuses to overwrite). If omitted, prints to stdout.",
|
|
111
|
+
),
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Scan visible hardware and emit a draft ROBOT.md.
|
|
114
|
+
|
|
115
|
+
Linux-only. Covers PCI (via lspci), USB (via lsusb), /dev/tty[ACM|USB]*,
|
|
116
|
+
and runtime info. Emitted draft has TODO markers for identity fields —
|
|
117
|
+
review before committing. Always run `robot-md validate` afterwards.
|
|
118
|
+
"""
|
|
119
|
+
scan = scan_system()
|
|
120
|
+
draft = emit_draft(scan)
|
|
121
|
+
if write is None:
|
|
122
|
+
sys.stdout.write(draft)
|
|
123
|
+
return
|
|
124
|
+
if write.exists():
|
|
125
|
+
err_console.print(f"[red]✗[/red] {write} already exists — refusing to overwrite")
|
|
126
|
+
raise typer.Exit(code=FILE_ERROR)
|
|
127
|
+
write.write_text(draft)
|
|
128
|
+
out_console.print(f"[green]✓[/green] wrote draft to {write}")
|
|
129
|
+
out_console.print(f" next: edit the TODOs, then `robot-md validate {write}`")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
app()
|