pyrepowiki-cli 0.1.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.
- pyrepowiki_cli-0.1.0.dist-info/METADATA +875 -0
- pyrepowiki_cli-0.1.0.dist-info/RECORD +28 -0
- pyrepowiki_cli-0.1.0.dist-info/WHEEL +4 -0
- pyrepowiki_cli-0.1.0.dist-info/entry_points.txt +2 -0
- repowiki/__init__.py +3 -0
- repowiki/cli.py +43 -0
- repowiki/services/__init__.py +1 -0
- repowiki/services/codewiki/__init__.py +5 -0
- repowiki/services/codewiki/boq.py +57 -0
- repowiki/services/codewiki/cli.py +315 -0
- repowiki/services/codewiki/client.py +237 -0
- repowiki/services/codewiki/wiki.py +162 -0
- repowiki/services/deepwiki/__init__.py +5 -0
- repowiki/services/deepwiki/cli.py +851 -0
- repowiki/services/deepwiki/client.py +164 -0
- repowiki/services/deepwiki/codemap.py +74 -0
- repowiki/services/deepwiki/devin.py +355 -0
- repowiki/services/zread/__init__.py +5 -0
- repowiki/services/zread/cli.py +653 -0
- repowiki/services/zread/client.py +414 -0
- repowiki/services/zread/flight.py +116 -0
- repowiki/shared/__init__.py +1 -0
- repowiki/shared/async_.py +15 -0
- repowiki/shared/export.py +36 -0
- repowiki/shared/model.py +29 -0
- repowiki/shared/output.py +204 -0
- repowiki/shared/repo.py +45 -0
- repowiki/shared/save.py +27 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
repowiki/__init__.py,sha256=macC4owSJgzDJZuZoTRKZ3Uav8-4JtVBAT7n3i0zXHM,92
|
|
2
|
+
repowiki/cli.py,sha256=-WhNtcsIRdUOl9ZNUtjqD4QK_hPXcaSqeh0zFmImdbw,885
|
|
3
|
+
repowiki/services/__init__.py,sha256=yHeMArTg_nkffFPw7BsPj6TJJe7AzhQTWJA8jXpkN8o,50
|
|
4
|
+
repowiki/services/codewiki/__init__.py,sha256=AR_m_WhuNvzZCOGkYAs2JSBHS4TX78j3u9u7glMQzro,101
|
|
5
|
+
repowiki/services/codewiki/boq.py,sha256=1Td8GzN2vwb-5KTzVvgB6my0KEdlyN2VY9ySJoIVSSo,1997
|
|
6
|
+
repowiki/services/codewiki/cli.py,sha256=g0cb_8p-ZXOyDe5KT0M4trOhHq0n9Y-6hsXm50nGWC0,10691
|
|
7
|
+
repowiki/services/codewiki/client.py,sha256=GyMFmylMh8f9fu_7EzVfFT-kaZdT18QZlnk-ExF3qJc,7973
|
|
8
|
+
repowiki/services/codewiki/wiki.py,sha256=ZVikuguFPlOdvP8_m8Ii5wZ1WeLZHELhaSI9TZD4nJ0,5203
|
|
9
|
+
repowiki/services/deepwiki/__init__.py,sha256=I_6UP9UxVOZ5EYUcEpWSBwBLbkO79YszlvgI3c-mkK4,101
|
|
10
|
+
repowiki/services/deepwiki/cli.py,sha256=B6L-0V4YmOKIoECiYoHOyQZqcV91OVTatHyWyGiMLXc,29299
|
|
11
|
+
repowiki/services/deepwiki/client.py,sha256=6WsJR5G8WXJicBdPKOIBtzA016-cJ-7YdmCST_C9ETY,5782
|
|
12
|
+
repowiki/services/deepwiki/codemap.py,sha256=tOh0xP57JP4sNIsMs2JN543gc1cyxUYqKXHU-dLSUhw,2576
|
|
13
|
+
repowiki/services/deepwiki/devin.py,sha256=E7uPP2cy50-WjYr1rZTDBGko4uxCMNfm5VYYT6hvf-Y,13420
|
|
14
|
+
repowiki/services/zread/__init__.py,sha256=u-74WtrivibwhPN7h7q5fqR4f44MSLbP7S3s6E6MCrQ,147
|
|
15
|
+
repowiki/services/zread/cli.py,sha256=KVtTLmQE5LaOrH4xnuZFsLgEG2nnAEM5w9OsmT1wOTg,23664
|
|
16
|
+
repowiki/services/zread/client.py,sha256=majl4AxhmXJSgKR7geV-RpYufW4bKBMF8oBwvhhGItE,15701
|
|
17
|
+
repowiki/services/zread/flight.py,sha256=YlTXiu_b9m4erapXSSCq7x9_WAPPnyFmwEUs6PBEVLg,3261
|
|
18
|
+
repowiki/shared/__init__.py,sha256=gmmylKO0R13Zq83Jcyq2ZkwDI-ejalZwtYwmygELgbE,49
|
|
19
|
+
repowiki/shared/async_.py,sha256=NS6TDblljY_ebz5E3LAkMs7u7dXkPyPTpUsxod0uV0U,389
|
|
20
|
+
repowiki/shared/export.py,sha256=t-APTy3FxyjYKcVIiHFSeKWWBHs18ozI99XZv7FiRIw,1475
|
|
21
|
+
repowiki/shared/model.py,sha256=cq8xHQooxRbxn9AJtvnVWk8gKTc5Y7_fveWIPs0O8xc,600
|
|
22
|
+
repowiki/shared/output.py,sha256=3AFMi4iOcTkvVRjB06nuzWa8kia8LFSfkpWiRHYqRvw,7249
|
|
23
|
+
repowiki/shared/repo.py,sha256=vZww60m9zdN4FYbl1hTlXQlJeXbHUAnpr64069847q0,1498
|
|
24
|
+
repowiki/shared/save.py,sha256=u3TL5bMlmpVeNjPjt6TGXGfi59W7VYNB6RJ9ydfU9JU,879
|
|
25
|
+
pyrepowiki_cli-0.1.0.dist-info/METADATA,sha256=Zon4gJ_iMsLWSuPGoGvqzPG1N4Odap9mlSF5PquukKU,33006
|
|
26
|
+
pyrepowiki_cli-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
27
|
+
pyrepowiki_cli-0.1.0.dist-info/entry_points.txt,sha256=1ivpGqPt4xHlrgt8xBRpOBp90Dt7pjq9HJMKnzTg9pk,51
|
|
28
|
+
pyrepowiki_cli-0.1.0.dist-info/RECORD,,
|
repowiki/__init__.py
ADDED
repowiki/cli.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""CLI entry point for repowiki-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from repowiki import __version__
|
|
10
|
+
from repowiki.services.codewiki import register as register_codewiki
|
|
11
|
+
from repowiki.services.deepwiki import register as register_deepwiki
|
|
12
|
+
from repowiki.services.zread import register as register_zread
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(add_completion=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _version_callback(value: bool) -> None:
|
|
18
|
+
if value:
|
|
19
|
+
typer.echo(f"repowiki-cli {__version__}")
|
|
20
|
+
raise typer.Exit()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.callback()
|
|
24
|
+
def _main(
|
|
25
|
+
version: Optional[bool] = typer.Option(
|
|
26
|
+
None, "--version", callback=_version_callback, is_eager=True,
|
|
27
|
+
help="Show the version and exit.",
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
register_deepwiki(app)
|
|
34
|
+
register_codewiki(app)
|
|
35
|
+
register_zread(app)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
app()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Wiki services, one subpackage per provider."""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Boq batchexecute request/response framing for CodeWiki."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
_PREFIX = ")]}'"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encode_request(rpc_id: str, inner_json: str) -> str:
|
|
12
|
+
"""Return the ``f.req=...&`` form body for a batchexecute call."""
|
|
13
|
+
envelope = [[[rpc_id, inner_json, None, "generic"]]]
|
|
14
|
+
serialized = json.dumps(envelope)
|
|
15
|
+
return f"f.req={quote(serialized, safe='')}&"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decode_response(body: str, rpc_id: str) -> object:
|
|
19
|
+
"""Strip the ``)]}'`` prefix, skip length headers, and return the inner
|
|
20
|
+
payload of the ``wrb.fr`` frame whose id matches ``rpc_id``."""
|
|
21
|
+
trimmed = body.lstrip("")
|
|
22
|
+
if not trimmed.startswith(_PREFIX):
|
|
23
|
+
raise ValueError("response missing `)]}'` prefix")
|
|
24
|
+
cursor = trimmed[len(_PREFIX):]
|
|
25
|
+
|
|
26
|
+
decoder = json.JSONDecoder()
|
|
27
|
+
while True:
|
|
28
|
+
start = next((i for i, ch in enumerate(cursor) if ch in "[{"), None)
|
|
29
|
+
if start is None:
|
|
30
|
+
break
|
|
31
|
+
cursor = cursor[start:]
|
|
32
|
+
frame, consumed = decoder.raw_decode(cursor)
|
|
33
|
+
cursor = cursor[consumed:]
|
|
34
|
+
payload = _find_rpc_payload(frame, rpc_id)
|
|
35
|
+
if payload is not None:
|
|
36
|
+
return payload
|
|
37
|
+
|
|
38
|
+
raise ValueError(f"no `wrb.fr` frame found for rpc id `{rpc_id}`")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_rpc_payload(frames: object, rpc_id: str) -> object | None:
|
|
42
|
+
if not isinstance(frames, list):
|
|
43
|
+
raise ValueError("frame is not an array")
|
|
44
|
+
for frame in frames:
|
|
45
|
+
if not isinstance(frame, list):
|
|
46
|
+
continue
|
|
47
|
+
tag = frame[0] if frame and isinstance(frame[0], str) else ""
|
|
48
|
+
if tag != "wrb.fr":
|
|
49
|
+
continue
|
|
50
|
+
fid = frame[1] if len(frame) > 1 and isinstance(frame[1], str) else ""
|
|
51
|
+
if fid != rpc_id:
|
|
52
|
+
continue
|
|
53
|
+
inner = frame[2] if len(frame) > 2 and isinstance(frame[2], str) else None
|
|
54
|
+
if inner is None:
|
|
55
|
+
raise ValueError("`wrb.fr` frame missing inner JSON string")
|
|
56
|
+
return json.loads(inner)
|
|
57
|
+
return None
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""CLI commands for the CodeWiki service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import NoReturn, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from repowiki.services.codewiki.client import (
|
|
12
|
+
CodeWikiClient,
|
|
13
|
+
CodeWikiConnectionError,
|
|
14
|
+
CodeWikiError,
|
|
15
|
+
)
|
|
16
|
+
from repowiki.services.codewiki.wiki import render_markdown, render_page, render_section, render_structure
|
|
17
|
+
from repowiki.shared.async_ import run_async
|
|
18
|
+
from repowiki.shared.export import export_pages
|
|
19
|
+
from repowiki.shared.model import Answer
|
|
20
|
+
from repowiki.shared.output import (
|
|
21
|
+
filter_page,
|
|
22
|
+
format_answer,
|
|
23
|
+
format_error_json,
|
|
24
|
+
format_header,
|
|
25
|
+
format_json,
|
|
26
|
+
format_result,
|
|
27
|
+
render_markdown as render_rich,
|
|
28
|
+
status,
|
|
29
|
+
)
|
|
30
|
+
from repowiki.shared.repo import normalize_repo
|
|
31
|
+
from repowiki.shared.save import append_entry
|
|
32
|
+
|
|
33
|
+
codewiki_app = typer.Typer(add_completion=False, help="Query Google Code Wiki documentation.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def register(app: typer.Typer) -> None:
|
|
37
|
+
"""Mount the CodeWiki service commands on the root app."""
|
|
38
|
+
app.add_typer(codewiki_app, name="codewiki")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mock_text() -> Optional[str]:
|
|
42
|
+
return os.environ.get("REPOWIKI_CODEWIKI_MOCK") or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_repo(raw: str, json_mode: bool) -> str:
|
|
46
|
+
try:
|
|
47
|
+
return normalize_repo(raw)
|
|
48
|
+
except ValueError as exc:
|
|
49
|
+
_fail(str(exc), "invalid_repo", json_mode)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fail(message: str, kind: str, json_mode: bool) -> NoReturn:
|
|
53
|
+
if json_mode:
|
|
54
|
+
typer.echo(format_error_json(kind, message), err=True)
|
|
55
|
+
else:
|
|
56
|
+
typer.secho(f"Error: {message}", fg=typer.colors.RED, err=True)
|
|
57
|
+
raise typer.Exit(code={"connection": 3}.get(kind, 1))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _handle_exception(exc: Exception, json_mode: bool) -> NoReturn:
|
|
61
|
+
if isinstance(exc, CodeWikiConnectionError):
|
|
62
|
+
_fail(str(exc), "connection", json_mode)
|
|
63
|
+
elif isinstance(exc, CodeWikiError):
|
|
64
|
+
_fail(str(exc), "error", json_mode)
|
|
65
|
+
_fail(f"Unexpected error: {exc}", "unexpected", json_mode)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _emit(
|
|
69
|
+
repo: str,
|
|
70
|
+
command: str,
|
|
71
|
+
text: str,
|
|
72
|
+
rich: bool,
|
|
73
|
+
json_mode: bool,
|
|
74
|
+
**json_fields: object,
|
|
75
|
+
) -> None:
|
|
76
|
+
if json_mode:
|
|
77
|
+
typer.echo(format_json(repo, command, **json_fields))
|
|
78
|
+
return
|
|
79
|
+
rendered = format_result("CodeWiki", repo, command, text)
|
|
80
|
+
if rich:
|
|
81
|
+
render_rich(rendered)
|
|
82
|
+
else:
|
|
83
|
+
typer.echo(rendered)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _repl_prompt() -> str:
|
|
87
|
+
"""Return the REPL input prompt, colored only on a terminal."""
|
|
88
|
+
if not sys.stdout.isatty():
|
|
89
|
+
return ">> "
|
|
90
|
+
return typer.style(">> ", fg=typer.colors.CYAN, bold=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_error(exc: Exception) -> None:
|
|
94
|
+
if isinstance(exc, CodeWikiError):
|
|
95
|
+
message = str(exc)
|
|
96
|
+
else:
|
|
97
|
+
message = f"Unexpected error: {exc}"
|
|
98
|
+
typer.secho(f"Error: {message}", fg=typer.colors.RED, err=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _append_save(
|
|
102
|
+
path: Optional[str], repo: str, question: str, answer: str
|
|
103
|
+
) -> None:
|
|
104
|
+
if path is None:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
append_entry(path, repo, question, answer)
|
|
108
|
+
except OSError as exc:
|
|
109
|
+
typer.secho(
|
|
110
|
+
f"Warning: could not save to {path}: {exc}",
|
|
111
|
+
fg=typer.colors.YELLOW,
|
|
112
|
+
err=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _repl(resolved: str, rich: bool, save_path: Optional[str]) -> None:
|
|
117
|
+
prompt = _repl_prompt()
|
|
118
|
+
client = CodeWikiClient()
|
|
119
|
+
while True:
|
|
120
|
+
try:
|
|
121
|
+
line = input(prompt)
|
|
122
|
+
except (EOFError, KeyboardInterrupt):
|
|
123
|
+
typer.echo()
|
|
124
|
+
break
|
|
125
|
+
q = line.strip()
|
|
126
|
+
if not q:
|
|
127
|
+
continue
|
|
128
|
+
if q in ("/exit", "/quit", "/q"):
|
|
129
|
+
break
|
|
130
|
+
try:
|
|
131
|
+
with status("Thinking..."):
|
|
132
|
+
answer = await client.ask(resolved, q)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
_print_error(exc)
|
|
135
|
+
continue
|
|
136
|
+
_append_save(save_path, resolved, q, answer)
|
|
137
|
+
typer.echo()
|
|
138
|
+
if rich:
|
|
139
|
+
render_rich(answer.strip())
|
|
140
|
+
else:
|
|
141
|
+
typer.echo(answer.strip())
|
|
142
|
+
typer.echo()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@codewiki_app.command()
|
|
146
|
+
def structure(
|
|
147
|
+
repo: str = typer.Argument(..., help="Repository (owner/repo or GitHub URL)"),
|
|
148
|
+
json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Show the CodeWiki table of contents for a repository."""
|
|
151
|
+
resolved = _resolve_repo(repo, json)
|
|
152
|
+
if (mock := _mock_text()) is not None:
|
|
153
|
+
_emit(resolved, "structure", mock, False, json, content=mock)
|
|
154
|
+
return
|
|
155
|
+
client = CodeWikiClient()
|
|
156
|
+
try:
|
|
157
|
+
with status("Fetching table of contents..."):
|
|
158
|
+
wiki = run_async(client.read_wiki(resolved))
|
|
159
|
+
text = render_structure(wiki)
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
_handle_exception(exc, json)
|
|
162
|
+
_emit(resolved, "structure", text, False, json, content=text)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@codewiki_app.command()
|
|
166
|
+
def contents(
|
|
167
|
+
repo: str = typer.Argument(..., help="Repository (owner/repo or GitHub URL)"),
|
|
168
|
+
page: Optional[str] = typer.Option(None, "--page", help="Show only the page with this title"),
|
|
169
|
+
rich: bool = typer.Option(False, "--rich", help="Render Markdown with rich"),
|
|
170
|
+
json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Show the full CodeWiki documentation for a repository."""
|
|
173
|
+
resolved = _resolve_repo(repo, json)
|
|
174
|
+
if (mock := _mock_text()) is not None:
|
|
175
|
+
text = mock
|
|
176
|
+
if page is not None:
|
|
177
|
+
try:
|
|
178
|
+
text = filter_page(text, page)
|
|
179
|
+
except ValueError as exc:
|
|
180
|
+
_fail(str(exc), "page_not_found", json)
|
|
181
|
+
else:
|
|
182
|
+
client = CodeWikiClient()
|
|
183
|
+
try:
|
|
184
|
+
with status("Fetching documentation..."):
|
|
185
|
+
wiki = run_async(client.read_wiki(resolved))
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
_handle_exception(exc, json)
|
|
188
|
+
try:
|
|
189
|
+
text = render_page(wiki, page) if page is not None else render_markdown(wiki)
|
|
190
|
+
except ValueError as exc:
|
|
191
|
+
_fail(str(exc), "page_not_found", json)
|
|
192
|
+
fields: dict[str, object] = {"content": text}
|
|
193
|
+
if page is not None:
|
|
194
|
+
fields["page"] = page
|
|
195
|
+
_emit(resolved, "contents", text, rich, json, **fields)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@codewiki_app.command()
|
|
199
|
+
def ask(
|
|
200
|
+
repo: str = typer.Argument(..., help="Repository (owner/repo or GitHub URL)"),
|
|
201
|
+
question: Optional[str] = typer.Argument(
|
|
202
|
+
None, help="Question (omit for interactive mode)"
|
|
203
|
+
),
|
|
204
|
+
rich: bool = typer.Option(False, "--rich", help="Render Markdown with rich"),
|
|
205
|
+
json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
206
|
+
save: Optional[str] = typer.Option(
|
|
207
|
+
None, "--save", help="Save the answer to a Markdown file (appends)."
|
|
208
|
+
),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Ask a question about a repository (single-shot or interactive)."""
|
|
211
|
+
resolved = _resolve_repo(repo, json)
|
|
212
|
+
if question is not None:
|
|
213
|
+
if not question.strip():
|
|
214
|
+
_fail("Question must not be empty.", "invalid_input", json)
|
|
215
|
+
if (mock := _mock_text()) is not None:
|
|
216
|
+
answer = Answer(body=mock)
|
|
217
|
+
else:
|
|
218
|
+
client = CodeWikiClient()
|
|
219
|
+
try:
|
|
220
|
+
with status("Thinking..."):
|
|
221
|
+
answer = Answer(body=run_async(client.ask(resolved, question)))
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
_handle_exception(exc, json)
|
|
224
|
+
if json:
|
|
225
|
+
typer.echo(format_json(resolved, "ask", question=question, answer=answer.body))
|
|
226
|
+
else:
|
|
227
|
+
_emit(resolved, "ask", format_answer(answer), rich, False)
|
|
228
|
+
_append_save(save, resolved, question, answer.body)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if json:
|
|
232
|
+
typer.secho(
|
|
233
|
+
"Warning: --json has no effect in interactive mode.",
|
|
234
|
+
fg=typer.colors.YELLOW,
|
|
235
|
+
err=True,
|
|
236
|
+
)
|
|
237
|
+
typer.echo(format_header("CodeWiki", resolved, "ask"))
|
|
238
|
+
typer.echo()
|
|
239
|
+
typer.echo("Ask a question, or /exit to quit.")
|
|
240
|
+
typer.echo()
|
|
241
|
+
try:
|
|
242
|
+
run_async(_repl(resolved, rich, save))
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
_handle_exception(exc, False)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _format_stale(info: dict) -> str:
|
|
248
|
+
wiki_sha = info.get("wiki_sha") or ""
|
|
249
|
+
github_sha = info.get("github_sha") or ""
|
|
250
|
+
when = info.get("github_when") or ""
|
|
251
|
+
if not github_sha:
|
|
252
|
+
return "Could not fetch GitHub HEAD."
|
|
253
|
+
if not wiki_sha:
|
|
254
|
+
return "No commit sha in the wiki."
|
|
255
|
+
if github_sha.startswith(wiki_sha):
|
|
256
|
+
return f"最新 (up-to-date): {wiki_sha}"
|
|
257
|
+
suffix = f" ({when})" if when else ""
|
|
258
|
+
return f"过期 (stale): wiki {wiki_sha[:7]} != github {github_sha[:7]}{suffix}"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@codewiki_app.command()
|
|
262
|
+
def stat(
|
|
263
|
+
repo: str = typer.Argument(..., help="Repository (owner/repo or GitHub URL)"),
|
|
264
|
+
stale: bool = typer.Option(False, "--stale", help="Compare the wiki commit against GitHub HEAD"),
|
|
265
|
+
json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Show the commit the CodeWiki documentation was generated from."""
|
|
268
|
+
resolved = _resolve_repo(repo, json)
|
|
269
|
+
client = CodeWikiClient()
|
|
270
|
+
wiki_sha: str = ""
|
|
271
|
+
stale_info: dict | None = None
|
|
272
|
+
try:
|
|
273
|
+
with status("Fetching wiki..."):
|
|
274
|
+
wiki = run_async(client.read_wiki(resolved))
|
|
275
|
+
wiki_sha = wiki.commit_sha
|
|
276
|
+
if stale:
|
|
277
|
+
with status("Checking GitHub HEAD..."):
|
|
278
|
+
head = run_async(client.github_head(resolved))
|
|
279
|
+
stale_info = {
|
|
280
|
+
"wiki_sha": wiki_sha,
|
|
281
|
+
"github_sha": head.get("sha") or "",
|
|
282
|
+
"github_when": head.get("when") or "",
|
|
283
|
+
}
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
_handle_exception(exc, json)
|
|
286
|
+
if json:
|
|
287
|
+
fields: dict[str, object] = {"commit": wiki_sha}
|
|
288
|
+
if stale_info is not None:
|
|
289
|
+
fields["stale"] = stale_info
|
|
290
|
+
typer.echo(format_json(resolved, "stat", **fields))
|
|
291
|
+
return
|
|
292
|
+
text = f"- commit: {wiki_sha}" if wiki_sha else "No data."
|
|
293
|
+
if stale_info is not None:
|
|
294
|
+
text += "\n\n" + _format_stale(stale_info)
|
|
295
|
+
typer.echo(format_result("CodeWiki", resolved, "stat", text))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@codewiki_app.command()
|
|
299
|
+
def cp(
|
|
300
|
+
repo: str = typer.Argument(..., help="Repository (owner/repo or GitHub URL)"),
|
|
301
|
+
output_dir: Optional[str] = typer.Argument(None, help="Output directory"),
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Export the whole wiki as Markdown files plus llms.txt."""
|
|
304
|
+
resolved = _resolve_repo(repo, False)
|
|
305
|
+
client = CodeWikiClient()
|
|
306
|
+
out = output_dir or resolved.replace("/", "_")
|
|
307
|
+
try:
|
|
308
|
+
with status("Exporting wiki..."):
|
|
309
|
+
wiki = run_async(client.read_wiki(resolved))
|
|
310
|
+
pages = [(s.title, render_section(s)) for s in wiki.sections]
|
|
311
|
+
full = render_markdown(wiki)
|
|
312
|
+
count = export_pages(out, resolved, pages, full)
|
|
313
|
+
except Exception as exc:
|
|
314
|
+
_handle_exception(exc, False)
|
|
315
|
+
typer.echo(f"Exported {count} pages to {out}")
|