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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repowiki-cli = repowiki.cli:main
repowiki/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """repowiki-cli - query DeepWiki documentation from the terminal."""
2
+
3
+ __version__ = "0.1.0"
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,5 @@
1
+ """CodeWiki service."""
2
+
3
+ from repowiki.services.codewiki.cli import register
4
+
5
+ __all__ = ["register"]
@@ -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}")