agent-wiki-cli 0.3.28__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.
- agent_wiki_cli-0.3.28.dist-info/METADATA +425 -0
- agent_wiki_cli-0.3.28.dist-info/RECORD +47 -0
- agent_wiki_cli-0.3.28.dist-info/WHEEL +5 -0
- agent_wiki_cli-0.3.28.dist-info/entry_points.txt +2 -0
- agent_wiki_cli-0.3.28.dist-info/licenses/LICENSE +21 -0
- agent_wiki_cli-0.3.28.dist-info/top_level.txt +1 -0
- llm_wiki_cli/__init__.py +7 -0
- llm_wiki_cli/cli.py +231 -0
- llm_wiki_cli/commands/__init__.py +1 -0
- llm_wiki_cli/commands/bootstrap_cmd.py +1072 -0
- llm_wiki_cli/commands/bump_cmd.py +55 -0
- llm_wiki_cli/commands/context_cmd.py +427 -0
- llm_wiki_cli/commands/extract_cmd.py +745 -0
- llm_wiki_cli/commands/generate_prompt_cmd.py +89 -0
- llm_wiki_cli/commands/hook_cmd.py +161 -0
- llm_wiki_cli/commands/init_cmd.py +92 -0
- llm_wiki_cli/commands/lint_cmd.py +294 -0
- llm_wiki_cli/commands/migrate_cmd.py +892 -0
- llm_wiki_cli/commands/release_cmd.py +163 -0
- llm_wiki_cli/commands/status_cmd.py +70 -0
- llm_wiki_cli/commands/sync_cmd.py +521 -0
- llm_wiki_cli/commands/trigger_cmd.py +205 -0
- llm_wiki_cli/commands/uninstall_cmd.py +221 -0
- llm_wiki_cli/commands/upgrade_cmd.py +196 -0
- llm_wiki_cli/config.py +318 -0
- llm_wiki_cli/extractors/__init__.py +46 -0
- llm_wiki_cli/extractors/common.py +90 -0
- llm_wiki_cli/extractors/go_extractor.py +143 -0
- llm_wiki_cli/extractors/go_scripts/go.mod +3 -0
- llm_wiki_cli/extractors/go_scripts/main.go +668 -0
- llm_wiki_cli/extractors/python_extractor.py +346 -0
- llm_wiki_cli/extractors/rust_extractor.py +143 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.lock +110 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.toml +11 -0
- llm_wiki_cli/extractors/rust_scripts/src/main.rs +803 -0
- llm_wiki_cli/extractors/ts_extractor.py +206 -0
- llm_wiki_cli/extractors/ts_scripts/extract.js +485 -0
- llm_wiki_cli/extractors/ts_scripts/package.json +10 -0
- llm_wiki_cli/services/__init__.py +0 -0
- llm_wiki_cli/services/circuit_breaker.py +79 -0
- llm_wiki_cli/services/io.py +47 -0
- llm_wiki_cli/services/lockfile.py +60 -0
- llm_wiki_cli/services/packages.py +173 -0
- llm_wiki_cli/services/paths.py +31 -0
- llm_wiki_cli/services/schema.py +214 -0
- llm_wiki_cli/services/secure_file.py +22 -0
- llm_wiki_cli/services/versioning.py +193 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Python AST extractor for agent-wiki-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..config import build_gitignore_matcher
|
|
10
|
+
from .common import discover_source_files
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── AST helper utilities ──────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _annotation_to_str(node) -> str:
|
|
17
|
+
"""Convert an AST annotation node to a readable string."""
|
|
18
|
+
if node is None:
|
|
19
|
+
return ""
|
|
20
|
+
if isinstance(node, ast.Constant):
|
|
21
|
+
return repr(node.value)
|
|
22
|
+
if isinstance(node, ast.Name):
|
|
23
|
+
return node.id
|
|
24
|
+
if isinstance(node, ast.Attribute):
|
|
25
|
+
return f"{_annotation_to_str(node.value)}.{node.attr}"
|
|
26
|
+
if isinstance(node, ast.Subscript):
|
|
27
|
+
return f"{_annotation_to_str(node.value)}[{_annotation_to_str(node.slice)}]"
|
|
28
|
+
if isinstance(node, ast.Tuple):
|
|
29
|
+
return ", ".join(_annotation_to_str(e) for e in node.elts)
|
|
30
|
+
if isinstance(node, ast.List):
|
|
31
|
+
return "[" + ", ".join(_annotation_to_str(e) for e in node.elts) + "]"
|
|
32
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
33
|
+
return f"{_annotation_to_str(node.left)} | {_annotation_to_str(node.right)}"
|
|
34
|
+
return ast.dump(node)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _default_to_str(node) -> str:
|
|
38
|
+
"""Convert a default-value AST node to a readable string."""
|
|
39
|
+
if node is None:
|
|
40
|
+
return ""
|
|
41
|
+
if isinstance(node, ast.Constant):
|
|
42
|
+
return repr(node.value)
|
|
43
|
+
if isinstance(node, ast.Name):
|
|
44
|
+
return node.id
|
|
45
|
+
if isinstance(node, ast.List):
|
|
46
|
+
return "[" + ", ".join(_default_to_str(e) for e in node.elts) + "]"
|
|
47
|
+
if isinstance(node, ast.Dict):
|
|
48
|
+
return "{...}"
|
|
49
|
+
if isinstance(node, ast.Call):
|
|
50
|
+
func = _annotation_to_str(node.func)
|
|
51
|
+
return f"{func}(...)"
|
|
52
|
+
return "..."
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_decorators(node) -> list[str]:
|
|
56
|
+
"""Extract decorator names from a node."""
|
|
57
|
+
decorators = []
|
|
58
|
+
for dec in node.decorator_list:
|
|
59
|
+
if isinstance(dec, ast.Name):
|
|
60
|
+
decorators.append(dec.id)
|
|
61
|
+
elif isinstance(dec, ast.Attribute):
|
|
62
|
+
decorators.append(_annotation_to_str(dec))
|
|
63
|
+
elif isinstance(dec, ast.Call):
|
|
64
|
+
func_str = _annotation_to_str(dec.func)
|
|
65
|
+
args_parts = []
|
|
66
|
+
for a in dec.args:
|
|
67
|
+
args_parts.append(_annotation_to_str(a))
|
|
68
|
+
for kw in dec.keywords:
|
|
69
|
+
args_parts.append(f"{kw.arg}={_annotation_to_str(kw.value)}")
|
|
70
|
+
decorators.append(f"{func_str}({', '.join(args_parts)})")
|
|
71
|
+
return decorators
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _extract_function_info(node) -> dict:
|
|
75
|
+
"""Extract full function/method info from a FunctionDef or AsyncFunctionDef."""
|
|
76
|
+
info = {
|
|
77
|
+
"name": node.name,
|
|
78
|
+
"line": node.lineno,
|
|
79
|
+
"docstring": ast.get_docstring(node) or "",
|
|
80
|
+
"decorators": _extract_decorators(node),
|
|
81
|
+
"is_async": isinstance(node, ast.AsyncFunctionDef),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Parameters (skip 'self'/'cls' for methods)
|
|
85
|
+
params = []
|
|
86
|
+
args_node = node.args
|
|
87
|
+
|
|
88
|
+
# Pair defaults with args (defaults align to the end of the args list)
|
|
89
|
+
num_args = len(args_node.args)
|
|
90
|
+
num_defaults = len(args_node.defaults)
|
|
91
|
+
default_offset = num_args - num_defaults
|
|
92
|
+
|
|
93
|
+
for i, arg in enumerate(args_node.args):
|
|
94
|
+
if arg.arg in ("self", "cls"):
|
|
95
|
+
continue
|
|
96
|
+
param = {
|
|
97
|
+
"name": arg.arg,
|
|
98
|
+
"type": _annotation_to_str(arg.annotation),
|
|
99
|
+
}
|
|
100
|
+
default_idx = i - default_offset
|
|
101
|
+
if default_idx >= 0:
|
|
102
|
+
param["default"] = _default_to_str(args_node.defaults[default_idx])
|
|
103
|
+
params.append(param)
|
|
104
|
+
|
|
105
|
+
info["params"] = params
|
|
106
|
+
info["return_type"] = _annotation_to_str(node.returns)
|
|
107
|
+
|
|
108
|
+
return info
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_class_attributes(node) -> list[dict]:
|
|
112
|
+
"""Extract annotated attributes from a class body (Pydantic fields, dataclass fields, etc.)."""
|
|
113
|
+
attrs = []
|
|
114
|
+
for child in node.body:
|
|
115
|
+
if isinstance(child, ast.AnnAssign) and isinstance(child.target, ast.Name):
|
|
116
|
+
attr = {
|
|
117
|
+
"name": child.target.id,
|
|
118
|
+
"type": _annotation_to_str(child.annotation),
|
|
119
|
+
"default": _default_to_str(child.value) if child.value else "",
|
|
120
|
+
}
|
|
121
|
+
attrs.append(attr)
|
|
122
|
+
return attrs
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── AST visitor ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ComponentVisitor(ast.NodeVisitor):
|
|
129
|
+
def __init__(self, deep: bool = False):
|
|
130
|
+
self.classes = []
|
|
131
|
+
self.functions = [] # top-level functions only
|
|
132
|
+
self.imports = []
|
|
133
|
+
self.constants = [] # UPPER_CASE module-level assignments
|
|
134
|
+
self.has_all = False # whether __all__ is defined
|
|
135
|
+
self._class_depth = 0
|
|
136
|
+
self._function_depth = 0
|
|
137
|
+
self._deep = deep
|
|
138
|
+
|
|
139
|
+
def visit_Import(self, node):
|
|
140
|
+
for alias in node.names:
|
|
141
|
+
self.imports.append({
|
|
142
|
+
"module": alias.name,
|
|
143
|
+
"name": alias.asname or alias.name,
|
|
144
|
+
"type": "import",
|
|
145
|
+
})
|
|
146
|
+
self.generic_visit(node)
|
|
147
|
+
|
|
148
|
+
def visit_ImportFrom(self, node):
|
|
149
|
+
module = "." * node.level + (node.module or "")
|
|
150
|
+
for alias in node.names:
|
|
151
|
+
self.imports.append({
|
|
152
|
+
"module": module,
|
|
153
|
+
"name": alias.name,
|
|
154
|
+
"alias": alias.asname,
|
|
155
|
+
"type": "from",
|
|
156
|
+
})
|
|
157
|
+
self.generic_visit(node)
|
|
158
|
+
|
|
159
|
+
def visit_ClassDef(self, node):
|
|
160
|
+
if self._class_depth > 0 or self._function_depth > 0:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
bases = [_annotation_to_str(b) for b in node.bases]
|
|
164
|
+
docstring = ast.get_docstring(node) or ""
|
|
165
|
+
decorators = _extract_decorators(node)
|
|
166
|
+
attributes = _extract_class_attributes(node)
|
|
167
|
+
|
|
168
|
+
# Extract methods (including private for completeness)
|
|
169
|
+
methods = []
|
|
170
|
+
for child in node.body:
|
|
171
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
172
|
+
methods.append(_extract_function_info(child))
|
|
173
|
+
|
|
174
|
+
self.classes.append({
|
|
175
|
+
"name": node.name,
|
|
176
|
+
"bases": bases,
|
|
177
|
+
"line": node.lineno,
|
|
178
|
+
"docstring": docstring,
|
|
179
|
+
"decorators": decorators,
|
|
180
|
+
"attributes": attributes,
|
|
181
|
+
"methods": methods,
|
|
182
|
+
})
|
|
183
|
+
# Don't generic_visit — we already walked class body for methods/attrs
|
|
184
|
+
|
|
185
|
+
def visit_FunctionDef(self, node):
|
|
186
|
+
# Only capture top-level functions (not methods inside classes)
|
|
187
|
+
if self._class_depth == 0 and self._function_depth == 0:
|
|
188
|
+
if not node.name.startswith("_"):
|
|
189
|
+
self.functions.append(_extract_function_info(node))
|
|
190
|
+
elif self._deep:
|
|
191
|
+
info = _extract_function_info(node)
|
|
192
|
+
info["private"] = True
|
|
193
|
+
self.functions.append(info)
|
|
194
|
+
self._function_depth += 1
|
|
195
|
+
try:
|
|
196
|
+
self.generic_visit(node)
|
|
197
|
+
finally:
|
|
198
|
+
self._function_depth -= 1
|
|
199
|
+
|
|
200
|
+
def visit_AsyncFunctionDef(self, node):
|
|
201
|
+
if self._class_depth == 0 and self._function_depth == 0:
|
|
202
|
+
if not node.name.startswith("_"):
|
|
203
|
+
self.functions.append(_extract_function_info(node))
|
|
204
|
+
elif self._deep:
|
|
205
|
+
info = _extract_function_info(node)
|
|
206
|
+
info["private"] = True
|
|
207
|
+
self.functions.append(info)
|
|
208
|
+
self._function_depth += 1
|
|
209
|
+
try:
|
|
210
|
+
self.generic_visit(node)
|
|
211
|
+
finally:
|
|
212
|
+
self._function_depth -= 1
|
|
213
|
+
|
|
214
|
+
def visit_Assign(self, node):
|
|
215
|
+
"""Detect module-level UPPER_CASE constants and ``__all__``."""
|
|
216
|
+
if self._class_depth == 0 and self._function_depth == 0:
|
|
217
|
+
for target in node.targets:
|
|
218
|
+
if isinstance(target, ast.Name):
|
|
219
|
+
if target.id == "__all__":
|
|
220
|
+
self.has_all = True
|
|
221
|
+
elif target.id == target.id.upper() and target.id.replace("_", "").isalnum() and not target.id[0].isdigit():
|
|
222
|
+
self.constants.append({
|
|
223
|
+
"name": target.id,
|
|
224
|
+
"line": node.lineno,
|
|
225
|
+
})
|
|
226
|
+
self.generic_visit(node)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── Core scan logic ──────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _scan_python_files(
|
|
233
|
+
src_dir: str,
|
|
234
|
+
deep: bool = False,
|
|
235
|
+
only_files: list[str] | None = None,
|
|
236
|
+
include_empty: bool = False,
|
|
237
|
+
) -> dict:
|
|
238
|
+
"""Scan Python files under *src_dir* and return a raw inventory dict.
|
|
239
|
+
|
|
240
|
+
The returned dict maps *relative* filepath strings (relative to
|
|
241
|
+
*src_dir*) to file entry dicts. The ``"language"`` key is
|
|
242
|
+
intentionally absent here — callers (e.g. :class:`PythonExtractor`)
|
|
243
|
+
are responsible for stamping it.
|
|
244
|
+
"""
|
|
245
|
+
src_path = Path(src_dir).resolve()
|
|
246
|
+
inventory = {}
|
|
247
|
+
matcher = build_gitignore_matcher(src_path)
|
|
248
|
+
py_files = [
|
|
249
|
+
src_path / rel
|
|
250
|
+
for rel in discover_source_files(
|
|
251
|
+
str(src_path), (".py",), only_files=only_files, language="python", matcher=matcher,
|
|
252
|
+
)
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for py_file in py_files:
|
|
256
|
+
rel = py_file.relative_to(src_path)
|
|
257
|
+
try:
|
|
258
|
+
data = py_file.read_bytes()
|
|
259
|
+
try:
|
|
260
|
+
source = data.decode("utf-8")
|
|
261
|
+
except UnicodeDecodeError:
|
|
262
|
+
source = data.decode("cp1252")
|
|
263
|
+
tree = ast.parse(source, filename=str(py_file))
|
|
264
|
+
except UnicodeDecodeError:
|
|
265
|
+
print(f"llm-wiki Python extractor: skipped undecodable file {rel.as_posix()}", file=sys.stderr)
|
|
266
|
+
continue
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
print(f"llm-wiki Python extractor: failed to read {rel.as_posix()}: {exc}", file=sys.stderr)
|
|
269
|
+
continue
|
|
270
|
+
except SyntaxError:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
visitor = ComponentVisitor(deep=deep)
|
|
274
|
+
visitor.visit(tree)
|
|
275
|
+
|
|
276
|
+
# Include the file if it has classes, public functions, constants,
|
|
277
|
+
# __all__, or (in deep mode) private functions.
|
|
278
|
+
has_content = (
|
|
279
|
+
visitor.classes
|
|
280
|
+
or visitor.functions
|
|
281
|
+
or visitor.constants
|
|
282
|
+
or visitor.has_all
|
|
283
|
+
or include_empty
|
|
284
|
+
)
|
|
285
|
+
if has_content:
|
|
286
|
+
file_entry = {
|
|
287
|
+
"classes": visitor.classes,
|
|
288
|
+
"functions": visitor.functions,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if visitor.constants:
|
|
292
|
+
file_entry["constants"] = visitor.constants
|
|
293
|
+
if visitor.has_all:
|
|
294
|
+
file_entry["has_all"] = True
|
|
295
|
+
|
|
296
|
+
if deep:
|
|
297
|
+
file_entry["imports"] = visitor.imports
|
|
298
|
+
file_entry["module_docstring"] = ast.get_docstring(tree) or ""
|
|
299
|
+
else:
|
|
300
|
+
# Slim format: strip rich fields for backward compat
|
|
301
|
+
file_entry["classes"] = [
|
|
302
|
+
{"name": c["name"], "bases": c["bases"], "line": c["line"]}
|
|
303
|
+
for c in visitor.classes
|
|
304
|
+
]
|
|
305
|
+
fns = []
|
|
306
|
+
for f in visitor.functions:
|
|
307
|
+
fn = {"name": f["name"], "line": f["line"]}
|
|
308
|
+
if f.get("is_async"):
|
|
309
|
+
fn["async"] = True
|
|
310
|
+
if f.get("private"):
|
|
311
|
+
fn["private"] = True
|
|
312
|
+
fns.append(fn)
|
|
313
|
+
file_entry["functions"] = fns
|
|
314
|
+
|
|
315
|
+
inventory[rel.as_posix()] = file_entry
|
|
316
|
+
|
|
317
|
+
return inventory
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ── Public extractor class ────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class PythonExtractor:
|
|
324
|
+
"""Extractor for Python source files using the built-in :mod:`ast` module.
|
|
325
|
+
|
|
326
|
+
Implements :class:`~llm_wiki_cli.extractors.ExtractorProtocol`.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def extract(
|
|
330
|
+
self,
|
|
331
|
+
src_dir: str,
|
|
332
|
+
only_files: list[str] | None = None,
|
|
333
|
+
deep: bool = False,
|
|
334
|
+
include_empty: bool = False,
|
|
335
|
+
) -> dict:
|
|
336
|
+
"""Scan *src_dir* for Python files and return an inventory dict.
|
|
337
|
+
|
|
338
|
+
Each file entry includes ``"language": "python"``.
|
|
339
|
+
"""
|
|
340
|
+
inventory = _scan_python_files(
|
|
341
|
+
src_dir, deep=deep, only_files=only_files,
|
|
342
|
+
include_empty=include_empty,
|
|
343
|
+
)
|
|
344
|
+
for entry in inventory.values():
|
|
345
|
+
entry["language"] = "python"
|
|
346
|
+
return inventory
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Rust AST extractor for agent-wiki-cli.
|
|
2
|
+
|
|
3
|
+
Implements :class:`~llm_wiki_cli.extractors.ExtractorProtocol` by delegating
|
|
4
|
+
to a bundled Rust binary (``rust_scripts/src/main.rs``) that uses the ``syn``
|
|
5
|
+
crate for Rust AST parsing.
|
|
6
|
+
|
|
7
|
+
Requirements
|
|
8
|
+
------------
|
|
9
|
+
* Rust toolchain (``cargo``) on PATH.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .common import discover_source_files, filter_bundled_inventory
|
|
21
|
+
|
|
22
|
+
_RUST_SCRIPTS_DIR = Path(__file__).parent / "rust_scripts"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RustExtractor:
|
|
26
|
+
"""Extractor for Rust source files using a ``cargo run`` subprocess.
|
|
27
|
+
|
|
28
|
+
Implements :class:`~llm_wiki_cli.extractors.ExtractorProtocol`.
|
|
29
|
+
|
|
30
|
+
Each returned file entry includes ``"language": "rust"``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
last_error: str | None = None
|
|
34
|
+
|
|
35
|
+
def extract(
|
|
36
|
+
self,
|
|
37
|
+
src_dir: str,
|
|
38
|
+
only_files: list[str] | None = None,
|
|
39
|
+
deep: bool = False,
|
|
40
|
+
) -> dict:
|
|
41
|
+
"""Scan *src_dir* for Rust files and return an inventory dict.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
src_dir:
|
|
46
|
+
Root directory to scan.
|
|
47
|
+
only_files:
|
|
48
|
+
Optional list of paths (relative to *src_dir*) to restrict
|
|
49
|
+
extraction to. When ``None``, all ``.rs`` files found under
|
|
50
|
+
*src_dir* are scanned (excluding ``target/``, ``vendor/``, etc.).
|
|
51
|
+
deep:
|
|
52
|
+
When ``True``, include enriched data (doc comments, struct fields,
|
|
53
|
+
method details, imports). When ``False``, return a slim format.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
dict
|
|
58
|
+
``{filepath: file_entry}`` where each ``file_entry`` contains at
|
|
59
|
+
minimum ``"classes"``, ``"functions"``, and ``"language"``.
|
|
60
|
+
"""
|
|
61
|
+
self.last_error = None
|
|
62
|
+
source_files = discover_source_files(
|
|
63
|
+
src_dir, (".rs",), only_files=only_files, language="rust",
|
|
64
|
+
)
|
|
65
|
+
if not source_files:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
if not shutil.which("cargo"):
|
|
69
|
+
self.last_error = "cargo not found. Install Rust (https://rustup.rs/) to enable Rust extraction."
|
|
70
|
+
print(f"llm-wiki Rust extractor: {self.last_error}", file=sys.stderr)
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
cmd = [
|
|
74
|
+
"cargo", "run", "--quiet", "--",
|
|
75
|
+
"--src-dir", str(Path(src_dir).resolve()),
|
|
76
|
+
]
|
|
77
|
+
cmd += ["--only-files", ",".join(source_files)]
|
|
78
|
+
if deep:
|
|
79
|
+
cmd.append("--deep")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
cmd,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
check=True,
|
|
87
|
+
timeout=180,
|
|
88
|
+
cwd=str(_RUST_SCRIPTS_DIR),
|
|
89
|
+
)
|
|
90
|
+
except subprocess.CalledProcessError as exc:
|
|
91
|
+
self.last_error = "extraction failed"
|
|
92
|
+
print(
|
|
93
|
+
f"llm-wiki Rust extractor: extraction failed.\n{exc.stderr}",
|
|
94
|
+
file=sys.stderr,
|
|
95
|
+
)
|
|
96
|
+
return {}
|
|
97
|
+
except subprocess.TimeoutExpired:
|
|
98
|
+
self.last_error = "extraction timed out after 180 s"
|
|
99
|
+
print(
|
|
100
|
+
"llm-wiki Rust extractor: extraction timed out after 180 s.",
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
103
|
+
return {}
|
|
104
|
+
except FileNotFoundError:
|
|
105
|
+
self.last_error = "cargo executable not found"
|
|
106
|
+
print(
|
|
107
|
+
"llm-wiki Rust extractor: cargo executable not found.",
|
|
108
|
+
file=sys.stderr,
|
|
109
|
+
)
|
|
110
|
+
return {}
|
|
111
|
+
|
|
112
|
+
# Forward any warnings the Rust script wrote to stderr.
|
|
113
|
+
if result.stderr.strip():
|
|
114
|
+
sys.stderr.write(result.stderr)
|
|
115
|
+
|
|
116
|
+
if not result.stdout.strip():
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
inventory: dict = json.loads(result.stdout)
|
|
121
|
+
except json.JSONDecodeError as exc:
|
|
122
|
+
self.last_error = "malformed JSON output"
|
|
123
|
+
print(
|
|
124
|
+
f"llm-wiki Rust extractor: malformed JSON output — {exc}",
|
|
125
|
+
file=sys.stderr,
|
|
126
|
+
)
|
|
127
|
+
return {}
|
|
128
|
+
|
|
129
|
+
for entry in inventory.values():
|
|
130
|
+
entry["language"] = "rust"
|
|
131
|
+
|
|
132
|
+
inventory = filter_bundled_inventory(inventory, _RUST_SCRIPTS_DIR)
|
|
133
|
+
|
|
134
|
+
src_root = Path(src_dir).resolve()
|
|
135
|
+
normalized_inventory: dict = {}
|
|
136
|
+
for fp, data in inventory.items():
|
|
137
|
+
try:
|
|
138
|
+
rel = Path(fp).resolve().relative_to(src_root).as_posix()
|
|
139
|
+
except ValueError:
|
|
140
|
+
rel = fp.replace("\\", "/")
|
|
141
|
+
normalized_inventory[rel] = data
|
|
142
|
+
|
|
143
|
+
return normalized_inventory
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "itoa"
|
|
7
|
+
version = "1.0.18"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "llm-wiki-rust-extractor"
|
|
13
|
+
version = "0.1.0"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"proc-macro2",
|
|
16
|
+
"quote",
|
|
17
|
+
"serde",
|
|
18
|
+
"serde_json",
|
|
19
|
+
"syn",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[[package]]
|
|
23
|
+
name = "memchr"
|
|
24
|
+
version = "2.8.0"
|
|
25
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
26
|
+
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "proc-macro2"
|
|
30
|
+
version = "1.0.106"
|
|
31
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
32
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"unicode-ident",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[package]]
|
|
38
|
+
name = "quote"
|
|
39
|
+
version = "1.0.45"
|
|
40
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
41
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
42
|
+
dependencies = [
|
|
43
|
+
"proc-macro2",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[[package]]
|
|
47
|
+
name = "serde"
|
|
48
|
+
version = "1.0.228"
|
|
49
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
50
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
51
|
+
dependencies = [
|
|
52
|
+
"serde_core",
|
|
53
|
+
"serde_derive",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[[package]]
|
|
57
|
+
name = "serde_core"
|
|
58
|
+
version = "1.0.228"
|
|
59
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
60
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
61
|
+
dependencies = [
|
|
62
|
+
"serde_derive",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "serde_derive"
|
|
67
|
+
version = "1.0.228"
|
|
68
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
69
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
70
|
+
dependencies = [
|
|
71
|
+
"proc-macro2",
|
|
72
|
+
"quote",
|
|
73
|
+
"syn",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[[package]]
|
|
77
|
+
name = "serde_json"
|
|
78
|
+
version = "1.0.149"
|
|
79
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
80
|
+
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
|
81
|
+
dependencies = [
|
|
82
|
+
"itoa",
|
|
83
|
+
"memchr",
|
|
84
|
+
"serde",
|
|
85
|
+
"serde_core",
|
|
86
|
+
"zmij",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[[package]]
|
|
90
|
+
name = "syn"
|
|
91
|
+
version = "2.0.117"
|
|
92
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
93
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
94
|
+
dependencies = [
|
|
95
|
+
"proc-macro2",
|
|
96
|
+
"quote",
|
|
97
|
+
"unicode-ident",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[[package]]
|
|
101
|
+
name = "unicode-ident"
|
|
102
|
+
version = "1.0.24"
|
|
103
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
104
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
105
|
+
|
|
106
|
+
[[package]]
|
|
107
|
+
name = "zmij"
|
|
108
|
+
version = "1.0.21"
|
|
109
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
110
|
+
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "llm-wiki-rust-extractor"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
syn = { version = "2", features = ["full", "extra-traits"] }
|
|
8
|
+
quote = "1"
|
|
9
|
+
proc-macro2 = { version = "1", features = ["span-locations"] }
|
|
10
|
+
serde = { version = "1", features = ["derive"] }
|
|
11
|
+
serde_json = "1"
|