github-agent-cli 1.2.1__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.
- ghagent.py +511 -0
- ghalib/__init__.py +5 -0
- ghalib/assets.py +475 -0
- ghalib/audit.py +649 -0
- ghalib/changelog.py +473 -0
- ghalib/config.py +52 -0
- ghalib/install.py +189 -0
- ghalib/mdcheck.py +424 -0
- ghalib/meta.py +274 -0
- ghalib/report.py +90 -0
- ghalib/util.py +522 -0
- ghalib/wiki.py +512 -0
- github_agent_cli-1.2.1.dist-info/METADATA +431 -0
- github_agent_cli-1.2.1.dist-info/RECORD +17 -0
- github_agent_cli-1.2.1.dist-info/WHEEL +4 -0
- github_agent_cli-1.2.1.dist-info/entry_points.txt +2 -0
- github_agent_cli-1.2.1.dist-info/licenses/LICENSE +21 -0
ghagent.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ghagent — one command per documentation job, so an agent stops reading files by hand.
|
|
3
|
+
|
|
4
|
+
ghagent.py audit [path] [--repo owner/name] [--json] [--http] [--min-score 80]
|
|
5
|
+
ghagent.py doc [paths...] [--root DIR] [--json] [--ignore RULE,...] [--no-privacy]
|
|
6
|
+
ghagent.py wiki build|verify|publish|init ...
|
|
7
|
+
ghagent.py changelog check|release|suggest|notes ...
|
|
8
|
+
ghagent.py meta suggest|apply|check ...
|
|
9
|
+
ghagent.py privacy [path]
|
|
10
|
+
ghagent.py install|verify-install|uninstall --target DIR [--write]
|
|
11
|
+
ghagent.py snippet
|
|
12
|
+
ghagent.py report [path] [--out FILE]
|
|
13
|
+
ghagent.py selftest
|
|
14
|
+
|
|
15
|
+
Exit codes: 0 clean, 1 findings at error level, 2 usage or environment problem.
|
|
16
|
+
Standard library only; no network except `audit --repo`, `meta`, and `wiki publish`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
HERE = Path(__file__).resolve().parent
|
|
27
|
+
if str(HERE) not in sys.path:
|
|
28
|
+
sys.path.insert(0, str(HERE))
|
|
29
|
+
|
|
30
|
+
from ghalib import assets as assets_module # noqa: E402
|
|
31
|
+
from ghalib import audit as audit_module # noqa: E402
|
|
32
|
+
from ghalib import changelog as changelog_module # noqa: E402
|
|
33
|
+
from ghalib import config as cfg # noqa: E402
|
|
34
|
+
from ghalib import install as install_module # noqa: E402
|
|
35
|
+
from ghalib import mdcheck # noqa: E402
|
|
36
|
+
from ghalib import meta as meta_module # noqa: E402
|
|
37
|
+
from ghalib import report as report_module # noqa: E402
|
|
38
|
+
from ghalib import wiki as wiki_module # noqa: E402
|
|
39
|
+
from ghalib.util import ( # noqa: E402
|
|
40
|
+
EXIT_FAIL,
|
|
41
|
+
EXIT_OK,
|
|
42
|
+
EXIT_USAGE,
|
|
43
|
+
PACK_NAME,
|
|
44
|
+
PACK_VERSION,
|
|
45
|
+
Report,
|
|
46
|
+
emit,
|
|
47
|
+
fail,
|
|
48
|
+
force_utf8_stdio,
|
|
49
|
+
read_text,
|
|
50
|
+
rel,
|
|
51
|
+
write_text,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# The source tree is the pack root: it holds tools/ghagent.py, tests/, skills/, reference/,
|
|
55
|
+
# docs/ and the wiki sources. An installed wheel carries the CLI and the library only, so a
|
|
56
|
+
# command that copies or verifies the pack has nothing to work with there and must say so
|
|
57
|
+
# instead of writing an empty manifest or printing a PASS it did not measure.
|
|
58
|
+
PACK_ROOT = HERE.parent
|
|
59
|
+
SOURCE_MARKER = PACK_ROOT / "tools" / "ghagent.py"
|
|
60
|
+
CLONE_URL = "https://github.com/paulmann/github-agent"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _needs_source_tree(command: str) -> int | None:
|
|
64
|
+
"""None when the pack root is present; exit 2 with an honest message when it is not."""
|
|
65
|
+
if SOURCE_MARKER.exists():
|
|
66
|
+
return None
|
|
67
|
+
print(f"ghagent: `{command}` needs the source tree: this installed copy has no "
|
|
68
|
+
f"tools/ghagent.py. Clone {CLONE_URL} and run "
|
|
69
|
+
f"`python tools/ghagent.py {command}` from the clone.", file=sys.stderr)
|
|
70
|
+
return EXIT_USAGE
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _add_json(parser: argparse.ArgumentParser) -> None:
|
|
74
|
+
parser.add_argument("--json", action="store_true", help="machine-readable output")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _add_ignore(parser: argparse.ArgumentParser) -> None:
|
|
78
|
+
parser.add_argument("--ignore", default="", help="comma-separated rule ids to skip")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_assets(args: argparse.Namespace) -> int:
|
|
82
|
+
root = Path(args.root)
|
|
83
|
+
if args.action == "check":
|
|
84
|
+
report = assets_module.check(root)
|
|
85
|
+
return emit(report, args.json)
|
|
86
|
+
if args.action == "suggest":
|
|
87
|
+
report = assets_module.suggest(root)
|
|
88
|
+
return emit(report, args.json)
|
|
89
|
+
if args.action == "render":
|
|
90
|
+
width, _, height = (args.size or "1280x640").partition("x")
|
|
91
|
+
try:
|
|
92
|
+
spec = assets_module.spec_from_repository(
|
|
93
|
+
root,
|
|
94
|
+
title=args.title or "",
|
|
95
|
+
tagline=args.tagline or "",
|
|
96
|
+
command=args.command or "",
|
|
97
|
+
chips=[chip.strip() for chip in args.chips.split(",") if chip.strip()] or None,
|
|
98
|
+
)
|
|
99
|
+
spec.width, spec.height = int(width), int(height)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return fail("--size must look like 1280x640")
|
|
102
|
+
report = assets_module.write_assets(root, out_dir=args.out_dir, spec=spec, write=args.write)
|
|
103
|
+
return emit(report, args.json)
|
|
104
|
+
return fail(f"unknown assets action: {args.action}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_audit(args: argparse.Namespace) -> int:
|
|
108
|
+
if args.repo:
|
|
109
|
+
config = cfg.load(Path(args.path))
|
|
110
|
+
result = audit_module.audit_remote(args.repo, config=config, check_http=args.http)
|
|
111
|
+
if args.json:
|
|
112
|
+
print(json.dumps(result.report.as_dict(), ensure_ascii=False, indent=2))
|
|
113
|
+
else:
|
|
114
|
+
percentage, scored, skipped = result.score()
|
|
115
|
+
print(audit_module.render_markdown(result))
|
|
116
|
+
print()
|
|
117
|
+
print(f"score: {percentage}% ({scored} scored, {skipped} skipped)")
|
|
118
|
+
if args.min_score and result.score()[0] < args.min_score:
|
|
119
|
+
print(f"ghagent: score {result.score()[0]}% is below --min-score {args.min_score}", file=sys.stderr)
|
|
120
|
+
return EXIT_FAIL
|
|
121
|
+
return audit_module.exit_code_for(result)
|
|
122
|
+
|
|
123
|
+
root = Path(args.path)
|
|
124
|
+
config = cfg.load(root)
|
|
125
|
+
result = audit_module.audit_local(root, config=config)
|
|
126
|
+
percentage, scored, skipped = result.score()
|
|
127
|
+
if args.json:
|
|
128
|
+
print(json.dumps(result.report.as_dict(), ensure_ascii=False, indent=2))
|
|
129
|
+
else:
|
|
130
|
+
print(audit_module.render_markdown(result))
|
|
131
|
+
print()
|
|
132
|
+
print(f"score: {percentage}% ({scored} scored, {skipped} skipped)")
|
|
133
|
+
if args.min_score and percentage < args.min_score:
|
|
134
|
+
print(f"ghagent: score {percentage}% is below --min-score {args.min_score}", file=sys.stderr)
|
|
135
|
+
return EXIT_FAIL
|
|
136
|
+
return audit_module.exit_code_for(result)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_doc(args: argparse.Namespace) -> int:
|
|
140
|
+
root = Path(args.root)
|
|
141
|
+
options = mdcheck.DocCheckOptions(
|
|
142
|
+
ignore={item.strip() for item in args.ignore.split(",") if item.strip()},
|
|
143
|
+
privacy=not args.no_privacy,
|
|
144
|
+
toc=not args.no_toc,
|
|
145
|
+
)
|
|
146
|
+
report = Report(title="ghagent doc", root=str(root))
|
|
147
|
+
checker = mdcheck.DocChecker(root, report, options)
|
|
148
|
+
# Wiki sources use wiki-namespace links (page names without .md); they are checked by
|
|
149
|
+
# `wiki build`/`wiki verify`, not as repository links. Opt in with --include-wiki.
|
|
150
|
+
skip_dirs = {".git", "node_modules", "vendor", ".ghagent", ".venv", "__pycache__"}
|
|
151
|
+
if not args.include_wiki:
|
|
152
|
+
skip_dirs.add("wiki")
|
|
153
|
+
targets = [Path(item) for item in args.paths] if args.paths else sorted(
|
|
154
|
+
path for path in root.rglob("*.md")
|
|
155
|
+
if not (set(path.parts) & skip_dirs)
|
|
156
|
+
)
|
|
157
|
+
if not targets:
|
|
158
|
+
return fail("no markdown files found to check")
|
|
159
|
+
for path in targets:
|
|
160
|
+
checker.check_file(path)
|
|
161
|
+
checker._report_coverage()
|
|
162
|
+
return emit(report, args.json)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_privacy(args: argparse.Namespace) -> int:
|
|
166
|
+
root = Path(args.root)
|
|
167
|
+
options = mdcheck.DocCheckOptions(privacy=True, toc=False, max_line_rule=False)
|
|
168
|
+
options.ignore = {"doc.bom", "doc.crlf", "doc.final-newline", "doc.tab-indent", "md.h1",
|
|
169
|
+
"md.heading-skip", "md.fence", "md.table", "md.alert", "md.image-alt",
|
|
170
|
+
"md.hard-break", "doc.line-length", "md.link-local", "md.toc", "md.link-anchor",
|
|
171
|
+
"agents.md-length"}
|
|
172
|
+
report = Report(title="ghagent privacy", root=str(root))
|
|
173
|
+
checker = mdcheck.DocChecker(root, report, options)
|
|
174
|
+
for path in mdcheck.iter_text_files(root):
|
|
175
|
+
checker.check_file(path)
|
|
176
|
+
checker._report_coverage()
|
|
177
|
+
return emit(report, args.json)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_wiki(args: argparse.Namespace) -> int:
|
|
181
|
+
root = Path(args.root)
|
|
182
|
+
config = cfg.load(root)
|
|
183
|
+
if args.action == "init":
|
|
184
|
+
report = wiki_module.scaffold(root, slug=args.repo or config.owner_repo, title=args.title,
|
|
185
|
+
wiki_dir=args.wiki_dir, from_docs=not args.no_docs)
|
|
186
|
+
return emit(report, args.json)
|
|
187
|
+
if args.action == "build":
|
|
188
|
+
report = wiki_module.build(root, wiki_dir=args.wiki_dir, manifest_path=args.manifest,
|
|
189
|
+
out_dir=Path(args.out) if args.out else None)
|
|
190
|
+
if not args.json and report.data.get("out"):
|
|
191
|
+
print(f"built {report.data.get('pages')} page(s) into {report.data['out']}")
|
|
192
|
+
return emit(report, args.json)
|
|
193
|
+
if args.action == "verify":
|
|
194
|
+
out = Path(args.out) if args.out else root / ".ghagent" / "wiki"
|
|
195
|
+
report = wiki_module.verify(out)
|
|
196
|
+
return emit(report, args.json)
|
|
197
|
+
if args.action == "publish":
|
|
198
|
+
slug = args.repo or config.owner_repo
|
|
199
|
+
if not slug:
|
|
200
|
+
return fail("publish needs --repo owner/name or repo in the wiki manifest")
|
|
201
|
+
build_dir = Path(args.out) if args.out else root / ".ghagent" / "wiki"
|
|
202
|
+
if not build_dir.exists():
|
|
203
|
+
return fail(f"no built wiki at {build_dir}; run `ghagent wiki build` first")
|
|
204
|
+
verify_report = wiki_module.verify(build_dir)
|
|
205
|
+
if verify_report.errors and not args.force:
|
|
206
|
+
print(verify_report.render())
|
|
207
|
+
print("ghagent: refusing to publish a wiki that fails verification (pass --force only if you accept broken links)",
|
|
208
|
+
file=sys.stderr)
|
|
209
|
+
return EXIT_FAIL
|
|
210
|
+
report = wiki_module.publish(
|
|
211
|
+
slug, build_dir, push=args.push, prune=args.prune, message=args.message or "",
|
|
212
|
+
author_name=args.author_name or "", author_email=args.author_email or "",
|
|
213
|
+
work_dir=Path(args.workdir) if args.workdir else None,
|
|
214
|
+
remote=args.remote_url or "",
|
|
215
|
+
)
|
|
216
|
+
return emit(report, args.json)
|
|
217
|
+
return fail(f"unknown wiki action: {args.action}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_changelog(args: argparse.Namespace) -> int:
|
|
221
|
+
root = Path(args.root)
|
|
222
|
+
config = cfg.load(root)
|
|
223
|
+
path = Path(args.file) if args.file else root / config.changelog
|
|
224
|
+
slug = args.repo or config.owner_repo
|
|
225
|
+
if args.action == "check":
|
|
226
|
+
report = changelog_module.check(path, slug=slug)
|
|
227
|
+
return emit(report, args.json)
|
|
228
|
+
if args.action == "release":
|
|
229
|
+
report = changelog_module.release(path, args.version, date=args.date or "", write=args.write,
|
|
230
|
+
slug=slug)
|
|
231
|
+
return emit(report, args.json)
|
|
232
|
+
if args.action == "suggest":
|
|
233
|
+
report = changelog_module.suggest(root, args.since, include_noise=args.include_noise)
|
|
234
|
+
if not args.json and report.data.get("markdown"):
|
|
235
|
+
print(report.render())
|
|
236
|
+
print()
|
|
237
|
+
print(report.data["markdown"])
|
|
238
|
+
return report.exit_code()
|
|
239
|
+
return emit(report, args.json)
|
|
240
|
+
if args.action == "notes":
|
|
241
|
+
report = changelog_module.notes(path, args.version)
|
|
242
|
+
if not args.json and report.data.get("body"):
|
|
243
|
+
print(report.data["body"])
|
|
244
|
+
return report.exit_code()
|
|
245
|
+
return emit(report, args.json)
|
|
246
|
+
return fail(f"unknown changelog action: {args.action}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def cmd_meta(args: argparse.Namespace) -> int:
|
|
250
|
+
root = Path(args.root)
|
|
251
|
+
config = cfg.load(root)
|
|
252
|
+
if args.action == "suggest":
|
|
253
|
+
facts = meta_module.derive(root)
|
|
254
|
+
topics = meta_module.suggest_topics(facts)
|
|
255
|
+
report = Report(title="ghagent meta suggest", root=str(root))
|
|
256
|
+
for level, rule, message in meta_module.validate_description(facts.get("summary", "")):
|
|
257
|
+
report.add(level, rule, message)
|
|
258
|
+
for level, rule, message in meta_module.check_topics(topics, minimum=config.topic_min,
|
|
259
|
+
maximum=config.topic_max):
|
|
260
|
+
report.add(level, rule, message)
|
|
261
|
+
report.data.update({"facts": facts, "topics": topics,
|
|
262
|
+
"draft_description": _draft_description(facts)})
|
|
263
|
+
if args.json:
|
|
264
|
+
print(json.dumps(report.as_dict(), ensure_ascii=False, indent=2))
|
|
265
|
+
else:
|
|
266
|
+
print(report.render())
|
|
267
|
+
print()
|
|
268
|
+
print("facts:")
|
|
269
|
+
print(json.dumps(facts, ensure_ascii=False, indent=2))
|
|
270
|
+
print("candidate topics: " + ", ".join(topics))
|
|
271
|
+
print("draft description: " + _draft_description(facts))
|
|
272
|
+
return report.exit_code()
|
|
273
|
+
if args.action == "check":
|
|
274
|
+
report = Report(title="ghagent meta check")
|
|
275
|
+
for level, rule, message in meta_module.validate_description(args.description):
|
|
276
|
+
report.add(level, rule, message)
|
|
277
|
+
topics = [item.strip() for item in args.topics.split(",") if item.strip()]
|
|
278
|
+
for level, rule, message in meta_module.check_topics(topics, minimum=config.topic_min,
|
|
279
|
+
maximum=config.topic_max):
|
|
280
|
+
report.add(level, rule, message)
|
|
281
|
+
return emit(report, args.json)
|
|
282
|
+
if args.action == "apply":
|
|
283
|
+
slug = args.repo or config.owner_repo
|
|
284
|
+
if not slug:
|
|
285
|
+
return fail("meta apply needs --repo owner/name or owner_repo in .ghagent.json")
|
|
286
|
+
topics = [item.strip() for item in args.topics.split(",") if item.strip()] if args.topics else None
|
|
287
|
+
report = meta_module.apply_metadata(
|
|
288
|
+
slug,
|
|
289
|
+
description=args.description,
|
|
290
|
+
topics=topics,
|
|
291
|
+
homepage=args.homepage,
|
|
292
|
+
merge_topics=not args.replace_topics,
|
|
293
|
+
write=args.write,
|
|
294
|
+
)
|
|
295
|
+
return emit(report, args.json)
|
|
296
|
+
return fail(f"unknown meta action: {args.action}")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _draft_description(facts: dict) -> str:
|
|
300
|
+
title = facts.get("title") or "Project"
|
|
301
|
+
languages = ", ".join(language for language, _count in facts.get("languages", [])[:3])
|
|
302
|
+
summary = facts.get("summary", "").strip()
|
|
303
|
+
runtime = f" {languages.capitalize()}." if languages else ""
|
|
304
|
+
if summary:
|
|
305
|
+
return f"{title}: {summary[:120].rstrip('.')}.{runtime}"
|
|
306
|
+
return f"{title}:{runtime} <add one capability a reader can check>"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
310
|
+
blocked = _needs_source_tree("install")
|
|
311
|
+
if blocked is not None:
|
|
312
|
+
return blocked
|
|
313
|
+
target, error = install_module.resolve_target(args.profile, args.target)
|
|
314
|
+
if target is None:
|
|
315
|
+
return fail(error)
|
|
316
|
+
report = install_module.install(PACK_ROOT, target, write=args.write)
|
|
317
|
+
code = emit(report, args.json)
|
|
318
|
+
if args.write and not args.json:
|
|
319
|
+
print()
|
|
320
|
+
print(install_module.snippet(target))
|
|
321
|
+
return code
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def cmd_verify_install(args: argparse.Namespace) -> int:
|
|
325
|
+
blocked = _needs_source_tree("verify-install")
|
|
326
|
+
if blocked is not None:
|
|
327
|
+
return blocked
|
|
328
|
+
target, error = install_module.resolve_target(args.profile, args.target)
|
|
329
|
+
if target is None:
|
|
330
|
+
return fail(error)
|
|
331
|
+
report = install_module.verify(target)
|
|
332
|
+
return emit(report, args.json)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
336
|
+
blocked = _needs_source_tree("uninstall")
|
|
337
|
+
if blocked is not None:
|
|
338
|
+
return blocked
|
|
339
|
+
target, error = install_module.resolve_target(args.profile, args.target)
|
|
340
|
+
if target is None:
|
|
341
|
+
return fail(error)
|
|
342
|
+
report = install_module.remove(target, write=args.write)
|
|
343
|
+
return emit(report, args.json)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def cmd_snippet(args: argparse.Namespace) -> int:
|
|
347
|
+
blocked = _needs_source_tree("snippet")
|
|
348
|
+
if blocked is not None:
|
|
349
|
+
return blocked
|
|
350
|
+
target, error = install_module.resolve_target(args.profile, args.target)
|
|
351
|
+
print(install_module.snippet(target))
|
|
352
|
+
return EXIT_OK
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def cmd_report(args: argparse.Namespace) -> int:
|
|
356
|
+
root = Path(args.root)
|
|
357
|
+
text, code = report_module.build(root)
|
|
358
|
+
if args.out:
|
|
359
|
+
target = Path(args.out)
|
|
360
|
+
if not target.is_absolute():
|
|
361
|
+
target = root / target
|
|
362
|
+
write_text(target, text if text.endswith("\n") else text + "\n")
|
|
363
|
+
print(f"report written to {rel(target, root)}")
|
|
364
|
+
else:
|
|
365
|
+
print(text)
|
|
366
|
+
return code
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def cmd_selftest(args: argparse.Namespace) -> int:
|
|
370
|
+
import subprocess
|
|
371
|
+
runner = PACK_ROOT / "tests" / "run_tests.py"
|
|
372
|
+
if not runner.exists():
|
|
373
|
+
print("ghagent: selftest needs the source tree: tests/run_tests.py is not part of the "
|
|
374
|
+
f"installed package, so this copy cannot be self-tested. Clone {CLONE_URL} and run "
|
|
375
|
+
"`python tests/run_tests.py` from the clone.", file=sys.stderr)
|
|
376
|
+
return EXIT_USAGE
|
|
377
|
+
proc = subprocess.run([sys.executable, str(runner), "--quiet"], capture_output=True, text=True,
|
|
378
|
+
encoding="utf-8", errors="replace")
|
|
379
|
+
sys.stdout.write(proc.stdout)
|
|
380
|
+
sys.stderr.write(proc.stderr)
|
|
381
|
+
if proc.returncode != 0:
|
|
382
|
+
print("ghagent: selftest FAILED — the installed copy does not behave as tested", file=sys.stderr)
|
|
383
|
+
else:
|
|
384
|
+
print(f"ghagent {PACK_VERSION} selftest: PASS")
|
|
385
|
+
return proc.returncode
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
389
|
+
parser = argparse.ArgumentParser(prog="ghagent", description=f"{PACK_NAME} {PACK_VERSION} — repository and wiki steward")
|
|
390
|
+
parser.add_argument("--version", action="version", version=f"{PACK_NAME} {PACK_VERSION}")
|
|
391
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
392
|
+
|
|
393
|
+
audit_parser = subparsers.add_parser("audit", help="repository hygiene, README and changelog contracts")
|
|
394
|
+
audit_parser.add_argument("path", nargs="?", default=".")
|
|
395
|
+
audit_parser.add_argument("--repo", default="", help="owner/name: audit the remote metadata instead")
|
|
396
|
+
audit_parser.add_argument("--http", action="store_true", help="also check that the homepage answers")
|
|
397
|
+
audit_parser.add_argument("--min-score", type=float, default=0.0)
|
|
398
|
+
_add_json(audit_parser)
|
|
399
|
+
audit_parser.set_defaults(func=cmd_audit)
|
|
400
|
+
|
|
401
|
+
doc_parser = subparsers.add_parser("doc", help="markdown lint, links, anchors, tables, privacy")
|
|
402
|
+
doc_parser.add_argument("paths", nargs="*")
|
|
403
|
+
doc_parser.add_argument("--root", default=".")
|
|
404
|
+
doc_parser.add_argument("--no-privacy", action="store_true")
|
|
405
|
+
doc_parser.add_argument("--no-toc", action="store_true")
|
|
406
|
+
doc_parser.add_argument("--include-wiki", action="store_true", help="also lint the wiki source directory")
|
|
407
|
+
_add_ignore(doc_parser)
|
|
408
|
+
_add_json(doc_parser)
|
|
409
|
+
doc_parser.set_defaults(func=cmd_doc)
|
|
410
|
+
|
|
411
|
+
privacy_parser = subparsers.add_parser("privacy", help="secrets, absolute paths, personal addresses")
|
|
412
|
+
privacy_parser.add_argument("--root", default=".")
|
|
413
|
+
_add_json(privacy_parser)
|
|
414
|
+
privacy_parser.set_defaults(func=cmd_privacy)
|
|
415
|
+
|
|
416
|
+
assets_parser = subparsers.add_parser("assets", help="banner and social preview: render, check, suggest")
|
|
417
|
+
assets_parser.add_argument("action", choices=["render", "check", "suggest"])
|
|
418
|
+
assets_parser.add_argument("--root", default=".")
|
|
419
|
+
assets_parser.add_argument("--title", default="")
|
|
420
|
+
assets_parser.add_argument("--tagline", default="")
|
|
421
|
+
assets_parser.add_argument("--command", default="")
|
|
422
|
+
assets_parser.add_argument("--chips", default="")
|
|
423
|
+
assets_parser.add_argument("--size", default="1280x640")
|
|
424
|
+
assets_parser.add_argument("--out-dir", default="assets")
|
|
425
|
+
assets_parser.add_argument("--write", action="store_true")
|
|
426
|
+
_add_json(assets_parser)
|
|
427
|
+
assets_parser.set_defaults(func=cmd_assets)
|
|
428
|
+
|
|
429
|
+
wiki_parser = subparsers.add_parser("wiki", help="build, verify and publish the GitHub wiki")
|
|
430
|
+
wiki_parser.add_argument("action", choices=["init", "build", "verify", "publish"])
|
|
431
|
+
wiki_parser.add_argument("--root", default=".")
|
|
432
|
+
wiki_parser.add_argument("--repo", default="")
|
|
433
|
+
wiki_parser.add_argument("--title", default="Project")
|
|
434
|
+
wiki_parser.add_argument("--wiki-dir", default="wiki")
|
|
435
|
+
wiki_parser.add_argument("--manifest", default="wiki/wiki.json")
|
|
436
|
+
wiki_parser.add_argument("--out", default="")
|
|
437
|
+
wiki_parser.add_argument("--push", action="store_true", help="actually push to the wiki remote")
|
|
438
|
+
wiki_parser.add_argument("--prune", action="store_true", help="delete remote pages absent from the build")
|
|
439
|
+
wiki_parser.add_argument("--force", action="store_true", help="publish even if verification failed")
|
|
440
|
+
wiki_parser.add_argument("--no-docs", action="store_true")
|
|
441
|
+
wiki_parser.add_argument("--message", default="")
|
|
442
|
+
wiki_parser.add_argument("--author-name", default="")
|
|
443
|
+
wiki_parser.add_argument("--author-email", default="")
|
|
444
|
+
wiki_parser.add_argument("--workdir", default="")
|
|
445
|
+
wiki_parser.add_argument("--remote-url", default="", help="override the wiki remote (testing and mirrors)")
|
|
446
|
+
_add_json(wiki_parser)
|
|
447
|
+
wiki_parser.set_defaults(func=cmd_wiki)
|
|
448
|
+
|
|
449
|
+
changelog_parser = subparsers.add_parser("changelog", help="check, release and generate entries")
|
|
450
|
+
changelog_parser.add_argument("action", choices=["check", "release", "suggest", "notes"])
|
|
451
|
+
changelog_parser.add_argument("version", nargs="?", default="")
|
|
452
|
+
changelog_parser.add_argument("--root", default=".")
|
|
453
|
+
changelog_parser.add_argument("--file", default="")
|
|
454
|
+
changelog_parser.add_argument("--repo", default="")
|
|
455
|
+
changelog_parser.add_argument("--date", default="")
|
|
456
|
+
changelog_parser.add_argument("--since", default="")
|
|
457
|
+
changelog_parser.add_argument("--include-noise", action="store_true")
|
|
458
|
+
changelog_parser.add_argument("--write", action="store_true")
|
|
459
|
+
_add_json(changelog_parser)
|
|
460
|
+
changelog_parser.set_defaults(func=cmd_changelog)
|
|
461
|
+
|
|
462
|
+
meta_parser = subparsers.add_parser("meta", help="description, topics and homepage")
|
|
463
|
+
meta_parser.add_argument("action", choices=["suggest", "check", "apply"])
|
|
464
|
+
meta_parser.add_argument("--root", default=".")
|
|
465
|
+
meta_parser.add_argument("--repo", default="")
|
|
466
|
+
meta_parser.add_argument("--description", default=None)
|
|
467
|
+
meta_parser.add_argument("--topics", default="")
|
|
468
|
+
meta_parser.add_argument("--homepage", default=None)
|
|
469
|
+
meta_parser.add_argument("--replace-topics", action="store_true")
|
|
470
|
+
meta_parser.add_argument("--write", action="store_true")
|
|
471
|
+
_add_json(meta_parser)
|
|
472
|
+
meta_parser.set_defaults(func=cmd_meta)
|
|
473
|
+
|
|
474
|
+
for name, handler, help_text in [
|
|
475
|
+
("install", cmd_install, "copy the pack into an agent skill root"),
|
|
476
|
+
("verify-install", cmd_verify_install, "compare an installed copy with its manifest"),
|
|
477
|
+
("uninstall", cmd_uninstall, "remove an installed pack (needs its manifest)"),
|
|
478
|
+
("snippet", cmd_snippet, "print the AGENTS.md block that points an agent at this pack"),
|
|
479
|
+
]:
|
|
480
|
+
sub = subparsers.add_parser(name, help=help_text)
|
|
481
|
+
sub.add_argument("--profile", default="autoclaw", choices=["autoclaw", "harness", "saga"])
|
|
482
|
+
sub.add_argument("--target", default="")
|
|
483
|
+
sub.add_argument("--write", action="store_true")
|
|
484
|
+
_add_json(sub)
|
|
485
|
+
sub.set_defaults(func=handler)
|
|
486
|
+
|
|
487
|
+
report_parser = subparsers.add_parser("report", help="one markdown report from every check")
|
|
488
|
+
report_parser.add_argument("--root", default=".")
|
|
489
|
+
report_parser.add_argument("--out", default="")
|
|
490
|
+
report_parser.set_defaults(func=cmd_report)
|
|
491
|
+
|
|
492
|
+
selftest_parser = subparsers.add_parser("selftest", help="run the pack's own test suite")
|
|
493
|
+
selftest_parser.set_defaults(func=cmd_selftest)
|
|
494
|
+
return parser
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def main(argv: list[str] | None = None) -> int:
|
|
498
|
+
force_utf8_stdio()
|
|
499
|
+
parser = build_parser()
|
|
500
|
+
args = parser.parse_args(argv)
|
|
501
|
+
try:
|
|
502
|
+
return args.func(args)
|
|
503
|
+
except KeyboardInterrupt:
|
|
504
|
+
return EXIT_USAGE
|
|
505
|
+
except Exception as exc: # noqa: BLE001 - a traceback is a finding, but exit 1 must still be honest
|
|
506
|
+
print(f"ghagent: internal error: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
507
|
+
return EXIT_FAIL
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
if __name__ == "__main__":
|
|
511
|
+
sys.exit(main())
|