wizard-central-toolkit 0.2.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.
Files changed (77) hide show
  1. wiz_central_toolkit/__init__.py +2 -0
  2. wiz_central_toolkit/bannerwiz/__main__.py +6 -0
  3. wiz_central_toolkit/bannerwiz/cli.py +79 -0
  4. wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
  5. wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
  6. wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
  7. wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
  8. wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
  9. wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
  10. wiz_central_toolkit/codeguardwiz/cli.py +971 -0
  11. wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
  12. wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
  13. wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
  14. wiz_central_toolkit/common/__init__.py +1 -0
  15. wiz_central_toolkit/debugwiz/__init__.py +1 -0
  16. wiz_central_toolkit/debugwiz/__main__.py +360 -0
  17. wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
  18. wiz_central_toolkit/diffwiz/__init__.py +0 -0
  19. wiz_central_toolkit/diffwiz/__main__.py +21 -0
  20. wiz_central_toolkit/diffwiz/cli.py +134 -0
  21. wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
  22. wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
  23. wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
  24. wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
  25. wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
  26. wiz_central_toolkit/godfatherwiz/ai.py +136 -0
  27. wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
  28. wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
  29. wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
  30. wiz_central_toolkit/jsonwiz/__init__.py +0 -0
  31. wiz_central_toolkit/jsonwiz/__main__.py +21 -0
  32. wiz_central_toolkit/jsonwiz/cli.py +4 -0
  33. wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
  34. wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
  35. wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
  36. wiz_central_toolkit/pythonwiz/__init__.py +0 -0
  37. wiz_central_toolkit/pythonwiz/__main__.py +21 -0
  38. wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
  39. wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
  40. wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
  41. wiz_central_toolkit/readwiz/__init__.py +5 -0
  42. wiz_central_toolkit/readwiz/__main__.py +15 -0
  43. wiz_central_toolkit/readwiz/cli.py +46 -0
  44. wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
  45. wiz_central_toolkit/readwiz/lib/cli.py +47 -0
  46. wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
  47. wiz_central_toolkit/rustwiz/__init__.py +5 -0
  48. wiz_central_toolkit/rustwiz/__main__.py +7 -0
  49. wiz_central_toolkit/rustwiz/cli.py +4 -0
  50. wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
  51. wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
  52. wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
  53. wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
  54. wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
  55. wiz_central_toolkit/tmuxwiz/cli.py +7 -0
  56. wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
  57. wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
  58. wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
  59. wiz_central_toolkit/todowiz/__init__.py +0 -0
  60. wiz_central_toolkit/todowiz/__main__.py +22 -0
  61. wiz_central_toolkit/todowiz/cli.py +4 -0
  62. wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
  63. wiz_central_toolkit/todowiz/lib/cli.py +82 -0
  64. wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
  65. wiz_central_toolkit/tools_registry.py +62 -0
  66. wiz_central_toolkit/wiz/__init__.py +0 -0
  67. wiz_central_toolkit/wiz/__main__.py +141 -0
  68. wiz_central_toolkit/writewiz/__init__.py +1 -0
  69. wiz_central_toolkit/writewiz/__main__.py +9 -0
  70. wiz_central_toolkit/writewiz/cli.py +264 -0
  71. wiz_central_toolkit/writewiz/write_wizard.py +384 -0
  72. wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
  73. wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
  74. wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
  75. wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
  76. wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
  77. wizard_central_toolkit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,353 @@
