Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 73 additions & 4 deletions src/agent_rules_kit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
from __future__ import annotations

import argparse
import json
import sys
from collections.abc import Sequence
from pathlib import Path

from agent_rules_kit import __version__
from agent_rules_kit.discovery import discover_instruction_files
from agent_rules_kit.discovery import InstructionFile, discover_instruction_files
from agent_rules_kit.redaction import redact_secret_like_values

OUTPUT_FORMATS = ("console", "json")


def build_parser() -> argparse.ArgumentParser:
Expand All @@ -35,6 +39,12 @@ def build_parser() -> argparse.ArgumentParser:
default=".",
help="Repository root to inspect. Defaults to the current directory.",
)
check_parser.add_argument(
"--format",
choices=OUTPUT_FORMATS,
default="console",
help="Output format. Defaults to console.",
)

return parser

Expand All @@ -49,19 +59,50 @@ def main(argv: Sequence[str] | None = None) -> int:
return 0

if args.command == "check":
return _run_check(Path(args.repository))
return _run_check(Path(args.repository), output_format=args.format)

parser.print_help()
return 0


def _run_check(repository_root: Path) -> int:
def _run_check(repository_root: Path, *, output_format: str = "console") -> int:
try:
instruction_files = discover_instruction_files(repository_root)
except ValueError as error:
print(f"ERROR: {error}", file=sys.stderr)
message = redact_secret_like_values(str(error))

if output_format == "json":
_print_json(
{
"command": "check",
"status": "error",
"repository": redact_secret_like_values(str(repository_root)),
"instruction_files": [],
"summary": {
"supported_instruction_file_count": 0,
},
"error": {
"message": message,
},
}
)
else:
print(f"ERROR: {message}", file=sys.stderr)

return 2

if output_format == "json":
status = "ok" if instruction_files else "no_instruction_files"
_print_json(_build_check_payload(repository_root, instruction_files, status=status))
return 0 if instruction_files else 1

return _print_console_check(repository_root, instruction_files)


def _print_console_check(
repository_root: Path,
instruction_files: tuple[InstructionFile, ...],
) -> int:
print(f"agent-rules-kit check: {repository_root}")

if not instruction_files:
Expand All @@ -75,5 +116,33 @@ def _run_check(repository_root: Path) -> int:
return 0


def _build_check_payload(
repository_root: Path,
instruction_files: tuple[InstructionFile, ...],
*,
status: str,
) -> dict[str, object]:
return {
"command": "check",
"status": status,
"repository": redact_secret_like_values(str(repository_root)),
"instruction_files": [
{
"path": redact_secret_like_values(instruction_file.path),
"kind": instruction_file.kind.value,
}
for instruction_file in instruction_files
],
"summary": {
"supported_instruction_file_count": len(instruction_files),
},
"error": None,
}


def _print_json(payload: dict[str, object]) -> None:
print(json.dumps(payload, indent=2, sort_keys=True))


if __name__ == "__main__":
raise SystemExit(main())
109 changes: 109 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import io
import json
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
Expand Down Expand Up @@ -68,6 +69,114 @@ def test_check_returns_two_for_invalid_repository_root(self) -> None:
self.assertEqual(exit_code, 2)
self.assertIn("ERROR: repository root does not exist:", output.getvalue())

def test_check_reports_discovered_instruction_files_as_json(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(
[
"check",
str(FIXTURE_ROOT / "multi-agent-overlap"),
"--format",
"json",
]
)

payload = json.loads(output.getvalue())

self.assertEqual(exit_code, 0)
self.assertEqual(payload["command"], "check")
self.assertEqual(payload["status"], "ok")
self.assertEqual(payload["error"], None)
self.assertEqual(payload["summary"]["supported_instruction_file_count"], 6)
self.assertEqual(
payload["instruction_files"],
[
{"path": "AGENTS.md", "kind": "agents"},
{"path": "CLAUDE.md", "kind": "claude"},
{"path": "GEMINI.md", "kind": "gemini"},
{
"path": ".github/copilot-instructions.md",
"kind": "copilot",
},
{
"path": ".cursor/rules/agent-rules.mdc",
"kind": "cursor-rule",
},
{
"path": ".github/instructions/agents.instructions.md",
"kind": "github-instruction",
},
],
)

def test_check_json_returns_one_when_no_instruction_files_are_found(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(
[
"check",
str(FIXTURE_ROOT / "empty-repo"),
"--format",
"json",
]
)

payload = json.loads(output.getvalue())

self.assertEqual(exit_code, 1)
self.assertEqual(payload["status"], "no_instruction_files")
self.assertEqual(payload["instruction_files"], [])
self.assertEqual(payload["summary"]["supported_instruction_file_count"], 0)
self.assertEqual(payload["error"], None)

def test_check_json_returns_two_for_invalid_repository_root(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(
[
"check",
str(FIXTURE_ROOT / "missing-repo"),
"--format",
"json",
]
)

payload = json.loads(output.getvalue())

self.assertEqual(exit_code, 2)
self.assertEqual(payload["status"], "error")
self.assertEqual(payload["instruction_files"], [])
self.assertEqual(payload["summary"]["supported_instruction_file_count"], 0)
self.assertIn(
"repository root does not exist:",
payload["error"]["message"],
)

def test_check_json_redacts_secret_like_repository_values(self) -> None:
output = io.StringIO()
secret_like_path = FIXTURE_ROOT / ("sk-" + ("A" * 24))

with redirect_stdout(output):
exit_code = main(
[
"check",
str(secret_like_path),
"--format",
"json",
]
)

payload = json.loads(output.getvalue())
text = output.getvalue()

self.assertEqual(exit_code, 2)
self.assertIn("[REDACTED]", text)
self.assertNotIn(secret_like_path.name, text)
self.assertEqual(payload["status"], "error")


if __name__ == "__main__":
unittest.main()
Loading