agentfront 0.9.0__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.
- agentfront/__init__.py +13 -0
- agentfront/__main__.py +8 -0
- agentfront/_brand.py +28 -0
- agentfront/cite/__init__.py +19 -0
- agentfront/cite/_engine.py +181 -0
- agentfront/cite/references/python-cli/AGENT.md +98 -0
- agentfront/cite/references/python-cli/MANIFEST.json +65 -0
- agentfront/cite/references/python-cli/tests/test_cli.py +46 -0
- agentfront/cite/references/python-cli/{{slug}}/__init__.py +12 -0
- agentfront/cite/references/python-cli/{{slug}}/__main__.py +10 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/__init__.py +84 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/_commands/__init__.py +0 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/_commands/explain.py +38 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/_commands/learn.py +81 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/_errors.py +41 -0
- agentfront/cite/references/python-cli/{{slug}}/cli/_output.py +53 -0
- agentfront/cite/references/python-cli/{{slug}}/explain/__init__.py +24 -0
- agentfront/cite/references/python-cli/{{slug}}/explain/catalog.py +66 -0
- agentfront/cli/__init__.py +160 -0
- agentfront/cli/_commands/__init__.py +0 -0
- agentfront/cli/_commands/cli.py +198 -0
- agentfront/cli/_commands/doctor.py +564 -0
- agentfront/cli/_commands/explain.py +38 -0
- agentfront/cli/_commands/learn.py +130 -0
- agentfront/cli/_commands/overview.py +51 -0
- agentfront/cli/_errors.py +42 -0
- agentfront/cli/_output.py +58 -0
- agentfront/doctor/__init__.py +66 -0
- agentfront/doctor/_self_checks.py +347 -0
- agentfront/doctor/fixes.py +73 -0
- agentfront/explain/__init__.py +28 -0
- agentfront/explain/catalog.py +390 -0
- agentfront/overview/__init__.py +136 -0
- agentfront/overview/cli_surface.py +460 -0
- agentfront/rubric/__init__.py +53 -0
- agentfront/rubric/_runner.py +102 -0
- agentfront/rubric/_types.py +70 -0
- agentfront/rubric/checks/__init__.py +0 -0
- agentfront/rubric/checks/doctor.py +270 -0
- agentfront/rubric/checks/errors.py +107 -0
- agentfront/rubric/checks/explain_cmd.py +87 -0
- agentfront/rubric/checks/json_output.py +97 -0
- agentfront/rubric/checks/learnability.py +74 -0
- agentfront/rubric/checks/overview_cmd.py +181 -0
- agentfront/rubric/checks/structure.py +297 -0
- agentfront-0.9.0.dist-info/METADATA +93 -0
- agentfront-0.9.0.dist-info/RECORD +50 -0
- agentfront-0.9.0.dist-info/WHEEL +4 -0
- agentfront-0.9.0.dist-info/entry_points.txt +3 -0
- agentfront-0.9.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""``{{project_name}} learn`` — the learnability affordance (shape-adapt).
|
|
2
|
+
|
|
3
|
+
Update the text to describe {{project_name}} specifically. Must satisfy the
|
|
4
|
+
agent-first rubric: >=200 chars and mention purpose, command map, exit
|
|
5
|
+
codes, --json, explain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
from {{module}} import __version__
|
|
13
|
+
from {{module}}.cli._output import emit_result
|
|
14
|
+
|
|
15
|
+
_TEXT = """\
|
|
16
|
+
{{project_name}} — <one-line tagline>.
|
|
17
|
+
|
|
18
|
+
Purpose
|
|
19
|
+
-------
|
|
20
|
+
<Describe the tool's job. Write for an agent reader: concrete and terse.>
|
|
21
|
+
|
|
22
|
+
Commands
|
|
23
|
+
--------
|
|
24
|
+
{{project_name}} learn Print this self-teaching prompt. Supports --json.
|
|
25
|
+
{{project_name}} explain <path>... Print markdown docs for any noun/verb path.
|
|
26
|
+
Supports --json.
|
|
27
|
+
# Add your noun-verb entries here.
|
|
28
|
+
|
|
29
|
+
Machine-readable output
|
|
30
|
+
-----------------------
|
|
31
|
+
Every command that produces a listing or report supports --json. Errors in
|
|
32
|
+
JSON mode emit {"code", "message", "remediation"} to stderr. Stdout and
|
|
33
|
+
stderr are never mixed.
|
|
34
|
+
|
|
35
|
+
Exit-code policy
|
|
36
|
+
----------------
|
|
37
|
+
0 success
|
|
38
|
+
1 user-input error (bad flag, bad path, missing arg)
|
|
39
|
+
2 environment / setup error
|
|
40
|
+
3+ reserved
|
|
41
|
+
|
|
42
|
+
More detail
|
|
43
|
+
-----------
|
|
44
|
+
{{project_name}} explain {{project_name}}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _as_json_payload() -> dict[str, object]:
|
|
49
|
+
return {
|
|
50
|
+
"tool": "{{project_name}}",
|
|
51
|
+
"version": __version__,
|
|
52
|
+
"purpose": "<describe purpose>",
|
|
53
|
+
"commands": [
|
|
54
|
+
{"path": ["learn"], "summary": "Self-teaching prompt."},
|
|
55
|
+
{"path": ["explain"], "summary": "Markdown docs by path."},
|
|
56
|
+
],
|
|
57
|
+
"exit_codes": {
|
|
58
|
+
"0": "success",
|
|
59
|
+
"1": "user-input error",
|
|
60
|
+
"2": "environment/setup error",
|
|
61
|
+
},
|
|
62
|
+
"json_support": True,
|
|
63
|
+
"explain_pointer": "{{project_name}} explain <path>",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_learn(args: argparse.Namespace) -> int:
|
|
68
|
+
if getattr(args, "json", False):
|
|
69
|
+
emit_result(_as_json_payload(), json_mode=True)
|
|
70
|
+
else:
|
|
71
|
+
emit_result(_TEXT, json_mode=False)
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
76
|
+
p = sub.add_parser(
|
|
77
|
+
"learn",
|
|
78
|
+
help="Print a structured self-teaching prompt for agent consumers.",
|
|
79
|
+
)
|
|
80
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
81
|
+
p.set_defaults(func=cmd_learn)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""AfiError and exit-code policy (stable-contract — copy verbatim).
|
|
2
|
+
|
|
3
|
+
Every failure inside {{project_name}} raises :class:`AfiError`. The CLI entry
|
|
4
|
+
point catches it and exits with :attr:`AfiError.code`. Guarantees:
|
|
5
|
+
|
|
6
|
+
* no Python traceback leaks to stderr;
|
|
7
|
+
* every error has shape ``{code, message, remediation}``;
|
|
8
|
+
* the exit-code policy is centralised.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
# Exit-code policy (documented in ``{{project_name}} learn`` output).
|
|
16
|
+
# 0 = success
|
|
17
|
+
# 1 = user-input error (bad flag, bad path, missing arg)
|
|
18
|
+
# 2 = environment / setup error
|
|
19
|
+
# 3+ = reserved
|
|
20
|
+
EXIT_SUCCESS = 0
|
|
21
|
+
EXIT_USER_ERROR = 1
|
|
22
|
+
EXIT_ENV_ERROR = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AfiError(Exception):
|
|
27
|
+
"""Structured error with a remediation hint for agents."""
|
|
28
|
+
|
|
29
|
+
code: int
|
|
30
|
+
message: str
|
|
31
|
+
remediation: str = ""
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
super().__init__(self.message)
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, object]:
|
|
37
|
+
return {
|
|
38
|
+
"code": self.code,
|
|
39
|
+
"message": self.message,
|
|
40
|
+
"remediation": self.remediation,
|
|
41
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""stdout / stderr helpers with a strict split (stable-contract).
|
|
2
|
+
|
|
3
|
+
Rule: **results go to stdout, diagnostics and errors go to stderr.** Agents
|
|
4
|
+
parsing output can rely on this invariant. JSON mode routes structured
|
|
5
|
+
payloads to the same streams — never mixes them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, TextIO
|
|
13
|
+
|
|
14
|
+
from {{module}}.cli._errors import AfiError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_result(data: Any, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
18
|
+
"""Write a command result to stdout (or ``stream``)."""
|
|
19
|
+
s = stream if stream is not None else sys.stdout
|
|
20
|
+
if json_mode:
|
|
21
|
+
json.dump(data, s, ensure_ascii=False)
|
|
22
|
+
s.write("\n")
|
|
23
|
+
return
|
|
24
|
+
text = data if isinstance(data, str) else str(data)
|
|
25
|
+
s.write(text)
|
|
26
|
+
if not text.endswith("\n"):
|
|
27
|
+
s.write("\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def emit_error(err: AfiError, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
31
|
+
"""Write an :class:`AfiError` to stderr.
|
|
32
|
+
|
|
33
|
+
Text mode renders as two lines when a remediation is present::
|
|
34
|
+
|
|
35
|
+
error: <message>
|
|
36
|
+
hint: <remediation>
|
|
37
|
+
|
|
38
|
+
The ``hint:`` prefix is required by the agent-first error rubric.
|
|
39
|
+
"""
|
|
40
|
+
s = stream if stream is not None else sys.stderr
|
|
41
|
+
if json_mode:
|
|
42
|
+
json.dump(err.to_dict(), s, ensure_ascii=False)
|
|
43
|
+
s.write("\n")
|
|
44
|
+
return
|
|
45
|
+
s.write(f"error: {err.message}\n")
|
|
46
|
+
if err.remediation:
|
|
47
|
+
s.write(f"hint: {err.remediation}\n")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
|
|
51
|
+
"""Write a human diagnostic (progress, summary) to stderr."""
|
|
52
|
+
s = stream if stream is not None else sys.stderr
|
|
53
|
+
s.write(message if message.endswith("\n") else message + "\n")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Explain catalog — markdown keyed by command-path tuples (stable-contract).
|
|
2
|
+
|
|
3
|
+
Every noun/verb registered in the CLI should have a catalog entry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from {{module}}.cli._errors import EXIT_USER_ERROR, AfiError
|
|
9
|
+
from {{module}}.explain.catalog import ENTRIES
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve(path: tuple[str, ...]) -> str:
|
|
13
|
+
if path in ENTRIES:
|
|
14
|
+
return ENTRIES[path]
|
|
15
|
+
display = " ".join(path) if path else "<root>"
|
|
16
|
+
raise AfiError(
|
|
17
|
+
code=EXIT_USER_ERROR,
|
|
18
|
+
message=f"no explain entry for: {display}",
|
|
19
|
+
remediation="list known entries with: {{project_name}} explain {{project_name}}",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def known_paths() -> list[tuple[str, ...]]:
|
|
24
|
+
return list(ENTRIES.keys())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Markdown catalog for ``{{project_name}} explain <path>``.
|
|
2
|
+
|
|
3
|
+
Each entry is verbatim markdown. Keys are command-path tuples. The empty
|
|
4
|
+
tuple and ``("{{project_name}}",)`` both resolve to the root entry.
|
|
5
|
+
|
|
6
|
+
Keep bodies self-contained: an agent reading one entry should get enough
|
|
7
|
+
context without chaining reads.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
_ROOT = """\
|
|
13
|
+
# {{project_name}}
|
|
14
|
+
|
|
15
|
+
<One-paragraph description of {{project_name}} aimed at an agent reader.>
|
|
16
|
+
|
|
17
|
+
## Verbs
|
|
18
|
+
|
|
19
|
+
- `{{project_name}} learn` — structured self-teaching prompt.
|
|
20
|
+
- `{{project_name}} explain <path>` — markdown docs for any noun/verb.
|
|
21
|
+
|
|
22
|
+
## Exit-code policy
|
|
23
|
+
|
|
24
|
+
- `0` success
|
|
25
|
+
- `1` user-input error
|
|
26
|
+
- `2` environment / setup error
|
|
27
|
+
- `3+` reserved
|
|
28
|
+
|
|
29
|
+
## See also
|
|
30
|
+
|
|
31
|
+
- `{{project_name}} explain learn`
|
|
32
|
+
- `{{project_name}} explain explain`
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_LEARN = """\
|
|
36
|
+
# {{project_name}} learn
|
|
37
|
+
|
|
38
|
+
Prints a structured self-teaching prompt covering {{project_name}}'s purpose,
|
|
39
|
+
command map, exit-code policy, `--json` support, and `explain` pointer.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
{{project_name}} learn
|
|
44
|
+
{{project_name}} learn --json
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_EXPLAIN = """\
|
|
48
|
+
# {{project_name}} explain <path>
|
|
49
|
+
|
|
50
|
+
Prints markdown documentation for any noun/verb path. Unlike `--help`
|
|
51
|
+
(terse, positional), `explain` is global and addressable by path.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
{{project_name}} explain {{project_name}}
|
|
56
|
+
{{project_name}} explain learn
|
|
57
|
+
{{project_name}} explain --json <path>
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
ENTRIES: dict[tuple[str, ...], str] = {
|
|
62
|
+
(): _ROOT,
|
|
63
|
+
("{{project_name}}",): _ROOT,
|
|
64
|
+
("learn",): _LEARN,
|
|
65
|
+
("explain",): _EXPLAIN,
|
|
66
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Unified CLI entry point for agentfront.
|
|
2
|
+
|
|
3
|
+
Noun-based command groups and globals are registered here. Top-level globals
|
|
4
|
+
(``learn``, ``explain``) live under :mod:`agentfront.cli._commands`; per-noun groups
|
|
5
|
+
(``cli``, later ``mcp``, ``site``) are registered via their own ``register()``
|
|
6
|
+
functions following the same pattern.
|
|
7
|
+
|
|
8
|
+
Error propagation contract
|
|
9
|
+
--------------------------
|
|
10
|
+
Every handler raises :class:`agentfront.cli._errors.AfiError` on failure; the
|
|
11
|
+
top-level ``main()`` catches it via :func:`_dispatch` and routes through
|
|
12
|
+
:mod:`agentfront.cli._output`. Unknown exceptions are wrapped into an ``AfiError``
|
|
13
|
+
so no Python traceback leaks to stderr.
|
|
14
|
+
|
|
15
|
+
Argparse errors (unknown verb, missing required arg) also route through our
|
|
16
|
+
structured format — ``_AgentfrontArgumentParser`` overrides ``.error()``. The
|
|
17
|
+
subparsers are built with ``parser_class=_AgentfrontArgumentParser`` so subparser
|
|
18
|
+
errors follow the same path. Whether the error is emitted as text or JSON
|
|
19
|
+
depends on whether ``--json`` appears in the raw argv (:func:`main` sets
|
|
20
|
+
``_AgentfrontArgumentParser._json_hint`` before ``parse_args``).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from agentfront import __version__, _brand
|
|
29
|
+
from agentfront.cli._errors import EXIT_USER_ERROR, AfiError
|
|
30
|
+
from agentfront.cli._output import emit_error
|
|
31
|
+
|
|
32
|
+
# Note: _commands submodules are imported lazily inside :func:`_build_parser`
|
|
33
|
+
# to avoid a circular dependency. agentfront.cite._engine imports agentfront.cli._errors;
|
|
34
|
+
# eagerly loading agentfront.cli._commands.cli (which imports agentfront.cite) at module
|
|
35
|
+
# init would create a cycle when agentfront.cite is the first-touched package.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _AgentfrontArgumentParser(argparse.ArgumentParser):
|
|
39
|
+
"""ArgumentParser that routes errors through :func:`emit_error`.
|
|
40
|
+
|
|
41
|
+
Argparse's default error handler writes ``prog: error: <msg>`` to stderr
|
|
42
|
+
and exits with code 2. That skips our AfiError plumbing and — crucially —
|
|
43
|
+
produces no ``hint:`` line, which would make agentfront itself fail the rubric's
|
|
44
|
+
error-propagation bundle. This subclass emits our structured error format
|
|
45
|
+
instead and exits with :attr:`EXIT_USER_ERROR`.
|
|
46
|
+
|
|
47
|
+
JSON mode: parse-time errors happen before ``args.json`` is populated, so
|
|
48
|
+
we rely on a class-level ``_json_hint`` that :func:`main` pre-populates
|
|
49
|
+
by scanning the raw argv for ``--json`` / ``--json=…``. Best-effort and
|
|
50
|
+
shared across all subparser instances (argparse's subparser factory
|
|
51
|
+
produces instances of the class but doesn't thread state).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_json_hint: bool = False
|
|
55
|
+
|
|
56
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
57
|
+
err = AfiError(
|
|
58
|
+
code=EXIT_USER_ERROR,
|
|
59
|
+
message=message,
|
|
60
|
+
remediation=f"run '{self.prog} --help' to see valid arguments",
|
|
61
|
+
)
|
|
62
|
+
emit_error(err, json_mode=type(self)._json_hint)
|
|
63
|
+
raise SystemExit(err.code)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _argv_has_json(argv: list[str] | None) -> bool:
|
|
67
|
+
tokens = argv if argv is not None else sys.argv[1:]
|
|
68
|
+
return any(t == "--json" or t.startswith("--json=") for t in tokens)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
72
|
+
# Deferred imports (see module-level note): avoids agentfront.cite ↔ agentfront.cli cycle.
|
|
73
|
+
from agentfront.cli._commands import cli as _cli_group
|
|
74
|
+
from agentfront.cli._commands import doctor as _doctor_cmd
|
|
75
|
+
from agentfront.cli._commands import explain as _explain_cmd
|
|
76
|
+
from agentfront.cli._commands import learn as _learn_cmd
|
|
77
|
+
from agentfront.cli._commands import overview as _overview_cmd
|
|
78
|
+
|
|
79
|
+
parser = _AgentfrontArgumentParser(
|
|
80
|
+
prog=_brand.PROG,
|
|
81
|
+
description=f"{_brand.PROG} — Agent First Interface scaffolder",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--version",
|
|
85
|
+
action="version",
|
|
86
|
+
version=f"%(prog)s {__version__}",
|
|
87
|
+
)
|
|
88
|
+
# parser_class propagates to every subparser so their .error() routes
|
|
89
|
+
# through _AgentfrontArgumentParser too. Without this, `agentfront cli bogus --foo`
|
|
90
|
+
# would hit argparse's default error path (no hint: line, wrong code).
|
|
91
|
+
sub = parser.add_subparsers(dest="command", parser_class=_AgentfrontArgumentParser)
|
|
92
|
+
|
|
93
|
+
# Globals (top-level, not nested under a noun).
|
|
94
|
+
_learn_cmd.register(sub)
|
|
95
|
+
_explain_cmd.register(sub)
|
|
96
|
+
_overview_cmd.register(sub)
|
|
97
|
+
_doctor_cmd.register(sub)
|
|
98
|
+
|
|
99
|
+
# Noun groups.
|
|
100
|
+
_cli_group.register(sub)
|
|
101
|
+
# Future noun groups (mcp, site) will register here in v0.4 / v0.5.
|
|
102
|
+
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
107
|
+
"""Invoke the registered handler and translate exceptions to exit codes.
|
|
108
|
+
|
|
109
|
+
Handler protocol: a handler may return ``None`` (treated as success,
|
|
110
|
+
exit 0) or an ``int`` (used directly as the exit code). Failures MUST
|
|
111
|
+
raise :class:`AfiError`; any other exception is wrapped into one so no
|
|
112
|
+
Python traceback leaks.
|
|
113
|
+
"""
|
|
114
|
+
json_mode = bool(getattr(args, "json", False))
|
|
115
|
+
try:
|
|
116
|
+
rc = args.func(args)
|
|
117
|
+
except AfiError as err:
|
|
118
|
+
emit_error(err, json_mode=json_mode)
|
|
119
|
+
return err.code
|
|
120
|
+
except Exception as err: # noqa: BLE001 - last-resort; wrap and route cleanly
|
|
121
|
+
wrapped = AfiError(
|
|
122
|
+
code=EXIT_USER_ERROR,
|
|
123
|
+
message=f"unexpected: {err.__class__.__name__}: {err}",
|
|
124
|
+
remediation=f"file a bug at {_brand.ISSUES_URL}",
|
|
125
|
+
)
|
|
126
|
+
emit_error(wrapped, json_mode=json_mode)
|
|
127
|
+
return wrapped.code
|
|
128
|
+
return rc if rc is not None else 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main(argv: list[str] | None = None) -> int:
|
|
132
|
+
# Pre-parse peek so argparse-level errors honour --json.
|
|
133
|
+
_AgentfrontArgumentParser._json_hint = _argv_has_json(argv)
|
|
134
|
+
parser = _build_parser()
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
|
|
137
|
+
if args.command is None:
|
|
138
|
+
parser.print_help()
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
return _dispatch(args)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main_teken_alias(argv: list[str] | None = None) -> int:
|
|
145
|
+
"""Entry point for the deprecated ``teken`` command.
|
|
146
|
+
|
|
147
|
+
The project was renamed ``teken`` → ``agentfront``. The ``teken`` console script is
|
|
148
|
+
retained as an alias; it prints a one-line deprecation note to **stderr**
|
|
149
|
+
(never stdout, so ``--json`` output stays clean) and forwards to :func:`main`.
|
|
150
|
+
"""
|
|
151
|
+
print(
|
|
152
|
+
f"deprecated: the '{_brand.LEGACY_PROG}' command is now '{_brand.PROG}'; "
|
|
153
|
+
f"'{_brand.LEGACY_PROG}' will be removed in a future release.",
|
|
154
|
+
file=sys.stderr,
|
|
155
|
+
)
|
|
156
|
+
return main(argv)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""The ``cli`` noun group — verbs ``cite``, ``doctor``, ``overview``.
|
|
2
|
+
|
|
3
|
+
``agentfront cli cite`` — drop the agent-first CLI reference tree into the target
|
|
4
|
+
project under ``.agentfront/reference/<lang>-cli/``.
|
|
5
|
+
``agentfront cli doctor`` — run the seven-bundle rubric against a target CLI
|
|
6
|
+
and surface inconsistencies with actionable remediation
|
|
7
|
+
(replaces ``agentfront cli verify``; ``--fix`` applies any
|
|
8
|
+
auto-fixable handlers).
|
|
9
|
+
``agentfront cli overview`` — read-only descriptive snapshot of a target CLI.
|
|
10
|
+
|
|
11
|
+
``agentfront cli verify`` is kept as a deprecated alias that forwards to
|
|
12
|
+
``cli doctor`` for one minor cycle; removed in v0.6.0.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from agentfront.cite import SUPPORTED_LANGS, emit_reference
|
|
21
|
+
from agentfront.cli._commands.doctor import cmd_cli_doctor, cmd_cli_verify_deprecated
|
|
22
|
+
from agentfront.cli._output import emit_result
|
|
23
|
+
from agentfront.overview import build as build_overview
|
|
24
|
+
from agentfront.overview import to_json_dict, to_markdown
|
|
25
|
+
|
|
26
|
+
_JSON_HELP = "Emit structured JSON."
|
|
27
|
+
_PATH_HELP = "Target project path (default: .)."
|
|
28
|
+
_STRICT_HELP = "Treat warnings as failures (non-zero exit on any not-passed check)."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cmd_cite(args: argparse.Namespace) -> None:
|
|
32
|
+
"""Handler for ``agentfront cli cite``.
|
|
33
|
+
|
|
34
|
+
Returns ``None`` on success (treated as ``EXIT_SUCCESS`` by
|
|
35
|
+
:func:`agentfront.cli._dispatch`); every failure path raises
|
|
36
|
+
:class:`AfiError`.
|
|
37
|
+
"""
|
|
38
|
+
target_path = Path(args.path).resolve()
|
|
39
|
+
lang = args.lang
|
|
40
|
+
out = Path(args.out).resolve() if args.out else None
|
|
41
|
+
report = emit_reference(target_path, lang=lang, out=out)
|
|
42
|
+
json_mode = bool(getattr(args, "json", False))
|
|
43
|
+
if json_mode:
|
|
44
|
+
emit_result(report.to_dict(), json_mode=True)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
lines = [
|
|
48
|
+
f"Wrote {report.written_count} files to {report.out}",
|
|
49
|
+
(
|
|
50
|
+
"Added `.agentfront/` to .gitignore"
|
|
51
|
+
if report.gitignore_updated
|
|
52
|
+
else ".gitignore already ignores `.agentfront/`"
|
|
53
|
+
),
|
|
54
|
+
"",
|
|
55
|
+
"Next steps:",
|
|
56
|
+
]
|
|
57
|
+
for i, step in enumerate(report.describe_next_steps(), start=1):
|
|
58
|
+
lines.append(f" {i}. {step}")
|
|
59
|
+
lines.extend(
|
|
60
|
+
[
|
|
61
|
+
"",
|
|
62
|
+
"For details on any step: agentfront explain cli cite",
|
|
63
|
+
"For the rubric itself: agentfront explain cli doctor",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_cli_overview(args: argparse.Namespace) -> int:
|
|
70
|
+
"""Handler for ``agentfront cli overview [path]``.
|
|
71
|
+
|
|
72
|
+
Read-only descriptive snapshot. If ``path`` is missing or points at a
|
|
73
|
+
project without a detectable CLI surface, emits the overview of agentfront's
|
|
74
|
+
own scaffolded template (the "zero-target default").
|
|
75
|
+
"""
|
|
76
|
+
raw = getattr(args, "path", None)
|
|
77
|
+
path: Path | None = Path(raw).resolve() if raw else None
|
|
78
|
+
report = build_overview("cli", path)
|
|
79
|
+
json_mode = bool(getattr(args, "json", False))
|
|
80
|
+
if json_mode:
|
|
81
|
+
emit_result(to_json_dict(report), json_mode=True)
|
|
82
|
+
else:
|
|
83
|
+
emit_result(to_markdown(report), json_mode=False)
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
88
|
+
cli_parser = sub.add_parser(
|
|
89
|
+
"cli",
|
|
90
|
+
help="CLI-related commands: cite a reference tree, doctor against the rubric.",
|
|
91
|
+
)
|
|
92
|
+
cli_sub = cli_parser.add_subparsers(dest="cli_command")
|
|
93
|
+
|
|
94
|
+
cite = cli_sub.add_parser(
|
|
95
|
+
"cite",
|
|
96
|
+
help="Emit the agent-first CLI reference tree into <path>/.agentfront/reference/.",
|
|
97
|
+
)
|
|
98
|
+
cite.add_argument("path", nargs="?", default=".", help=_PATH_HELP)
|
|
99
|
+
cite.add_argument(
|
|
100
|
+
"--lang",
|
|
101
|
+
default="python",
|
|
102
|
+
choices=list(SUPPORTED_LANGS),
|
|
103
|
+
help="Reference language (default: python).",
|
|
104
|
+
)
|
|
105
|
+
cite.add_argument(
|
|
106
|
+
"--out",
|
|
107
|
+
default=None,
|
|
108
|
+
help="Override output directory (default: <path>/.agentfront/reference/<lang>-cli/).",
|
|
109
|
+
)
|
|
110
|
+
cite.add_argument("--json", action="store_true", help=_JSON_HELP)
|
|
111
|
+
cite.set_defaults(func=cmd_cite)
|
|
112
|
+
|
|
113
|
+
doctor = cli_sub.add_parser(
|
|
114
|
+
"doctor",
|
|
115
|
+
help=(
|
|
116
|
+
"Audit the CLI at <path> against the seven-bundle agent-first rubric "
|
|
117
|
+
"and surface remediations; --fix applies auto-fixable ones."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
# Default kept as None (not "."): the cwd fallback lives in cmd_cli_doctor
|
|
121
|
+
# so we can tell "no args" from "explicit path" and reject `--package`
|
|
122
|
+
# combined with an explicit path.
|
|
123
|
+
doctor.add_argument(
|
|
124
|
+
"path",
|
|
125
|
+
nargs="?",
|
|
126
|
+
default=None,
|
|
127
|
+
help=_PATH_HELP + " Omit and pass --package to audit by distribution name.",
|
|
128
|
+
)
|
|
129
|
+
doctor.add_argument(
|
|
130
|
+
"--package",
|
|
131
|
+
default=None,
|
|
132
|
+
metavar="NAME",
|
|
133
|
+
help=(
|
|
134
|
+
"Audit an editable-installed distribution by name (looks up "
|
|
135
|
+
"its source root via PEP 610 direct_url.json). Mutually "
|
|
136
|
+
"exclusive with the path positional."
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
doctor.add_argument("--json", action="store_true", help=_JSON_HELP)
|
|
140
|
+
# --fix and --dry-run are alternatives: --dry-run previews what --fix
|
|
141
|
+
# would do. Mutually exclusive at the argparse layer so passing both
|
|
142
|
+
# gives a clear error instead of silent precedence.
|
|
143
|
+
fix_group = doctor.add_mutually_exclusive_group()
|
|
144
|
+
fix_group.add_argument(
|
|
145
|
+
"--fix",
|
|
146
|
+
action="store_true",
|
|
147
|
+
help="Apply auto-fixable remediations.",
|
|
148
|
+
)
|
|
149
|
+
fix_group.add_argument(
|
|
150
|
+
"--dry-run",
|
|
151
|
+
action="store_true",
|
|
152
|
+
dest="dry_run",
|
|
153
|
+
help="Preview which fixes would be applied without mutating.",
|
|
154
|
+
)
|
|
155
|
+
doctor.add_argument(
|
|
156
|
+
"--strict",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help=_STRICT_HELP,
|
|
159
|
+
)
|
|
160
|
+
doctor.set_defaults(func=cmd_cli_doctor)
|
|
161
|
+
|
|
162
|
+
# Deprecated alias for one minor cycle. Mirror the v0.4 verify shape so
|
|
163
|
+
# CI and scripts that call `agentfront cli verify .` keep working; the deprecation
|
|
164
|
+
# diagnostic is emitted by cmd_cli_verify_deprecated to stderr.
|
|
165
|
+
verify = cli_sub.add_parser(
|
|
166
|
+
"verify",
|
|
167
|
+
help="(Deprecated) Alias for `agentfront cli doctor`. Removed in v0.6.0.",
|
|
168
|
+
)
|
|
169
|
+
verify.add_argument("path", nargs="?", default=".", help=_PATH_HELP)
|
|
170
|
+
verify.add_argument("--json", action="store_true", help=_JSON_HELP)
|
|
171
|
+
verify.add_argument(
|
|
172
|
+
"--strict",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help=_STRICT_HELP,
|
|
175
|
+
)
|
|
176
|
+
verify.set_defaults(func=cmd_cli_verify_deprecated)
|
|
177
|
+
|
|
178
|
+
overview = cli_sub.add_parser(
|
|
179
|
+
"overview",
|
|
180
|
+
help="Read-only descriptive snapshot of the CLI at <path> (defaults: agentfront template).",
|
|
181
|
+
)
|
|
182
|
+
overview.add_argument(
|
|
183
|
+
"path",
|
|
184
|
+
nargs="?",
|
|
185
|
+
default=None,
|
|
186
|
+
help=(
|
|
187
|
+
"Target project path. If omitted (or the target has no CLI surface), "
|
|
188
|
+
"describe agentfront's default scaffolded template."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
overview.add_argument("--json", action="store_true", help=_JSON_HELP)
|
|
192
|
+
overview.set_defaults(func=cmd_cli_overview)
|
|
193
|
+
|
|
194
|
+
def _no_verb(_args: argparse.Namespace) -> int:
|
|
195
|
+
cli_parser.print_help()
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
cli_parser.set_defaults(func=_no_verb)
|