1
+ """python-wizard core: standalone syntax doctor for Python files."""
2
+ import ast
3
+ import re
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ SKIP_DIRS = {".venv", ".git", "__pycache__", ".bootstrap", "node_modules"}
9
+ STDLIB = set(sys.stdlib_module_names)
10
+
11
+ class PythonWizard:
12
+ """AST analysis engine. Never executes the target code — parse only."""
13
+
14
+ # ---------- existing ----------
15
+ def check_file(self, path: Path) -> dict:
16
+ try:
17
+ ast.parse(path.read_text())
18
+ return {"file": str(path), "valid": True}
19
+ except FileNotFoundError:
20
+ return {"file": str(path), "valid": False, "error": "file not found"}
21
+ except SyntaxError as e:
22
+ return {"file": str(path), "valid": False, "line": e.lineno,
23
+ "col": e.offset, "error": e.msg}
24
+
25
+ def check_dir(self, root: Path) -> list:
26
+ results = []
27
+ for py in self.py_files(root):
28
+ results.append(self.check_file(py))
29
+ return results
30
+
31
+ def check_entry(self, path: Path) -> dict:
32
+ if not path.exists():
33
+ return {"file": str(path), "valid": False, "error": "file not found"}
34
+ content = path.read_text()
35
+ first = content.splitlines()[0] if content else ""
36
+ if not first.startswith("#!"):
37
+ return {"file": str(path), "valid": False, "error": "no shebang line"}
38
+ parts = first[2:].split()
39
+ if not parts:
40
+ return {"file": str(path), "valid": False, "error": "empty shebang line"}
41
+ exe = parts[0]
42
+ if Path(exe).name == "env":
43
+ # env form: #!/usr/bin/env [-flags] [VAR=value ...] <interpreter> [args]
44
+ cmd = None
45
+ for tok in parts[1:]:
46
+ if tok.startswith("-"):
47
+ continue
48
+ if "=" in tok and "/" not in tok.split("=")[0]:
49
+ continue # VAR=value assignment, not the command
50
+ cmd = tok
51
+ break
52
+ if cmd is None:
53
+ return {"file": str(path), "valid": False,
54
+ "error": f"interpreter missing: {first[2:]}"}
55
+ found = shutil.which(cmd)
56
+ if not found:
57
+ return {"file": str(path), "valid": False,
58
+ "error": f"interpreter missing: {cmd}"}
59
+ return {"file": str(path), "valid": True, "interpreter": found}
60
+ interp = Path(exe)
61
+ if interp.is_absolute():
62
+ if not interp.exists():
63
+ return {"file": str(path), "valid": False,
64
+ "error": f"interpreter missing: {exe}"}
65
+ return {"file": str(path), "valid": True, "interpreter": str(interp)}
66
+ found = shutil.which(exe)
67
+ if not found:
68
+ return {"file": str(path), "valid": False,
69
+ "error": f"interpreter missing: {exe}"}
70
+ return {"file": str(path), "valid": True, "interpreter": found}
71
+
72
+ # ---------- helpers ----------
73
+ @staticmethod
74
+ def py_files(root: Path) -> list:
75
+ return [p for p in sorted(root.rglob("*.py"))
76
+ if not any(part in SKIP_DIRS for part in p.parts)]
77
+
78
+ @staticmethod
79
+ def parse(path: Path) -> ast.Module:
80
+ return ast.parse(path.read_text())
81
+
82
+ # ---------- new commands ----------
83
+ def outline(self, path: Path) -> list:
84
+ tree = self.parse(path)
85
+ symbols = []
86
+
87
+ def visit(node, prefix=""):
88
+ for child in ast.iter_child_nodes(node):
89
+ if isinstance(child, ast.ClassDef):
90
+ symbols.append(("class", prefix + child.name, child.lineno))
91
+ visit(child, prefix + child.name + ".")
92
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
93
+ symbols.append(("def", prefix + child.name, child.lineno))
94
+
95
+ visit(tree)
96
+ return symbols
97
+
98
+ def imports_of(self, path: Path) -> dict:
99
+ tree = self.parse(path)
100
+ siblings = {p.stem for p in path.parent.glob("*.py")}
101
+ result = {"stdlib": set(), "third_party": set(), "local": set(), "relative": set()}
102
+ for node in ast.walk(tree):
103
+ if isinstance(node, ast.Import):
104
+ for a in node.names:
105
+ top = a.name.split(".")[0]
106
+ if top in STDLIB:
107
+ result["stdlib"].add(a.name)
108
+ elif top in siblings:
109
+ result["local"].add(a.name)
110
+ else:
111
+ result["third_party"].add(a.name)
112
+ elif isinstance(node, ast.ImportFrom):
113
+ if node.level > 0:
114
+ result["relative"].add("." * node.level + (node.module or ""))
115
+ elif node.module:
116
+ top = node.module.split(".")[0]
117
+ if top in siblings:
118
+ result["local"].add(node.module)
119
+ elif top in STDLIB:
120
+ result["stdlib"].add(node.module)
121
+ else:
122
+ result["third_party"].add(node.module)
123
+ return {k: sorted(v) for k, v in result.items()}
124
+
125
+ def todos_of(self, path: Path) -> list:
126
+ pat = re.compile(r"#\s*(TODO|FIXME|XXX|BUG|HACK)\b[:]?(.*)", re.I)
127
+ out = []
128
+ for i, line in enumerate(path.read_text().splitlines(), 1):
129
+ m = pat.search(line)
130
+ if m:
131
+ out.append((i, m.group(1).upper(), m.group(2).strip()))
132
+ return out
133
+
134
+ def deadcode(self, path: Path) -> list:
135
+ """Candidates only: names defined but never referenced by name.
136
+ Attribute-called methods may appear here — static analysis limit."""
137
+ tree = self.parse(path)
138
+ defined = {}
139
+ for node in ast.walk(tree):
140
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
141
+ defined[node.name] = node.lineno
142
+ used = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
143
+ dead = [(n, ln) for n, ln in defined.items()
144
+ if n not in used and not n.startswith("__")]
145
+ return sorted(dead, key=lambda x: x[1])
146
+
147
+ def complexity(self, path: Path) -> list:
148
+ tree = self.parse(path)
149
+ out = []
150
+ for node in ast.walk(tree):
151
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
152
+ c = 1
153
+ for sub in ast.walk(node):
154
+ if isinstance(sub, (ast.If, ast.For, ast.While,
155
+ ast.ExceptHandler, ast.With)):
156
+ c += 1
157
+ elif isinstance(sub, ast.BoolOp):
158
+ c += len(sub.values) - 1
159
+ out.append((node.name, node.lineno, c))
160
+ return sorted(out, key=lambda x: -x[2])
161
+
162
+ def loc_stats(self, path: Path) -> dict:
163
+ lines = path.read_text().splitlines()
164
+ code = blank = comment = 0
165
+ for l in lines:
166
+ s = l.strip()
167
+ if not s:
168
+ blank += 1
169
+ elif s.startswith("#"):
170
+ comment += 1
171
+ else:
172
+ code += 1
173
+ tree = self.parse(path)
174
+ funcs = sum(1 for n in ast.walk(tree)
175
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)))
176
+ classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
177
+ return {"lines": len(lines), "code": code, "blank": blank,
178
+ "comment": comment, "functions": funcs, "classes": classes}
179
+
180
+ def docstring_report(self, path: Path) -> list:
181
+ tree = self.parse(path)
182
+ out = []
183
+ for node in ast.walk(tree):
184
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
185
+ if ast.get_docstring(node) is None:
186
+ out.append((node.lineno, node.name))
187
+ return sorted(out)
188
+
189
+ def explain_error(self, path: Path) -> dict:
190
+ try:
191
+ ast.parse(path.read_text())
192
+ return {"valid": True}
193
+ except SyntaxError as e:
194
+ lines = path.read_text().splitlines()
195
+ ctx = []
196
+ lo = max(0, (e.lineno or 1) - 2)
197
+ hi = min(len(lines), (e.lineno or 1) + 1)
198
+ for i in range(lo, hi):
199
+ mark = "->" if i + 1 == e.lineno else " "
200
+ ctx.append(f"{mark} {i+1:4d}| {lines[i]}")
201
+ return {"valid": False, "line": e.lineno, "col": e.offset,
202
+ "error": e.msg, "context": ctx}
203
+
204
+ def shebangs_dir(self, root: Path) -> list:
205
+ results = []
206
+ for p in self.py_files(root):
207
+ with p.open() as f:
208
+ first = f.readline().strip()
209
+ if first.startswith("#!"):
210
+ interp = first[2:]
211
+ ok = Path(interp).is_absolute() and Path(interp).exists()
212
+ results.append((p, interp, ok))
213
+ return results
214
+
215
+ def deps(self, path: Path) -> list:
216
+ return self.imports_of(path)["third_party"]
217
+
218
+ def call_map(self, path: Path) -> dict:
219
+ tree = self.parse(path)
220
+ m = {}
221
+ for node in ast.walk(tree):
222
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
223
+ calls = set()
224
+ for n in ast.walk(node):
225
+ if isinstance(n, ast.Call):
226
+ if isinstance(n.func, ast.Name):
227
+ calls.add(n.func.id)
228
+ elif isinstance(n.func, ast.Attribute):
229
+ calls.add("." + getattr(n.func, "attr", "?"))
230
+ m[node.name] = sorted(calls)
231
+ return m
232
+
233
+ def longest_functions(self, path: Path) -> list:
234
+ tree = self.parse(path)
235
+ out = []
236
+ for n in ast.walk(tree):
237
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.end_lineno:
238
+ out.append((n.name, n.lineno, (n.end_lineno - n.lineno) + 1))
239
+ return sorted(out, key=lambda x: -x[2])
240
+
241
+ def bare_excepts(self, path: Path) -> list:
242
+ tree = self.parse(path)
243
+ out = []
244
+ for n in ast.walk(tree):
245
+ if isinstance(n, ast.ExceptHandler):
246
+ name = n.type.id if isinstance(n.type, ast.Name) else None
247
+ if n.type is None or name in ("Exception", "BaseException"):
248
+ swallows = any(isinstance(s, ast.Pass) for s in n.body)
249
+ out.append((n.lineno, "bare" if n.type is None else name, swallows))
250
+ return out
251
+
252
+ def secrets_scan(self, path: Path, show_secrets: bool = False) -> list:
253
+ """Hardcoded-credential findings as (lineno, kind, text) rows.
254
+
255
+ Redaction is the default (AVS ruling, 2026-11-09 — the RS006
256
+ paste-safety standard applied toolkit-wide): `text` is the
257
+ source line with the secret value replaced by <redacted>, so
258
+ the value appears nowhere in the report; `kind` names the
259
+ credential pattern matched. Only the deliberate opt-in
260
+ show_secrets=True returns the line exactly as written.
261
+ """
262
+ pat = re.compile(
263
+ r"(?i)(password|passwd|pwd|token|secret|api[_-]?key)"
264
+ r"\s*[=:]\s*(['\"])([^'\"]{3,})")
265
+ out = []
266
+ for i, line in enumerate(path.read_text().splitlines(), 1):
267
+ m = pat.search(line)
268
+ if not m:
269
+ continue
270
+ kind = m.group(1).lower().replace("-", "_")
271
+ if kind == "apikey":
272
+ kind = "api_key"
273
+ text = line.strip()
274
+ if not show_secrets:
275
+ lead = len(line) - len(line.lstrip())
276
+ start = m.start(3) - lead
277
+ end = m.end(3) - lead
278
+ text = text[:start] + "<redacted>" + text[end:]
279
+ out.append((i, kind, text))
280
+ return out
281
+
282
+ def main_guard(self, path: Path) -> dict:
283
+ tree = self.parse(path)
284
+ has = False
285
+ for node in ast.walk(tree):
286
+ if isinstance(node, ast.If):
287
+ src = ast.dump(node.test)
288
+ if "__name__" in src and "__main__" in src:
289
+ has = True
290
+ return {"has_main_guard": has}
291
+
292
+ def globals_audit(self, path: Path) -> list:
293
+ tree = self.parse(path)
294
+ out = []
295
+ for node in tree.body:
296
+ if isinstance(node, ast.Assign):
297
+ val = node.value
298
+ mutable = isinstance(val, (ast.List, ast.Dict, ast.Set,
299
+ ast.ListComp, ast.DictComp, ast.SetComp))
300
+ names = [t.id for t in node.targets if isinstance(t, ast.Name)]
301
+ out.append((node.lineno, ", ".join(names),
302
+ "mutable" if mutable else "immutable"))
303
+ return out
304
+
305
+ def magic_numbers(self, path: Path) -> list:
306
+ COMMON = {0, 1, 2, -1, 3, 4, 8, 10, 16, 32, 64, 100, 1000}
307
+ tree = self.parse(path)
308
+ found = []
309
+ for node in ast.walk(tree):
310
+ if (isinstance(node, ast.Constant)
311
+ and isinstance(node.value, (int, float))
312
+ and not isinstance(node.value, bool)
313
+ and node.value not in COMMON):
314
+ found.append((node.lineno, node.value))
315
+ return sorted(set(found), key=lambda x: x[0])
316
+
317
+ def compare_symbols(self, a: Path, b: Path) -> dict:
318
+ try:
319
+ sa = {(k, n) for k, n, _ in self.outline(a)}
320
+ sb = {(k, n) for k, n, _ in self.outline(b)}
321
+ except SyntaxError as e:
322
+ return {"error": f"line {e.lineno}, col {e.offset}: {e.msg}"}
323
+ return {"only_a": sorted(sa - sb), "only_b": sorted(sb - sa),
324
+ "common": sorted(sa & sb)}
325
+
326
+ def tree_summary(self, root: Path) -> dict:
327
+ files = self.py_files(root)
328
+ total_lines = funcs = classes = invalid = 0
329
+ for p in files:
330
+ if not self.check_file(p)["valid"]:
331
+ invalid += 1
332
+ continue
333
+ text = p.read_text()
334
+ total_lines += len(text.splitlines())
335
+ for n in ast.walk(ast.parse(text)):
336
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
337
+ funcs += 1
338
+ elif isinstance(n, ast.ClassDef):
339
+ classes += 1
340
+ return {"files": len(files), "lines": total_lines,
341
+ "functions": funcs, "classes": classes, "invalid": invalid}
342
+
343
+ def health_score(self, path: Path) -> dict:
344
+ r = self.check_file(path)
345
+ if not r["valid"]:
346
+ return {"score": 0, "reason": f"syntax error at line {r.get('line')}"}
347
+ docs_missing = len(self.docstring_report(path))
348
+ bare = len(self.bare_excepts(path))
349
+ td = len(self.todos_of(path))
350
+ hotspots = len([c for _, _, c in self.complexity(path) if c > 10])
351
+ penalty = 5 * docs_missing + 10 * bare + 3 * td + 10 * hotspots
352
+ return {"score": max(0, 100 - penalty), "docstrings_missing": docs_missing,
353
+ "bare_excepts": bare, "todos": td, "complexity_hotspots": hotspots}
@@ -0,0 +1,5 @@
1
+ """Readwiz package exports."""
2
+
3
+ from .lib import read_wizard
4
+
5
+ __all__ = ["read_wizard"]
@@ -0,0 +1,15 @@
1
+ """Entry point for readwiz dispatch."""
2
+ import sys
3
+ from .lib import cli as internal_cli
4
+
5
+ def main(argv=None):
6
+ old_argv = sys.argv
7
+ if argv is not None:
8
+ sys.argv = [sys.argv[0]] + list(argv)
9
+ try:
10
+ return internal_cli.main()
11
+ finally:
12
+ sys.argv = old_argv
13
+
14
+ if __name__ == "__main__":
15
+ sys.exit(main())
@@ -0,0 +1,46 @@
1
+ """read-wizard face — thin CLI over the policy brain."""
2
+
3
+ import sys
4
+
5
+ from .lib.read_wizard import check_command, LAW_TEXT, ALLOW_VERBS, DENY_VERBS
6
+
7
+ USAGE = """Read Wizard — read-only mode policy engine (verb gate)
8
+ usage: readwiz check "<raw shell command>"
9
+ readwiz verbs dump the allow/deny tables
10
+ readwiz law print the READ LAW alone
11
+ verdicts: ALLOW / DENIED | exit 0 = allow, 1 = denied, 2 = usage error"""
12
+
13
+
14
+ def main():
15
+ args = sys.argv[1:]
16
+ if args and args[0] in ("-h", "--help"):
17
+ print(USAGE)
18
+ return 0
19
+ if not args or args[0] not in ("check", "verbs", "law"):
20
+ print(USAGE)
21
+ return 2
22
+
23
+ if args[0] == "law":
24
+ print(LAW_TEXT)
25
+ return 0
26
+
27
+ if args[0] == "verbs":
28
+ print("[ALLOW]")
29
+ for v in sorted(ALLOW_VERBS):
30
+ print(f" {v}")
31
+ print("[DENY]")
32
+ for v in sorted(DENY_VERBS):
33
+ print(f" {v}")
34
+ return 0
35
+
36
+ # check mode
37
+ if len(args) < 2:
38
+ print("[FAIL] readwiz check requires a quoted command string")
39
+ return 2
40
+
41
+ verdict, reason = check_command(" ".join(args[1:]))
42
+ if verdict == "ALLOW":
43
+ print(f"[ALLOW] {reason}")
44
+ return 0
45
+ print(f"[DENIED] {reason}")
46
+ return 1
@@ -0,0 +1 @@
1
+ # wizard package init
@@ -0,0 +1,47 @@
1
+ """read-wizard face — thin CLI over the policy brain."""
2
+
3
+ import sys
4
+
5
+ from .read_wizard import check_command, LAW_TEXT, ALLOW_VERBS, DENY_VERBS
6
+
7
+ USAGE = """Read Wizard — read-only mode policy engine (verb gate)
8
+ alias: readwiz
9
+ usage: read_wizard check "<raw shell command>"
10
+ read_wizard verbs dump the allow/deny tables
11
+ read_wizard law print the READ LAW alone
12
+ verdicts: ALLOW / DENIED | exit 0 = allow, 1 = denied, 2 = usage error"""
13
+
14
+
15
+ def main():
16
+ args = sys.argv[1:]
17
+ if args and args[0] in ("-h", "--help", "help"):
18
+ print(USAGE)
19
+ return 0
20
+ if not args or args[0] not in ("check", "verbs", "law"):
21
+ print(USAGE)
22
+ return 2
23
+
24
+ if args[0] == "law":
25
+ print(LAW_TEXT)
26
+ return 0
27
+
28
+ if args[0] == "verbs":
29
+ print("[ALLOW]")
30
+ for v in sorted(ALLOW_VERBS):
31
+ print(f" {v}")
32
+ print("[DENY]")
33
+ for v in sorted(DENY_VERBS):
34
+ print(f" {v}")
35
+ return 0
36
+
37
+ # check mode
38
+ if len(args) < 2:
39
+ print("[FAIL] read_wizard check requires a quoted command string")
40
+ return 2
41
+
42
+ verdict, reason = check_command(" ".join(args[1:]))
43
+ if verdict == "ALLOW":
44
+ print(f"[ALLOW] {reason}")
45
+ return 0
46
+ print(f"[DENIED] {reason}")
47
+ return 1
@@ -0,0 +1,121 @@
1
+ """read-wizard brain — the read-only policy engine.
2
+
3
+ READ LAW:
4
+ This wizard judges, it never acts. Given a raw shell command,
5
+ it answers ALLOW / DENIED / out of scope. Its tables are the
6
+ single source of truth for read-only mode in every REPL that
7
+ binds it. Mortal and stateless: one verdict per invocation.
8
+ """
9
+
10
+ import shlex
11
+
12
+ # ------------------------------------------------------------------
13
+ # THE VERB TABLES — the law itself. Amend only by Governor decree.
14
+ # ------------------------------------------------------------------
15
+
16
+ ALLOW_VERBS = frozenset({
17
+ # observation
18
+ "ls", "ll", "la", "cat", "head", "tail", "less", "more", "wc",
19
+ "file", "stat", "du", "df", "tree", "find", "locate", "which",
20
+ "whereis", "type", "echo", "date", "whoami", "hostname", "pwd",
21
+ "uname", "uptime", "env", "printenv", "id", "groups", "history",
22
+ # search
23
+ "grep", "rg", "fgrep", "ag", "awk", "cut", "sort", "uniq", "nl",
24
+ "column", "strings",
25
+ # fleet read-only citizens — other wizards ARE read verbs
26
+ "helpwiz", "pythonwiz", "diffwiz", "jsonwiz", "cmdwiz", "manualwiz",
27
+ "gitwiz", "mapwiz", # Governor decree 2026-09-18 — newborn inspectors admitted
28
+ "gitwiz", "mapwiz", # Governor decree 2026-09-18 — newborn inspectors admitted
29
+ "truthwiz", "ollamawiz", "probewiz", "rustwiz", "readwiz",
30
+ "benchwiz", "modelfilewiz", # exam/show/hash/verify/list only
31
+ # introspection
32
+ "jq", "git", # git gated separately (see GIT_DENY_SUBS)
33
+ "python3", # gated: only with -c check or read scripts
34
+ })
35
+
36
+ # git subverbs that mutate — git itself is allowed, these are not
37
+ GIT_DENY_SUBS = frozenset({
38
+ "push", "commit", "add", "reset", "checkout", "merge", "rebase",
39
+ "cherry-pick", "stash", "clean", "rm", "mv", "apply", "tag",
40
+ "restore", "switch", "worktree",
41
+ })
42
+
43
+ # absolute denies — anything touching these dies regardless of position
44
+ DENY_VERBS = frozenset({
45
+ "rm", "rmdir", "mv", "cp", "ln", "touch", "mkdir", "tee", "truncate",
46
+ "dd", "shred", "chmod", "chown", "chattr", "setfacl", "install",
47
+ "kill", "pkill", "shutdown", "reboot", "poweroff", "systemctl",
48
+ "service", "crontab", "useradd", "userdel", "passwd", "sudo", "su",
49
+ "doas", "mount", "umount", "umount", "mkfs", "pip", "pip3", "apt",
50
+ "apt-get", "snap", "npm", "curl", "wget", "scp", "rsync", "ssh",
51
+ "sftp", "nc", "socat", "bootwiz", # birthing citizens is Tier 2
52
+ })
53
+
54
+ # raw-string tokens that mean writing no matter which verb fronts them
55
+ WRITE_MARKERS = (">>", ">", "$(", "`")
56
+
57
+
58
+ def _first_verb(segment):
59
+ """Peel the first word of one pipe-segment, tolerating quoting."""
60
+ try:
61
+ words = shlex.split(segment)
62
+ except ValueError:
63
+ return None
64
+ words = [w for w in words if w and w != "sudo"]
65
+ return words[0] if words else None
66
+
67
+
68
+ def check_command(raw):
69
+ """Judge one raw shell line. Returns (verdict, reason).
70
+
71
+ verdict: 'ALLOW' | 'DENIED'
72
+ """
73
+ if not raw or not raw.strip():
74
+ return "DENIED", "empty command"
75
+ raw = raw.strip()
76
+
77
+ # stage 1 — write markers anywhere in the raw line
78
+ for marker in WRITE_MARKERS:
79
+ if marker in raw:
80
+ return "DENIED", f"write marker '{marker}' present"
81
+
82
+ # stage 2 — judge every segment of every pipe
83
+ for segment in raw.split("|"):
84
+ seg = segment.strip()
85
+ if not seg:
86
+ continue
87
+ verb = _first_verb(seg)
88
+ if verb is None:
89
+ return "DENIED", "unparseable segment"
90
+ # peel path prefixes: /usr/bin/grep -> grep
91
+ verb_clean = verb.rsplit("/", 1)[-1]
92
+
93
+ if verb_clean in DENY_VERBS:
94
+ return "DENIED", f"verb '{verb_clean}' is a write/mutate verb"
95
+ if verb_clean not in ALLOW_VERBS:
96
+ return "DENIED", f"verb '{verb_clean}' not on the read allowlist"
97
+
98
+ # git subverb gate
99
+ if verb_clean in ("git", "/usr/bin/git"):
100
+ try:
101
+ words = shlex.split(seg)
102
+ except ValueError:
103
+ return "DENIED", "unparseable git command"
104
+ subs = [w for w in words[1:] if not w.startswith("-")]
105
+ if subs and subs[0] in GIT_DENY_SUBS:
106
+ return "DENIED", f"git subverb '{subs[0]}' mutates state"
107
+
108
+ return "ALLOW", "read-only"
109
+
110
+
111
+ # ---------------- charter text ----------------
112
+
113
+ LAW_TEXT = """READ LAW (Read Wizard, the verb gate)
114
+ 1. The Read Wizard judges; it never executes. REPLs bind it as policy engine.
115
+ 2. ALLOW means read-only: observation, search, fleet read-only citizens.
116
+ 3. Redirection (>, >>), substitution ($(), `) and every write verb: DENIED.
117
+ 4. Unknown verbs default DENIED — absence from the table is not permission.
118
+ 5. git is read-only ONLY for its observing subverbs (status, log, diff, show...).
119
+ 6. Tables amend only by Governor decree, witnessed by commit.
120
+ 7. Mortal and stateless: one verdict, then death. No memory of commands seen.
121
+ """
@@ -0,0 +1,5 @@
1
+ """Rustwiz package exports."""
2
+
3
+ from .lib import rust_wizard
4
+
5
+ __all__ = ["rust_wizard"]
@@ -0,0 +1,7 @@
1
+ """Entry point for wiz dispatch."""
2
+ import sys
3
+ from .lib.cli import main
4
+ from .lib.rust_wizard import RustWizard # noqa: F401 (re-exported for tests)
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,4 @@
1
+ """rustwiz CLI entry point; the implementation lives in lib/cli.py."""
2
+ from .lib.cli import main
3
+
4
+ __all__ = ["main"]
@@ -0,0 +1 @@
1
+ # wizard package init
@@ -0,0 +1,53 @@
1
+ import sys
2
+ try:
3
+ from .rust_wizard import LAW_TEXT, RustWizard, package_version
4
+ except ImportError: # run directly as a script, outside the package
5
+ sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
6
+ from rust_wizard import LAW_TEXT, RustWizard, package_version
7
+
8
+ USAGE = """Rust Wizard — Rust project checker
9
+ alias: rustwiz
10
+ usage: rust_wizard check <file.rs|dir> ...
11
+ rust_wizard build <crate-dir> cargo check the crate
12
+ rust_wizard run <crate-dir> build and run the crate's binary
13
+ rust_wizard test <crate-dir> cargo test
14
+ rust_wizard clippy <crate-dir> cargo clippy lints
15
+ rust_wizard fmt <crate-dir> cargo fmt --check (reports drift only)
16
+ rust_wizard doc <crate-dir> cargo doc --no-deps
17
+ rust_wizard clean <crate-dir> cargo clean (removes target/ only)
18
+ rust_wizard law print the RUST LAW alone
19
+ rust_wizard version print the version
20
+ rust_wizard --help | --version"""
21
+
22
+ CRATE_COMMANDS = ("build", "run", "test", "clippy", "fmt", "doc", "clean")
23
+
24
+
25
+ def main(argv=None):
26
+ argv = list(sys.argv[1:] if argv is None else argv)
27
+ if argv and argv[0] in ("-h", "--help", "help"):
28
+ print(USAGE)
29
+ return 0
30
+ if argv and argv[0] == "--version":
31
+ print(f"rust_wizard {package_version()}")
32
+ return 0
33
+ if not argv:
34
+ print(f"out of scope - usage:\n{USAGE}")
35
+ return 2
36
+ cmd, args = argv[0], argv[1:]
37
+ if cmd == "law":
38
+ print(LAW_TEXT)
39
+ return 0
40
+ if cmd == "version":
41
+ print(f"rust_wizard {package_version()}")
42
+ return 0
43
+ wz = RustWizard()
44
+ if cmd == "check" and args:
45
+ return wz.check(args)
46
+ if cmd in CRATE_COMMANDS and args:
47
+ return getattr(wz, cmd)(args[0])
48
+ print(f"out of scope - unknown or incomplete command: {' '.join(argv)}")
49
+ return 2
50
+
51
+
52
+ if __name__ == "__main__":
53
+ sys.exit(main())